fix(coding-agent): refresh session state before next turn
This commit is contained in:
@@ -21,6 +21,7 @@ import type {
|
|||||||
AgentTool,
|
AgentTool,
|
||||||
BeforeToolCallContext,
|
BeforeToolCallContext,
|
||||||
BeforeToolCallResult,
|
BeforeToolCallResult,
|
||||||
|
PrepareNextTurnContext,
|
||||||
QueueMode,
|
QueueMode,
|
||||||
StreamFn,
|
StreamFn,
|
||||||
ToolExecutionMode,
|
ToolExecutionMode,
|
||||||
@@ -104,6 +105,7 @@ export interface AgentOptions {
|
|||||||
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
|
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
|
||||||
afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
|
afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
|
||||||
prepareNextTurn?: (
|
prepareNextTurn?: (
|
||||||
|
context: PrepareNextTurnContext,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
||||||
steeringMode?: QueueMode;
|
steeringMode?: QueueMode;
|
||||||
@@ -184,6 +186,7 @@ export class Agent {
|
|||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => Promise<AfterToolCallResult | undefined>;
|
) => Promise<AfterToolCallResult | undefined>;
|
||||||
public prepareNextTurn?: (
|
public prepareNextTurn?: (
|
||||||
|
context: PrepareNextTurnContext,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
||||||
private activeRun?: ActiveRun;
|
private activeRun?: ActiveRun;
|
||||||
@@ -433,7 +436,9 @@ export class Agent {
|
|||||||
toolExecution: this.toolExecution,
|
toolExecution: this.toolExecution,
|
||||||
beforeToolCall: this.beforeToolCall,
|
beforeToolCall: this.beforeToolCall,
|
||||||
afterToolCall: this.afterToolCall,
|
afterToolCall: this.afterToolCall,
|
||||||
prepareNextTurn: this.prepareNextTurn ? async () => await this.prepareNextTurn?.(this.signal) : undefined,
|
prepareNextTurn: this.prepareNextTurn
|
||||||
|
? async (context) => await this.prepareNextTurn?.(context, this.signal)
|
||||||
|
: undefined,
|
||||||
convertToLlm: this.convertToLlm,
|
convertToLlm: this.convertToLlm,
|
||||||
transformContext: this.transformContext,
|
transformContext: this.transformContext,
|
||||||
getApiKey: this.getApiKey,
|
getApiKey: this.getApiKey,
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ export class AgentSession {
|
|||||||
// (session persistence, extensions, auto-compaction, retry logic)
|
// (session persistence, extensions, auto-compaction, retry logic)
|
||||||
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
||||||
this._installAgentToolHooks();
|
this._installAgentToolHooks();
|
||||||
|
this._installAgentNextTurnRefresh();
|
||||||
|
|
||||||
this._buildRuntime({
|
this._buildRuntime({
|
||||||
activeToolNames: this._initialActiveToolNames,
|
activeToolNames: this._initialActiveToolNames,
|
||||||
@@ -462,6 +463,25 @@ export class AgentSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _installAgentNextTurnRefresh(): void {
|
||||||
|
const previousPrepareNextTurn = this.agent.prepareNextTurn;
|
||||||
|
this.agent.prepareNextTurn = async (turn, signal) => {
|
||||||
|
const previousSnapshot = await previousPrepareNextTurn?.(turn, signal);
|
||||||
|
const previousContext = previousSnapshot?.context ?? turn.context;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previousSnapshot,
|
||||||
|
context: {
|
||||||
|
...previousContext,
|
||||||
|
systemPrompt: this.agent.state.systemPrompt,
|
||||||
|
tools: this.agent.state.tools.slice(),
|
||||||
|
},
|
||||||
|
model: this.agent.state.model,
|
||||||
|
thinkingLevel: this.agent.state.thinkingLevel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Event Subscription
|
// Event Subscription
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ExtensionFactory } from "../../../src/index.ts";
|
||||||
|
import { createHarness } from "../harness.ts";
|
||||||
|
|
||||||
|
describe("extension active tools next-turn refresh", () => {
|
||||||
|
it("applies pi.setActiveTools before the next provider request in the same run", async () => {
|
||||||
|
const extensionFactories: ExtensionFactory[] = [
|
||||||
|
(pi) => {
|
||||||
|
pi.registerTool({
|
||||||
|
name: "switch_tools",
|
||||||
|
label: "Switch Tools",
|
||||||
|
description: "Switch the active extension tool set",
|
||||||
|
promptSnippet: "Switch to the next extension tool",
|
||||||
|
parameters: Type.Object({}),
|
||||||
|
execute: async () => {
|
||||||
|
pi.setActiveTools(["after_switch"]);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "switched" }],
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerTool({
|
||||||
|
name: "after_switch",
|
||||||
|
label: "After Switch",
|
||||||
|
description: "Tool that should be available after switching",
|
||||||
|
promptSnippet: "Run after the active tool set changes",
|
||||||
|
parameters: Type.Object({}),
|
||||||
|
execute: async () => ({
|
||||||
|
content: [{ type: "text", text: "after" }],
|
||||||
|
details: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const harness = await createHarness({
|
||||||
|
extensionFactories,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
harness.session.setActiveToolsByName(["switch_tools"]);
|
||||||
|
|
||||||
|
const providerToolNames: string[][] = [];
|
||||||
|
harness.setResponses([
|
||||||
|
(context) => {
|
||||||
|
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
|
||||||
|
return fauxAssistantMessage(fauxToolCall("switch_tools", {}), { stopReason: "toolUse" });
|
||||||
|
},
|
||||||
|
(context) => {
|
||||||
|
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
|
||||||
|
return fauxAssistantMessage("done");
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(harness.session.getActiveToolNames()).toEqual(["switch_tools"]);
|
||||||
|
|
||||||
|
await harness.session.prompt("start");
|
||||||
|
|
||||||
|
expect(harness.session.getActiveToolNames()).toEqual(["after_switch"]);
|
||||||
|
expect(providerToolNames).toEqual([["switch_tools"], ["after_switch"]]);
|
||||||
|
} finally {
|
||||||
|
harness.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user