fix(prompting): refresh bash guidance on tool changes
Signed-off-by: Christian Stewart <christian@aperture.us>
This commit is contained in:
@@ -1525,10 +1525,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
// entries capture it at fetch time and are dropped at injection if a newer
|
||||
// mutation (any tool) bumped it in the meantime.
|
||||
const fileMutationVersions = new Map<string, number>();
|
||||
const activeToolNames = new Set<string>();
|
||||
const setActiveToolNames = (names: Iterable<string>): void => {
|
||||
activeToolNames.clear();
|
||||
for (const name of names) {
|
||||
activeToolNames.add(name);
|
||||
}
|
||||
};
|
||||
const toolSession: ToolSession = {
|
||||
get cwd() {
|
||||
return sessionManager.getCwd();
|
||||
},
|
||||
isToolActive: name => activeToolNames.has(name),
|
||||
setActiveToolNames,
|
||||
hasUI: options.hasUI ?? false,
|
||||
enableLsp,
|
||||
get hasEditTool() {
|
||||
@@ -2540,6 +2549,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
});
|
||||
hasRegistered = true;
|
||||
|
||||
setActiveToolNames(initialToolNames);
|
||||
const { systemPrompt } = await logger.time(
|
||||
"buildSystemPrompt",
|
||||
rebuildSystemPrompt,
|
||||
@@ -2834,6 +2844,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
rebuildSystemPrompt,
|
||||
reloadSshTool,
|
||||
requestedToolNames: requestedToolNameSet,
|
||||
setActiveToolNames,
|
||||
getMcpServerInstructions: mcpManager
|
||||
? () => {
|
||||
const raw = mcpManager.getServerInstructions();
|
||||
|
||||
@@ -686,6 +686,8 @@ export interface AgentSessionConfig {
|
||||
toolRegistry?: Map<string, AgentTool>;
|
||||
/** Tool names whose current registry entry is still the built-in implementation. */
|
||||
builtInToolNames?: Iterable<string>;
|
||||
/** Update tool-session predicates that render guidance from the live active tool set. */
|
||||
setActiveToolNames?: (names: Iterable<string>) => void;
|
||||
/** Current session pre-LLM message transform pipeline */
|
||||
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
||||
/**
|
||||
@@ -1717,6 +1719,7 @@ export class AgentSession {
|
||||
| undefined;
|
||||
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
||||
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
||||
#setActiveToolNames: ((names: Iterable<string>) => void) | undefined;
|
||||
#disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
|
||||
#requestedToolNames: ReadonlySet<string> | undefined;
|
||||
#baseSystemPrompt: string[];
|
||||
@@ -2155,6 +2158,7 @@ export class AgentSession {
|
||||
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
||||
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
||||
this.#reloadSshTool = config.reloadSshTool;
|
||||
this.#setActiveToolNames = config.setActiveToolNames;
|
||||
this.#disconnectOwnedMcpManager = config.disconnectOwnedMcpManager;
|
||||
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
||||
this.#promptModelKey = this.#currentPromptModelKey();
|
||||
@@ -6290,6 +6294,7 @@ export class AgentSession {
|
||||
),
|
||||
);
|
||||
}
|
||||
this.#setActiveToolNames?.(validToolNames);
|
||||
const activeNameSet = new Set(validToolNames);
|
||||
for (const name of Array.from(this.#selectedDiscoveredToolNames)) {
|
||||
if (!activeNameSet.has(name) || isMCPToolName(name) || !this.#toolRegistry.has(name)) {
|
||||
@@ -6395,6 +6400,7 @@ export class AgentSession {
|
||||
async refreshBaseSystemPrompt(): Promise<void> {
|
||||
if (!this.#rebuildSystemPrompt) return;
|
||||
const activeToolNames = this.getActiveToolNames();
|
||||
this.#setActiveToolNames?.(activeToolNames);
|
||||
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
||||
this.#baseSystemPrompt = built.systemPrompt;
|
||||
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
||||
|
||||
@@ -376,7 +376,21 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
||||
};
|
||||
readonly label = "Bash";
|
||||
readonly loadMode = "essential";
|
||||
readonly description: string;
|
||||
get description(): string {
|
||||
const evalBackends = resolveEvalBackends(this.session);
|
||||
const isToolActive = (name: string, fallback: boolean): boolean => this.session.isToolActive?.(name) ?? fallback;
|
||||
return prompt.render(bashDescription, {
|
||||
asyncEnabled: this.#asyncEnabled,
|
||||
autoBackgroundEnabled: this.#autoBackgroundEnabled,
|
||||
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
|
||||
hasAstGrep: isToolActive("ast_grep", this.session.settings.get("astGrep.enabled")),
|
||||
hasAstEdit: isToolActive("ast_edit", this.session.settings.get("astEdit.enabled")),
|
||||
hasGrep: isToolActive("grep", this.session.settings.get("grep.enabled")),
|
||||
hasGlob: isToolActive("glob", this.session.settings.get("glob.enabled")),
|
||||
hasRead: isToolActive("read", true),
|
||||
hasEval: isToolActive("eval", evalBackends.python || evalBackends.js || evalBackends.ruby || evalBackends.julia),
|
||||
});
|
||||
}
|
||||
readonly parameters: BashToolSchema;
|
||||
// Non-pty calls run alongside each other (the executor isolates overlapping
|
||||
// runs on the same shell session); pty takes over the terminal UI and must
|
||||
@@ -398,19 +412,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
||||
),
|
||||
);
|
||||
this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
|
||||
const evalBackends = resolveEvalBackends(this.session);
|
||||
const isToolActive = (name: string, fallback: boolean): boolean => this.session.isToolActive?.(name) ?? fallback;
|
||||
this.description = prompt.render(bashDescription, {
|
||||
asyncEnabled: this.#asyncEnabled,
|
||||
autoBackgroundEnabled: this.#autoBackgroundEnabled,
|
||||
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
|
||||
hasAstGrep: isToolActive("ast_grep", this.session.settings.get("astGrep.enabled")),
|
||||
hasAstEdit: isToolActive("ast_edit", this.session.settings.get("astEdit.enabled")),
|
||||
hasGrep: isToolActive("grep", this.session.settings.get("grep.enabled")),
|
||||
hasGlob: isToolActive("glob", this.session.settings.get("glob.enabled")),
|
||||
hasRead: isToolActive("read", true),
|
||||
hasEval: isToolActive("eval", evalBackends.python || evalBackends.js || evalBackends.ruby || evalBackends.julia),
|
||||
});
|
||||
}
|
||||
|
||||
#formatResultOutput(result: BashResult | BashInteractiveResult): string {
|
||||
|
||||
@@ -226,6 +226,8 @@ export interface ToolSession {
|
||||
getToolByName?: (name: string) => AgentTool | undefined;
|
||||
/** Return whether a built-in tool is active in this turn's tool set. */
|
||||
isToolActive?: (name: string) => boolean;
|
||||
/** Update the active built-in tool predicate when a session changes tools mid-run. */
|
||||
setActiveToolNames?: (names: Iterable<string>) => void;
|
||||
/** Agent registry for IRC routing across live sessions. */
|
||||
agentRegistry?: AgentRegistry;
|
||||
/** Get artifacts directory for artifact:// URLs */
|
||||
@@ -650,7 +652,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
||||
];
|
||||
|
||||
const activeToolNames = new Set(baseEntries.map(([name]) => name));
|
||||
session.isToolActive = name => activeToolNames.has(name);
|
||||
if (session.setActiveToolNames) {
|
||||
session.setActiveToolNames(activeToolNames);
|
||||
} else {
|
||||
session.isToolActive = name => activeToolNames.has(name);
|
||||
}
|
||||
|
||||
const baseResults = await Promise.all(
|
||||
baseEntries.map(async ([name, factory]) => {
|
||||
|
||||
@@ -176,6 +176,52 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => {
|
||||
expect(rebuildCount).toBe(baseline + 2);
|
||||
});
|
||||
|
||||
it("updates live active-tool predicates before rebuilding the prompt", async () => {
|
||||
const activeToolNames = new Set(["read", "bash", "grep"]);
|
||||
const readTool = createBasicTool("read", "Read");
|
||||
const bashTool = createBasicTool("bash", "Bash");
|
||||
const grepTool = createBasicTool("grep", "Grep");
|
||||
Object.defineProperty(bashTool, "description", {
|
||||
get: () => (activeToolNames.has("grep") ? "bash sees grep" : "bash hides grep"),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
const toolRegistry = new Map<string, AgentTool>([
|
||||
[readTool.name, readTool],
|
||||
[bashTool.name, bashTool],
|
||||
[grepTool.name, grepTool],
|
||||
]);
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
model: createModel(),
|
||||
systemPrompt: ["initial"],
|
||||
tools: [readTool, bashTool, grepTool],
|
||||
messages: [],
|
||||
},
|
||||
});
|
||||
const session = new AgentSession({
|
||||
agent,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settings: Settings.isolated({ "compaction.enabled": false }),
|
||||
modelRegistry: {} as never,
|
||||
toolRegistry,
|
||||
setActiveToolNames: names => {
|
||||
activeToolNames.clear();
|
||||
for (const name of names) {
|
||||
activeToolNames.add(name);
|
||||
}
|
||||
},
|
||||
rebuildSystemPrompt: async (_toolNames, tools) => ({
|
||||
systemPrompt: [tools.get("bash")?.description ?? "missing bash"],
|
||||
}),
|
||||
});
|
||||
sessions.push(session);
|
||||
|
||||
await session.setActiveToolsByName(["read", "bash"]);
|
||||
|
||||
expect(agent.state.systemPrompt).toEqual(["bash hides grep"]);
|
||||
});
|
||||
|
||||
it("does not skip when refreshBaseSystemPrompt is called explicitly", async () => {
|
||||
let rebuildCount = 0;
|
||||
const { session } = newSession(async toolNames => {
|
||||
|
||||
@@ -273,6 +273,28 @@ describe("createTools", () => {
|
||||
expect(session.isToolActive?.("read")).toBe(false);
|
||||
});
|
||||
|
||||
it("renders bash guidance from the live active tool predicate", async () => {
|
||||
const activeToolNames = new Set<string>();
|
||||
const session = createTestSession({
|
||||
isToolActive: name => activeToolNames.has(name),
|
||||
setActiveToolNames: names => {
|
||||
activeToolNames.clear();
|
||||
for (const name of names) {
|
||||
activeToolNames.add(name);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const tools = await createTools(session, ["bash", "grep", "read", "glob"]);
|
||||
const bash = tools.find(tool => tool.name === "bash");
|
||||
|
||||
expect(bash?.description).toContain("`grep` tool");
|
||||
session.setActiveToolNames?.(["bash"]);
|
||||
expect(bash?.description).not.toContain("`grep` tool");
|
||||
expect(bash?.description).not.toContain("`ls` → `read`");
|
||||
expect(bash?.description).not.toContain("`find` → the `glob` tool");
|
||||
});
|
||||
|
||||
it("includes search_tool_bm25 when MCP tool discovery is enabled and executable", async () => {
|
||||
const session = createTestSession({
|
||||
settings: createSettingsWithOverrides({
|
||||
|
||||
Reference in New Issue
Block a user