mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-10-21 14:21:48 +00:00
fix(core): retain deferred Agent guidance
This commit is contained in:
parent
dc84c2e615
commit
091672639f
5 changed files with 69 additions and 15 deletions
|
|
@ -2892,6 +2892,7 @@ export class Config {
|
|||
* built. See {@link getPromptToolSnapshot}.
|
||||
*/
|
||||
private promptToolSnapshot: ReadonlySet<string> | undefined;
|
||||
private promptAgentReachable = false;
|
||||
|
||||
/**
|
||||
* Volatile system-prompt layer: the managed auto-memory section
|
||||
|
|
@ -8661,6 +8662,14 @@ export class Config {
|
|||
this.promptToolSnapshot = names;
|
||||
}
|
||||
|
||||
getPromptAgentReachable(): boolean {
|
||||
return this.promptAgentReachable;
|
||||
}
|
||||
|
||||
setPromptAgentReachable(reachable: boolean): void {
|
||||
this.promptAgentReachable = reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The managed auto-memory section of the system prompt (volatile layer).
|
||||
* Empty when managed memory is unavailable. Callers assembling a system
|
||||
|
|
|
|||
|
|
@ -652,6 +652,7 @@ describe('Gemini Client (client.ts)', () => {
|
|||
isTodoWriteEnabled: vi.fn().mockReturnValue(false),
|
||||
getStaticSystemPrefix: vi.fn().mockReturnValue(undefined),
|
||||
setStaticSystemPrefix: vi.fn(),
|
||||
setPromptAgentReachable: vi.fn(),
|
||||
getFullContext: vi.fn().mockReturnValue(false),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
takeActiveTodoReminder: vi.fn().mockReturnValue(undefined),
|
||||
|
|
@ -1816,6 +1817,7 @@ describe('Gemini Client (client.ts)', () => {
|
|||
// can stub the deferred-summary + bridge availability per case.
|
||||
function getRegistryMock() {
|
||||
return vi.mocked(mockConfig.getToolRegistry)() as unknown as {
|
||||
getFunctionDeclarations: ReturnType<typeof vi.fn>;
|
||||
getDeferredToolSummary: ReturnType<typeof vi.fn>;
|
||||
getTool: ReturnType<typeof vi.fn>;
|
||||
isDeferredToolRevealed: ReturnType<typeof vi.fn>;
|
||||
|
|
@ -1825,6 +1827,28 @@ describe('Gemini Client (client.ts)', () => {
|
|||
};
|
||||
}
|
||||
|
||||
it('records bridge-reachable Agent for prompt guidance', async () => {
|
||||
const reg = getRegistryMock();
|
||||
reg.getFunctionDeclarations.mockReturnValue([
|
||||
{ name: ToolNames.TOOL_SEARCH },
|
||||
{ name: ToolNames.TOOL_CALL },
|
||||
]);
|
||||
reg.getDeferredToolSummary.mockReturnValue([
|
||||
{ name: ToolNames.AGENT, description: 'delegate work' },
|
||||
]);
|
||||
reg.getTool.mockImplementation((name: string) =>
|
||||
name === ToolNames.TOOL_SEARCH || name === ToolNames.TOOL_CALL
|
||||
? ({} as never)
|
||||
: null,
|
||||
);
|
||||
const setReachable = vi.mocked(mockConfig.setPromptAgentReachable);
|
||||
setReachable.mockClear();
|
||||
|
||||
await client.startChat();
|
||||
|
||||
expect(setReachable).toHaveBeenLastCalledWith(true);
|
||||
});
|
||||
|
||||
it('re-reveals deferred tools that appear in resumed history', async () => {
|
||||
// Resume contract: a transcript referencing `cron_create` (a
|
||||
// deferred tool) must re-reveal it on startChat so the API
|
||||
|
|
|
|||
|
|
@ -395,7 +395,10 @@ type MainSessionPromptConfig = Pick<
|
|||
Partial<
|
||||
Pick<
|
||||
Config,
|
||||
'isTrustedFolder' | 'getPromptToolSnapshot' | 'getShellExecutionSandbox'
|
||||
| 'isTrustedFolder'
|
||||
| 'getPromptToolSnapshot'
|
||||
| 'getPromptAgentReachable'
|
||||
| 'getShellExecutionSandbox'
|
||||
>
|
||||
>;
|
||||
|
||||
|
|
@ -419,6 +422,7 @@ export function getMainSessionBaseSystemPrompt(
|
|||
config.getCodeModeOnly(),
|
||||
{
|
||||
declaredTools: config.getPromptToolSnapshot?.(),
|
||||
agentReachable: config.getPromptAgentReachable?.(),
|
||||
executionSandboxFilesystem:
|
||||
config.getShellExecutionSandbox?.()?.filesystem,
|
||||
executionSandboxBackend:
|
||||
|
|
@ -2695,13 +2699,18 @@ export class LlmClient {
|
|||
// Optional call: partial Config stubs (tests, derived agent shims) do not
|
||||
// carry the setter, and a missing snapshot simply leaves the prompt
|
||||
// ungated rather than failing session startup.
|
||||
this.config.setPromptToolSnapshot?.(
|
||||
new Set(
|
||||
toolRegistry
|
||||
.getFunctionDeclarations()
|
||||
.map((declaration) => declaration.name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
),
|
||||
const declaredTools = new Set(
|
||||
toolRegistry
|
||||
.getFunctionDeclarations()
|
||||
.map((declaration) => declaration.name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
);
|
||||
this.config.setPromptToolSnapshot?.(declaredTools);
|
||||
this.config.setPromptAgentReachable?.(
|
||||
declaredTools.has(ToolNames.AGENT) ||
|
||||
(declaredTools.has(ToolNames.TOOL_SEARCH) &&
|
||||
declaredTools.has(ToolNames.TOOL_CALL) &&
|
||||
deferredSummary.some(({ name }) => name === ToolNames.AGENT)),
|
||||
);
|
||||
const deferredTools = profiler.timeSync('deferred_reminder_setup', () => {
|
||||
const resolved = this.resolveDeferredToolsForReminder(deferredSummary);
|
||||
|
|
|
|||
|
|
@ -1736,7 +1736,7 @@ describe('resident tool gating (#12032)', () => {
|
|||
expect(codeModePrompt(new Set([ToolNames.EXEC]))).toBe(codeModePrompt());
|
||||
});
|
||||
|
||||
it('takes the declared set from the Config snapshot', () => {
|
||||
it('keeps Agent guidance when Agent is bridge-reachable', () => {
|
||||
const base = {
|
||||
getSystemPrompt: () => undefined,
|
||||
getModel: () => 'gpt-4',
|
||||
|
|
@ -1748,15 +1748,18 @@ describe('resident tool gating (#12032)', () => {
|
|||
isTodoWriteEnabled: () => false,
|
||||
};
|
||||
|
||||
// The snapshot is the single source `/context` and the request share, so
|
||||
// the prompt must actually read it rather than recompute from a registry.
|
||||
// `/context` and the request share these session snapshots, so the prompt
|
||||
// must read them rather than recompute from a live registry.
|
||||
const gated = getMainSessionBaseSystemPrompt({
|
||||
...base,
|
||||
getPromptToolSnapshot: () => FILE_WORK_TOOLS,
|
||||
getPromptToolSnapshot: () =>
|
||||
new Set([...FILE_WORK_TOOLS, ToolNames.TOOL_CALL]),
|
||||
getPromptAgentReachable: () => true,
|
||||
});
|
||||
const ungated = getMainSessionBaseSystemPrompt(base);
|
||||
|
||||
expect(gated).not.toContain('- **Subagent Delegation:**');
|
||||
expect(gated).toContain('- **Subagent Delegation:**');
|
||||
expect(gated).toContain('- **Codebase Search:**');
|
||||
expect(ungated).toContain('- **Subagent Delegation:**');
|
||||
expect(gated.length).toBeLessThan(ungated.length);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ export function getCustomSystemPrompt(
|
|||
*/
|
||||
export interface PromptToolSurface {
|
||||
declaredTools?: ReadonlySet<string>;
|
||||
agentReachable?: boolean;
|
||||
executionSandboxFilesystem?: 'read-only' | 'workspace-write';
|
||||
executionSandboxBackend?: 'bwrap' | 'landlock';
|
||||
}
|
||||
|
|
@ -311,7 +312,8 @@ const TOOL_GUIDANCE_LINE_GATES: ReadonlyArray<{
|
|||
const PREFER_DEDICATED_TOOLS_PREFIX = '- **Prefer Dedicated Tools:**';
|
||||
|
||||
/**
|
||||
* Drops the tool-guidance lines whose tools this session did not declare.
|
||||
* Drops tool-guidance lines whose tools the session cannot call. Agent policy
|
||||
* also survives when Agent is reachable through the deferred-tool bridge.
|
||||
*
|
||||
* Implemented as a line filter rather than a rebuilt template on purpose: with
|
||||
* no snapshot the section returns unchanged, so the default prompt cannot drift
|
||||
|
|
@ -328,7 +330,14 @@ function gateToolGuidance(
|
|||
const gate = TOOL_GUIDANCE_LINE_GATES.find((entry) =>
|
||||
line.startsWith(entry.prefix),
|
||||
);
|
||||
return !gate || gate.tools.every((tool) => declared.has(tool));
|
||||
return (
|
||||
!gate ||
|
||||
gate.tools.every(
|
||||
(tool) =>
|
||||
declared.has(tool) ||
|
||||
(tool === ToolNames.AGENT && surface?.agentReachable === true),
|
||||
)
|
||||
);
|
||||
});
|
||||
// The "prefer dedicated tools" bullet only introduces its sub-bullets, so it
|
||||
// goes when every tool it was going to recommend is gone.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue