mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(plugins): emit agent_settled when a session run fully quiesces (#110174)
This commit is contained in:
committed by
GitHub
parent
ac6fa704e8
commit
f7e386d26d
@@ -18,7 +18,7 @@ ec22d7a039fb58d0b8343ad149322960d3d8ca58b3f4c70f2fa8a099f8186d0c module/agent-h
|
||||
5f63bf587bf3547d59d0dc5d0dc2fee54745aa6edaab4aa3ae700dba03443edb module/agent-harness-tool-runtime
|
||||
5168648cd946abad8a92822889f13ceacc87ed502314a66190d0b1eb8ebe76ea module/agent-media-payload
|
||||
7e43370bef8d69479f958ee5f8c72d2c227b8d638e04857018439f7b34d4b116 module/agent-runtime
|
||||
df9ef9807f5b7563885c2a77a621103a817f6c0690d2204ab10a1e42a049c060 module/agent-sessions
|
||||
d24a44ad9030f4657fa339287ac342724a9c0969f2a390b915c82359345a568c module/agent-sessions
|
||||
dd9282f1eeadf44db2887599b52d80db7f5fb99c6d9eac720dbf1b77065f2145 module/allow-from
|
||||
55cea5390d68839ca7768b4a0cc570b17b65fa0fa3bc4d76130ef0f16cb79ede module/allowlist-config-edit
|
||||
7ddd81bd5f55de9adf64bf4d92d012f24b37b6da0a72805a3a220d8feff24ca3 module/approval-auth-runtime
|
||||
|
||||
@@ -201,6 +201,24 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("AgentSession loop correctness", () => {
|
||||
it("emits agent_settled once after a normal run", async () => {
|
||||
const lifecycleEvents: string[] = [];
|
||||
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>([
|
||||
["agent_end", [async () => lifecycleEvents.push("agent_end")]],
|
||||
["agent_settled", [async () => lifecycleEvents.push("agent_settled")]],
|
||||
]);
|
||||
streamMocks.streamSimple.mockImplementation((activeModel: Model) =>
|
||||
createAssistantResultStream(
|
||||
createAssistant(activeModel, [{ type: "text", text: "complete answer" }]),
|
||||
),
|
||||
);
|
||||
const { session } = await createTestSession({ resourceLoader: createResourceLoader(handlers) });
|
||||
|
||||
await session.prompt("new prompt");
|
||||
|
||||
expect(lifecycleEvents).toEqual(["agent_end", "agent_settled"]);
|
||||
});
|
||||
|
||||
it("manually compacts a completed turn smaller than the retained-token budget", async () => {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
appendHistory(
|
||||
@@ -383,11 +401,13 @@ describe("AgentSession loop correctness", () => {
|
||||
it("drains a follow-up queued by an agent-end handler", async () => {
|
||||
const sessionRef: { current?: AgentSession } = {};
|
||||
let queued = false;
|
||||
const lifecycleEvents: string[] = [];
|
||||
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>([
|
||||
[
|
||||
"agent_end",
|
||||
[
|
||||
async () => {
|
||||
lifecycleEvents.push("agent_end");
|
||||
if (!queued) {
|
||||
queued = true;
|
||||
await sessionRef.current?.followUp("queued after end");
|
||||
@@ -396,6 +416,7 @@ describe("AgentSession loop correctness", () => {
|
||||
},
|
||||
],
|
||||
],
|
||||
["agent_settled", [async () => lifecycleEvents.push("agent_settled")]],
|
||||
]);
|
||||
const requests: Context[] = [];
|
||||
streamMocks.streamSimple.mockImplementation((activeModel: Model, context: Context) => {
|
||||
@@ -412,10 +433,15 @@ describe("AgentSession loop correctness", () => {
|
||||
expect(requests).toHaveLength(2);
|
||||
expect(JSON.stringify(requests[1]?.messages)).toContain("queued after end");
|
||||
expect(session.agent.hasQueuedMessages()).toBe(false);
|
||||
expect(lifecycleEvents).toEqual(["agent_end", "agent_end", "agent_settled"]);
|
||||
});
|
||||
|
||||
it("leaves queued messages dormant after a turn handoff", async () => {
|
||||
const sessionRef: { current?: AgentSession } = {};
|
||||
const settled = vi.fn();
|
||||
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>([
|
||||
["agent_settled", [async () => settled()]],
|
||||
]);
|
||||
const yieldTool: ToolDefinition = {
|
||||
name: "yield_turn",
|
||||
label: "Yield turn",
|
||||
@@ -446,13 +472,17 @@ describe("AgentSession loop correctness", () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { session } = await createTestSession({ customTools: [yieldTool] });
|
||||
const { session } = await createTestSession({
|
||||
customTools: [yieldTool],
|
||||
resourceLoader: createResourceLoader(handlers),
|
||||
});
|
||||
sessionRef.current = session;
|
||||
|
||||
await session.prompt("yield now");
|
||||
|
||||
expect(streamMocks.streamSimple).toHaveBeenCalledOnce();
|
||||
expect(session.agent.hasQueuedMessages()).toBe(true);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
session.agent.clearAllQueues();
|
||||
});
|
||||
|
||||
|
||||
@@ -14,35 +14,53 @@ import type { CustomMessage } from "./messages.js";
|
||||
import { expandPromptTemplate } from "./prompt-templates.js";
|
||||
import type { ResourceLoader } from "./resource-loader.js";
|
||||
|
||||
type PostAgentRunAction = "continue" | "settled" | "handoff";
|
||||
|
||||
export abstract class AgentSessionPrompting extends AgentSessionBase {
|
||||
// =========================================================================
|
||||
// Prompting
|
||||
// =========================================================================
|
||||
|
||||
private async runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
|
||||
let endedForTurnHandoff = false;
|
||||
try {
|
||||
await this.agent.prompt(messages);
|
||||
while (await this.handlePostAgentRun()) {
|
||||
while (true) {
|
||||
const action = await this.handlePostAgentRun();
|
||||
if (action !== "continue") {
|
||||
endedForTurnHandoff = action === "handoff";
|
||||
break;
|
||||
}
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this.systemPromptOverride = undefined;
|
||||
this.flushPendingBashMessages();
|
||||
// Consume handoff state before callbacks can start a nested run and set it again.
|
||||
endedForTurnHandoff ||= this.lastRunEndedForTurnHandoff;
|
||||
this.lastRunEndedForTurnHandoff = false;
|
||||
// Failed or aborted runs can still be idle; only handoff leaves external delivery pending.
|
||||
if (!endedForTurnHandoff) {
|
||||
await this.currentExtensionRunner.emit({ type: "agent_settled" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePostAgentRun(): Promise<boolean> {
|
||||
private async handlePostAgentRun(): Promise<PostAgentRunAction> {
|
||||
const msg = this.lastAssistantMessage;
|
||||
this.lastAssistantMessage = undefined;
|
||||
const endedForTurnHandoff = this.lastRunEndedForTurnHandoff;
|
||||
this.lastRunEndedForTurnHandoff = false;
|
||||
if (!msg || endedForTurnHandoff) {
|
||||
if (endedForTurnHandoff) {
|
||||
// External delivery owns the next run after a deliberate turn handoff.
|
||||
return false;
|
||||
return "handoff";
|
||||
}
|
||||
if (!msg) {
|
||||
return "settled";
|
||||
}
|
||||
|
||||
if (this.isRetryableError(msg) && (await this.prepareRetry(msg))) {
|
||||
return true;
|
||||
return "continue";
|
||||
}
|
||||
|
||||
if (msg.stopReason === "error" && this.retryCount > 0) {
|
||||
@@ -56,11 +74,11 @@ export abstract class AgentSessionPrompting extends AgentSessionBase {
|
||||
}
|
||||
|
||||
if (await this.checkCompaction(msg)) {
|
||||
return true;
|
||||
return "continue";
|
||||
}
|
||||
|
||||
// Messages queued by agent_end handlers arrive after the loop's final queue drain.
|
||||
return this.agent.hasQueuedMessages();
|
||||
return this.agent.hasQueuedMessages() ? "continue" : "settled";
|
||||
}
|
||||
|
||||
private createUserContent(
|
||||
|
||||
@@ -717,6 +717,11 @@ interface AgentEndEvent {
|
||||
messages: AgentMessage[];
|
||||
}
|
||||
|
||||
/** Fired once the session has no automatic retry, compaction, or queued continuation left. */
|
||||
interface AgentSettledEvent {
|
||||
type: "agent_settled";
|
||||
}
|
||||
|
||||
/** Fired at the start of each turn */
|
||||
export interface TurnStartEvent {
|
||||
type: "turn_start";
|
||||
@@ -1047,6 +1052,7 @@ export type ExtensionEvent =
|
||||
| BeforeAgentStartEvent
|
||||
| AgentStartEvent
|
||||
| AgentEndEvent
|
||||
| AgentSettledEvent
|
||||
| TurnStartEvent
|
||||
| TurnEndEvent
|
||||
| MessageStartEvent
|
||||
@@ -1218,6 +1224,7 @@ export interface ExtensionAPI {
|
||||
): void;
|
||||
on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
|
||||
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
||||
on(event: "agent_settled", handler: ExtensionHandler<AgentSettledEvent>): void;
|
||||
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
||||
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
||||
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
|
||||
|
||||
Reference in New Issue
Block a user