diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index c49f98e615..2b2f4e04d8 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -15,7 +15,7 @@ import { EDIT_TOOL_NAME, WEB_FETCH_TOOL_NAME, ASK_USER_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, type ExtensionLoader, debugLogger, ApprovalMode, @@ -1091,7 +1091,7 @@ describe('mergeExcludeTools', () => { 'tool3', 'tool4', 'tool5', - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, ]), ); expect(config.getExcludeTools()).toHaveLength(6); @@ -1116,7 +1116,7 @@ describe('mergeExcludeTools', () => { const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig(settings, 'test-session', argv); expect(config.getExcludeTools()).toEqual( - new Set(['tool1', 'tool2', 'tool3', CREATE_NEW_TOPIC_TOOL_NAME]), + new Set(['tool1', 'tool2', 'tool3', UPDATE_TOPIC_TOOL_NAME]), ); expect(config.getExcludeTools()).toHaveLength(4); }); @@ -1149,7 +1149,7 @@ describe('mergeExcludeTools', () => { const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig(settings, 'test-session', argv); expect(config.getExcludeTools()).toEqual( - new Set(['tool1', 'tool2', 'tool3', 'tool4', CREATE_NEW_TOPIC_TOOL_NAME]), + new Set(['tool1', 'tool2', 'tool3', 'tool4', UPDATE_TOPIC_TOOL_NAME]), ); expect(config.getExcludeTools()).toHaveLength(5); }); @@ -1160,9 +1160,7 @@ describe('mergeExcludeTools', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig(settings, 'test-session', argv); - expect(config.getExcludeTools()).toEqual( - new Set([CREATE_NEW_TOPIC_TOOL_NAME]), - ); + expect(config.getExcludeTools()).toEqual(new Set([UPDATE_TOPIC_TOOL_NAME])); }); it('should return default excludes when no excludeTools are specified and it is not interactive', async () => { @@ -1172,7 +1170,7 @@ describe('mergeExcludeTools', () => { const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig(settings, 'test-session', argv); expect(config.getExcludeTools()).toEqual( - new Set([ASK_USER_TOOL_NAME, CREATE_NEW_TOPIC_TOOL_NAME]), + new Set([ASK_USER_TOOL_NAME, UPDATE_TOPIC_TOOL_NAME]), ); }); @@ -1185,7 +1183,7 @@ describe('mergeExcludeTools', () => { vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]); const config = await loadCliConfig(settings, 'test-session', argv); expect(config.getExcludeTools()).toEqual( - new Set(['tool1', 'tool2', CREATE_NEW_TOPIC_TOOL_NAME]), + new Set(['tool1', 'tool2', UPDATE_TOPIC_TOOL_NAME]), ); expect(config.getExcludeTools()).toHaveLength(3); }); @@ -1207,7 +1205,7 @@ describe('mergeExcludeTools', () => { const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig(settings, 'test-session', argv); expect(config.getExcludeTools()).toEqual( - new Set(['tool1', 'tool2', CREATE_NEW_TOPIC_TOOL_NAME]), + new Set(['tool1', 'tool2', UPDATE_TOPIC_TOOL_NAME]), ); expect(config.getExcludeTools()).toHaveLength(3); }); diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index be0bae302e..1f09ee5598 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -59,7 +59,7 @@ import { RipgrepFallbackEvent } from '../telemetry/types.js'; import { ToolRegistry } from '../tools/tool-registry.js'; import { ACTIVATE_SKILL_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, } from '../tools/tool-names.js'; import type { SkillDefinition } from '../skills/skillLoader.js'; import type { McpClientManager } from '../tools/mcp-client-manager.js'; @@ -1675,7 +1675,7 @@ describe('Server Config (config.ts)', () => { topicUpdateNarration: true, }); const excluded = config.getExcludeTools(); - expect(excluded!.has(CREATE_NEW_TOPIC_TOOL_NAME)).toBe(false); + expect(excluded!.has(UPDATE_TOPIC_TOOL_NAME)).toBe(false); }); it('should exclude topic tool when narration is disabled', () => { @@ -1685,14 +1685,14 @@ describe('Server Config (config.ts)', () => { }); const excluded = config.getExcludeTools(); expect(excluded).toBeDefined(); - expect(excluded!.has(CREATE_NEW_TOPIC_TOOL_NAME)).toBe(true); + expect(excluded!.has(UPDATE_TOPIC_TOOL_NAME)).toBe(true); }); it('should default to disabled and exclude topic tool', () => { const config = new Config(baseParams); expect(config.isTopicUpdateNarrationEnabled()).toBe(false); const excluded = config.getExcludeTools(); - expect(excluded!.has(CREATE_NEW_TOPIC_TOOL_NAME)).toBe(true); + expect(excluded!.has(UPDATE_TOPIC_TOOL_NAME)).toBe(true); }); it('should have independent TopicState across instances', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 6d86285889..bd5e62c05d 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -20,7 +20,7 @@ import type { OverageStrategy } from '../billing/billing.js'; import { PromptRegistry } from '../prompts/prompt-registry.js'; import { ResourceRegistry } from '../resources/resource-registry.js'; import { ToolRegistry } from '../tools/tool-registry.js'; -import { CREATE_NEW_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; +import { UPDATE_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; import { LSTool } from '../tools/ls.js'; import { ReadFileTool } from '../tools/read-file.js'; import { GrepTool } from '../tools/grep.js'; @@ -34,7 +34,7 @@ import { WebFetchTool } from '../tools/web-fetch.js'; import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js'; import { WebSearchTool } from '../tools/web-search.js'; import { AskUserTool } from '../tools/ask-user.js'; -import { CreateNewTopicTool, TopicState } from '../tools/topicTool.js'; +import { UpdateTopicTool, TopicState } from '../tools/topicTool.js'; import { ExitPlanModeTool } from '../tools/exit-plan-mode.js'; import { EnterPlanModeTool } from '../tools/enter-plan-mode.js'; import { GeminiClient } from '../core/client.js'; @@ -2029,7 +2029,7 @@ export class Config implements McpContext, AgentLoopContext { } if (!this.isTopicUpdateNarrationEnabled()) { - excludeToolsSet.add(CREATE_NEW_TOPIC_TOOL_NAME); + excludeToolsSet.add(UPDATE_TOPIC_TOOL_NAME); } return excludeToolsSet; @@ -3198,8 +3198,8 @@ export class Config implements McpContext, AgentLoopContext { } }; - maybeRegister(CreateNewTopicTool, () => - registry.registerTool(new CreateNewTopicTool(this, this.messageBus)), + maybeRegister(UpdateTopicTool, () => + registry.registerTool(new UpdateTopicTool(this, this.messageBus)), ); maybeRegister(LSTool, () => diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index bcd687c409..81c2d60851 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -103,7 +103,7 @@ modes = ["plan"] # Topic grouping tool is innocuous and used for UI organization. [[rule]] -toolName = "create_new_topic" +toolName = "update_topic" decision = "allow" priority = 70 modes = ["plan"] diff --git a/packages/core/src/policy/policies/read-only.toml b/packages/core/src/policy/policies/read-only.toml index dacc027859..66aa4c33ce 100644 --- a/packages/core/src/policy/policies/read-only.toml +++ b/packages/core/src/policy/policies/read-only.toml @@ -59,6 +59,6 @@ priority = 50 # Topic grouping tool is innocuous and used for UI organization. [[rule]] -toolName = "create_new_topic" +toolName = "update_topic" decision = "allow" priority = 50 \ No newline at end of file diff --git a/packages/core/src/policy/topic-policy.test.ts b/packages/core/src/policy/topic-policy.test.ts index 7fc6197a72..91450af056 100644 --- a/packages/core/src/policy/topic-policy.test.ts +++ b/packages/core/src/policy/topic-policy.test.ts @@ -9,7 +9,7 @@ import * as path from 'node:path'; import { loadPoliciesFromToml } from './toml-loader.js'; import { PolicyEngine } from './policy-engine.js'; import { ApprovalMode, PolicyDecision } from './types.js'; -import { CREATE_NEW_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; +import { UPDATE_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; describe('Topic Tool Policy', () => { async function loadDefaultPolicies() { @@ -20,7 +20,7 @@ describe('Topic Tool Policy', () => { return result.rules; } - it('should allow create_new_topic in DEFAULT mode', async () => { + it('should allow update_topic in DEFAULT mode', async () => { const rules = await loadDefaultPolicies(); const engine = new PolicyEngine({ rules, @@ -28,13 +28,13 @@ describe('Topic Tool Policy', () => { }); const result = await engine.check( - { name: CREATE_NEW_TOPIC_TOOL_NAME }, + { name: UPDATE_TOPIC_TOOL_NAME }, undefined, ); expect(result.decision).toBe(PolicyDecision.ALLOW); }); - it('should allow create_new_topic in PLAN mode', async () => { + it('should allow update_topic in PLAN mode', async () => { const rules = await loadDefaultPolicies(); const engine = new PolicyEngine({ rules, @@ -42,13 +42,13 @@ describe('Topic Tool Policy', () => { }); const result = await engine.check( - { name: CREATE_NEW_TOPIC_TOOL_NAME }, + { name: UPDATE_TOPIC_TOOL_NAME }, undefined, ); expect(result.decision).toBe(PolicyDecision.ALLOW); }); - it('should allow create_new_topic in YOLO mode', async () => { + it('should allow update_topic in YOLO mode', async () => { const rules = await loadDefaultPolicies(); const engine = new PolicyEngine({ rules, @@ -56,7 +56,7 @@ describe('Topic Tool Policy', () => { }); const result = await engine.check( - { name: CREATE_NEW_TOPIC_TOOL_NAME }, + { name: UPDATE_TOPIC_TOOL_NAME }, undefined, ); expect(result.decision).toBe(PolicyDecision.ALLOW); diff --git a/packages/core/src/prompts/promptProvider.test.ts b/packages/core/src/prompts/promptProvider.test.ts index 978eec05bf..24d01fa507 100644 --- a/packages/core/src/prompts/promptProvider.test.ts +++ b/packages/core/src/prompts/promptProvider.test.ts @@ -15,7 +15,7 @@ import { PREVIEW_GEMINI_MODEL } from '../config/models.js'; import { ApprovalMode } from '../policy/types.js'; import { DiscoveredMCPTool } from '../tools/mcp-tool.js'; import { MockTool } from '../test-utils/mock-tool.js'; -import { CREATE_NEW_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; +import { UPDATE_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; import { TopicState } from '../tools/topicTool.js'; import type { CallableTool } from '@google/genai'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; @@ -244,10 +244,10 @@ describe('PromptProvider', () => { mockConfig.topicState.reset(); vi.mocked(mockConfig.isTopicUpdateNarrationEnabled).mockReturnValue(true); (mockConfig.getToolRegistry as ReturnType).mockReturnValue({ - getAllToolNames: vi.fn().mockReturnValue([CREATE_NEW_TOPIC_TOOL_NAME]), + getAllToolNames: vi.fn().mockReturnValue([UPDATE_TOPIC_TOOL_NAME]), getAllTools: vi.fn().mockReturnValue([ new MockTool({ - name: CREATE_NEW_TOPIC_TOOL_NAME, + name: UPDATE_TOPIC_TOOL_NAME, displayName: 'Topic', }), ]), @@ -275,7 +275,7 @@ describe('PromptProvider', () => { expect(prompt).not.toContain('[Active Topic: Active Chapter]'); }); - it('should filter out create_new_topic tool when narration is disabled', () => { + it('should filter out update_topic tool when narration is disabled', () => { vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.PLAN); vi.mocked(mockConfig.isTopicUpdateNarrationEnabled).mockReturnValue( false, @@ -283,18 +283,16 @@ describe('PromptProvider', () => { const provider = new PromptProvider(); const prompt = provider.getCoreSystemPrompt(mockConfig); - expect(prompt).not.toContain(CREATE_NEW_TOPIC_TOOL_NAME); + expect(prompt).not.toContain(UPDATE_TOPIC_TOOL_NAME); }); - it('should NOT filter out create_new_topic tool when narration is enabled', () => { + it('should NOT filter out update_topic tool when narration is enabled', () => { vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.PLAN); vi.mocked(mockConfig.isTopicUpdateNarrationEnabled).mockReturnValue(true); const provider = new PromptProvider(); const prompt = provider.getCoreSystemPrompt(mockConfig); - expect(prompt).toContain( - `\`${CREATE_NEW_TOPIC_TOOL_NAME}\``, - ); + expect(prompt).toContain(`\`${UPDATE_TOPIC_TOOL_NAME}\``); }); }); }); diff --git a/packages/core/src/prompts/promptProvider.ts b/packages/core/src/prompts/promptProvider.ts index 169c394b5f..407ffe8b30 100644 --- a/packages/core/src/prompts/promptProvider.ts +++ b/packages/core/src/prompts/promptProvider.ts @@ -26,7 +26,7 @@ import { ENTER_PLAN_MODE_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, } from '../tools/tool-names.js'; import { resolveModel, supportsModernFeatures } from '../config/models.js'; import { DiscoveredMCPTool } from '../tools/mcp-tool.js'; @@ -60,7 +60,7 @@ export class PromptProvider { const enabledToolNames = new Set(toolNames); if (!context.config.isTopicUpdateNarrationEnabled()) { - enabledToolNames.delete(CREATE_NEW_TOPIC_TOOL_NAME); + enabledToolNames.delete(UPDATE_TOPIC_TOOL_NAME); } const approvedPlanPath = context.config.getApprovedPlanPath(); @@ -81,7 +81,7 @@ export class PromptProvider { const allTools = context.toolRegistry.getAllTools().filter((t) => { if ( !context.config.isTopicUpdateNarrationEnabled() && - t.name === CREATE_NEW_TOPIC_TOOL_NAME + t.name === UPDATE_TOPIC_TOOL_NAME ) { return false; } diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 99cd794cc9..dac6de3538 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -10,9 +10,7 @@ import { EDIT_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + UPDATE_TOPIC_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, MEMORY_TOOL_NAME, @@ -581,32 +579,34 @@ function mandateConfirm(interactive: boolean): string { function mandateTopicUpdateModel(): string { return ` - **Protocol: Topic Model** - You are an agentic system. You must organize your work into logical "Chapters" using the \`${CREATE_NEW_TOPIC_TOOL_NAME}\` tool. + You are an agentic system. You must organize your work into logical "Chapters" and tactical "Intents" using the \`${UPDATE_TOPIC_TOOL_NAME}\` tool. -- **1. Chapter Initialization:** - - **The Trigger:** You MUST call \`${CREATE_NEW_TOPIC_TOOL_NAME}\` ONLY when beginning a new task or when the broad logical nature of your work changes (e.g., transitioning from Research to Strategy, or from Strategy to Implementation). - - **Granularity:** You MUST NOT combine topics (e.g., do NOT use "Researching and Implementing"). Use granular, single-focus titles like "Researching [Task]", "Strategy for [Task]", or "Implementing [Specific Idea]". - - **Deviations:** If your work deviates from the current topic's stated goal, you MUST call \`${CREATE_NEW_TOPIC_TOOL_NAME}\` again with a new title (e.g., "Implementing [New Idea]") to reflect the shift. - - **The Format:** Provide a concise, high-level title for the chapter. You MUST also provide a detailed explanation (5-10 sentences) of what the new topic intends to achieve in the \`${TOPIC_PARAM_CURRENT_SUMMARY}\` parameter. When shifting topics, you MUST additionally provide a detailed summary (5-10 sentences) of the work completed in the previous topic in the \`${TOPIC_PARAM_PREVIOUS_SUMMARY}\` parameter. - - **Example:** \`create_new_topic(title="Implementing Auth", ${TOPIC_PARAM_CURRENT_SUMMARY}="Implement the OAuth2 flow and link it to the existing user session. This involves creating new routes for the callback and state management for the token. We will also need to update the middleware to handle authenticated sessions correctly. This will ensure that all subsequent API calls are properly authorized and user-specific. The implementation will follow the project's security standards and include unit tests for each new component.", ${TOPIC_PARAM_PREVIOUS_SUMMARY}="Completed research on OAuth2 and mapped endpoints. We identified the necessary client libraries and configured the initial developer credentials for the authentication provider. A prototype script was written to verify the handshake process with the API. The existing user database schema was reviewed and found to be compatible with the new OAuth IDs. We also finalized the design of the new login UI and obtained user feedback on the mockups.")\` - - **Start of Task:** Your very first tool execution in a new session must be \`${CREATE_NEW_TOPIC_TOOL_NAME}\`. For the first topic, leave \`${TOPIC_PARAM_PREVIOUS_SUMMARY}\` empty but you MUST still provide \`${TOPIC_PARAM_CURRENT_SUMMARY}\`. +- **1. Chapter Initialization & First Call:** + - **Start of Task:** Your very first tool execution in a new session MUST be \`${UPDATE_TOPIC_TOOL_NAME}\`. + - **Single Idea Scope:** Every Chapter MUST be scoped to exactly ONE logical idea or phase. You are strictly FORBIDDEN from clubbing multiple distinct phases into a single topic. + - **Granularity:** Use granular, single-focus titles. + - **CORRECT:** "Researching Parser", "Strategy for Fix", "Implementing Buffer Logic". + - **INCORRECT:** "Researching and implementing parser fix", "Planning and coding the update". + - **Mandatory Transitions:** When transitioning from one logical phase to another (e.g., from Research to Implementation), you MUST create a new Chapter. Research and Implementation must ALWAYS be separate topics. + - **The Format:** For a new chapter, you MUST provide a concise, high-level \`title\`. You MUST also provide a detailed summary (5-10 sentences) in the \`summary\` parameter. This summary MUST cover both the work completed in the previous topic and the strategic intent of the new topic to ensure narrative continuity. + - **Example (First Call):** \`update_topic(title="Researching Parser", summary="I am starting an investigation into the parser timeout bug. My goal is to first understand the current test coverage and then attempt to reproduce the failure. This phase will focus on identifying the bottleneck in the main loop before we move to implementation.", strategic_intent="Listing files in the parser directory.")\` + - **Example (Transition):** \`update_topic(title="Implementing Buffer Fix", summary="I have completed the research phase and identified a race condition in the tokenizer's buffer management. I am now transitioning to implementation. This new chapter will focus on refactoring the buffer logic to handle async chunks safely, followed by unit testing the fix.", strategic_intent="Opening tokenizer.ts to apply the fix.")\` -- **2. Strategic Narration:** - - **Intent:** At the start of each chapter, or before a significant sequence of tools, you SHOULD provide a concise, one-sentence statement of your intent or strategy. - - **No Text Headers:** You are FORBIDDEN from printing "Topic: " or any similar text-based headers in your response. The tool handles all UI narration. - - **Minimal Noise:** Avoid conversational filler and apologies. While executing tools within a chapter, maintain a high signal-to-noise ratio; brief explanations are encouraged if they provide essential context or clarify your strategy. +- **2. Tactical Heartbeat (Every Turn):** + - **The Mandate:** For EVERY response that contains functional tools (e.g., \`read_file\`, \`grep_search\`), your FIRST tool call in that turn MUST be \`update_topic\`. + - **The Intent:** You MUST provide a one-sentence statement of your immediate goal in the \`strategic_intent\` parameter. This replaces the raw text "Explain Before Acting" preamble. + - **Example (Tactical):** \`update_topic(strategic_intent="Reading the parser index file to check the main loop.")\` -- **3. Internal Reasoning:** - - You MUST reason about your plan, track tool calls, and strategize internally before executing tools. - - While your deep reasoning should remain internal, you are encouraged to share your high-level strategy and key findings in your text responses to maintain alignment and improve task performance. +- **3. Strategic Transitions:** + - **The Trigger:** Call \`update_topic\` with a new \`title\` and \`current_summary\` ONLY when the broad logical nature of your work changes (e.g., transitioning from Research to Strategy, or from Strategy to Implementation). -- **4. Completion:** - - Only when the entire task is finalized do you provide a **Final Summary**. +- **4. Constraints:** + - **No Raw Text:** You are FORBIDDEN from writing raw text preambles or explanations before tool calls. All reasoning about "what I am doing next" MUST be captured in the \`strategic_intent\` parameter. + - **No Text Headers:** Do NOT print "Topic: " or any similar text-based headers in your response. The tool handles all UI narration. + - **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked. **IMPORTANT: Tool Usage vs. Text** -The \`${CREATE_NEW_TOPIC_TOOL_NAME}\` tool MUST be called as a proper tool. Do NOT simply write the tool name in your text response. - -- **Constraint Enforcement:** If you provide a manual "Topic:" text header, or if you call the tool for every single tool execution without a fundamental shift in work, you have failed the system integrity protocol.`; +The \`update_topic\` tool MUST be called as a proper tool. Do NOT simply write the tool name in your text response.`; } function mandateExplainBeforeActing(): string { diff --git a/packages/core/src/scheduler/scheduler.test.ts b/packages/core/src/scheduler/scheduler.test.ts index 11a4ab5cd6..c3b7270322 100644 --- a/packages/core/src/scheduler/scheduler.test.ts +++ b/packages/core/src/scheduler/scheduler.test.ts @@ -75,7 +75,7 @@ import { type AnyDeclarativeTool, type AnyToolInvocation, } from '../tools/tools.js'; -import { CREATE_NEW_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; +import { UPDATE_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; import { CoreToolCallStatus, ROOT_SCHEDULER_ID, @@ -441,10 +441,10 @@ describe('Scheduler (Orchestrator)', () => { ); }); - it('should sort CREATE_NEW_TOPIC_TOOL_NAME to the front of the batch', async () => { + it('should sort UPDATE_TOPIC_TOOL_NAME to the front of the batch', async () => { const topicReq: ToolCallRequestInfo = { callId: 'call-topic', - name: CREATE_NEW_TOPIC_TOOL_NAME, + name: UPDATE_TOPIC_TOOL_NAME, args: { title: 'New Chapter' }, prompt_id: 'p1', isClientInitiated: false, @@ -457,11 +457,11 @@ describe('Scheduler (Orchestrator)', () => { isClientInitiated: false, }; - // Mock tool registry to return a tool for create_new_topic + // Mock tool registry to return a tool for update_topic vi.mocked(mockToolRegistry.getTool).mockImplementation((name) => { - if (name === CREATE_NEW_TOPIC_TOOL_NAME) { + if (name === UPDATE_TOPIC_TOOL_NAME) { return { - name: CREATE_NEW_TOPIC_TOOL_NAME, + name: UPDATE_TOPIC_TOOL_NAME, build: vi.fn().mockReturnValue({}), } as unknown as AnyDeclarativeTool; } diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index d37d88d2b9..6690efc5c9 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -25,7 +25,7 @@ import { type ScheduledToolCall, } from './types.js'; import { ToolErrorType } from '../tools/tool-error.js'; -import { CREATE_NEW_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; +import { UPDATE_TOPIC_TOOL_NAME } from '../tools/tool-names.js'; import { PolicyDecision, type ApprovalMode } from '../policy/types.js'; import { ToolConfirmationOutcome, @@ -301,8 +301,8 @@ export class Scheduler { // Sort requests to ensure Topic changes happen before actions in the same batch. const sortedRequests = [...requests].sort((a, b) => { - if (a.name === CREATE_NEW_TOPIC_TOOL_NAME) return -1; - if (b.name === CREATE_NEW_TOPIC_TOOL_NAME) return 1; + if (a.name === UPDATE_TOPIC_TOOL_NAME) return -1; + if (b.name === UPDATE_TOPIC_TOOL_NAME) return 1; return 0; }); diff --git a/packages/core/src/tools/definitions/base-declarations.ts b/packages/core/src/tools/definitions/base-declarations.ts index 4ee06c1199..8a64237cc5 100644 --- a/packages/core/src/tools/definitions/base-declarations.ts +++ b/packages/core/src/tools/definitions/base-declarations.ts @@ -123,8 +123,8 @@ export const EXIT_PLAN_PARAM_PLAN_PATH = 'plan_path'; export const ENTER_PLAN_MODE_TOOL_NAME = 'enter_plan_mode'; export const PLAN_MODE_PARAM_REASON = 'reason'; -// -- create_new_topic -- -export const CREATE_NEW_TOPIC_TOOL_NAME = 'create_new_topic'; +// -- update_topic -- +export const UPDATE_TOPIC_TOOL_NAME = 'update_topic'; export const TOPIC_PARAM_TITLE = 'title'; -export const TOPIC_PARAM_PREVIOUS_SUMMARY = 'previous_topic_summary'; -export const TOPIC_PARAM_CURRENT_SUMMARY = 'current_topic_summary'; +export const TOPIC_PARAM_SUMMARY = 'summary'; +export const TOPIC_PARAM_STRATEGIC_INTENT = 'strategic_intent'; diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index ccc2080ed2..3c3254d045 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -17,6 +17,7 @@ import { getShellDeclaration, getExitPlanModeDeclaration, getActivateSkillDeclaration, + getUpdateTopicDeclaration, } from './dynamic-declaration-helpers.js'; import { @@ -90,10 +91,10 @@ import { PLAN_MODE_PARAM_REASON, EXIT_PLAN_PARAM_PLAN_PATH, SKILL_PARAM_NAME, - CREATE_NEW_TOPIC_TOOL_NAME as CREATE_NEW_TOPIC_TOOL_NAME_BASE, + UPDATE_TOPIC_TOOL_NAME as UPDATE_TOPIC_TOOL_NAME_BASE, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, } from './base-declarations.js'; export { @@ -114,7 +115,7 @@ export { ASK_USER_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME_BASE as CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME_BASE as UPDATE_TOPIC_TOOL_NAME, // Shared parameter names PARAM_FILE_PATH, PARAM_DIR_PATH, @@ -169,8 +170,8 @@ export { EXIT_PLAN_PARAM_PLAN_PATH, SKILL_PARAM_NAME, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, }; // Re-export sets for compatibility @@ -301,16 +302,11 @@ export const ENTER_PLAN_MODE_DEFINITION: ToolDefinition = { overrides: (modelId) => getToolSet(modelId).enter_plan_mode, }; -export const CREATE_NEW_TOPIC_DEFINITION: ToolDefinition = { +export const UPDATE_TOPIC_DEFINITION: ToolDefinition = { get base() { - // This tool is only supported on Gemini 3. - // For other models, we provide a placeholder that won't be used if gated correctly. - return { - name: CREATE_NEW_TOPIC_TOOL_NAME_BASE, - description: 'NOT SUPPORTED', - }; + return getUpdateTopicDeclaration(); }, - overrides: (modelId) => getToolSet(modelId).create_new_topic, + overrides: (modelId) => getToolSet(modelId).update_topic, }; // ============================================================================ diff --git a/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts b/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts index c1bd7c7727..a9d750bb04 100644 --- a/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts +++ b/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts @@ -23,10 +23,10 @@ import { SHELL_PARAM_IS_BACKGROUND, EXIT_PLAN_PARAM_PLAN_PATH, SKILL_PARAM_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, } from './base-declarations.js'; /** @@ -177,13 +177,13 @@ export function getActivateSkillDeclaration( } /** - * Returns the FunctionDeclaration for creating a new topic. + * Returns the FunctionDeclaration for updating the topic context. */ -export function getCreateNewTopicDeclaration(): FunctionDeclaration { +export function getUpdateTopicDeclaration(): FunctionDeclaration { return { - name: CREATE_NEW_TOPIC_TOOL_NAME, + name: UPDATE_TOPIC_TOOL_NAME, description: - 'Organizes work into a granular, single-focus "Chapter" or "Topic". You MUST NOT combine distinct phases (e.g., do NOT use "Researching and Implementing"). Use specific, singular titles like "Researching", "Planning", or "Implementing [Specific Idea]". If your work deviates from the current topic\'s stated goal, you MUST create a new topic to reflect the shift (e.g., "Implementing [New Idea]"). When shifting topics, you MUST provide a detailed summary (5-10 sentences) of the work completed in the previous topic and a clear statement of what the new topic aims to achieve.', + 'Manages your narrative flow. You MUST call this in EVERY turn to state your `strategic_intent`. Include `title` and `summary` only when starting a new Chapter (logical phase) or shifting strategic intent.', parametersJsonSchema: { type: 'object', properties: { @@ -191,18 +191,18 @@ export function getCreateNewTopicDeclaration(): FunctionDeclaration { type: 'string', description: 'The title of the new topic or chapter.', }, - [TOPIC_PARAM_PREVIOUS_SUMMARY]: { + [TOPIC_PARAM_SUMMARY]: { type: 'string', description: - '(OPTIONAL) A detailed summary (5-10 sentences) of the work completed in the previous topic. This is required when transitioning between topics to maintain continuity.', + '(OPTIONAL) A detailed summary (5-10 sentences) covering both the work completed in the previous topic and the strategic intent of the new topic. This is required when transitioning between topics to maintain continuity.', }, - [TOPIC_PARAM_CURRENT_SUMMARY]: { + [TOPIC_PARAM_STRATEGIC_INTENT]: { type: 'string', description: - 'A detailed explanation (5-10 sentences) of what the new topic intends to achieve. This is mandatory for all new topics to provide clear strategic intent.', + 'A mandatory one-sentence statement of your immediate intent. You MUST provide this for every tool-use turn to maintain strategic alignment. This replaces the raw text preamble.', }, }, - required: [TOPIC_PARAM_TITLE, TOPIC_PARAM_CURRENT_SUMMARY], + required: [TOPIC_PARAM_STRATEGIC_INTENT], }, }; } diff --git a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts index c792cf9fef..5a859d14d6 100644 --- a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts +++ b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts @@ -78,7 +78,7 @@ import { getShellDeclaration, getExitPlanModeDeclaration, getActivateSkillDeclaration, - getCreateNewTopicDeclaration, + getUpdateTopicDeclaration, } from '../dynamic-declaration-helpers.js'; import { DEFAULT_MAX_LINES_TEXT_FILE, @@ -717,5 +717,5 @@ The agent did not use the todo list because this task could be completed by a ti exit_plan_mode: (plansDir) => getExitPlanModeDeclaration(plansDir), activate_skill: (skillNames) => getActivateSkillDeclaration(skillNames), - create_new_topic: getCreateNewTopicDeclaration(), + update_topic: getUpdateTopicDeclaration(), }; diff --git a/packages/core/src/tools/definitions/types.ts b/packages/core/src/tools/definitions/types.ts index 547be60997..93b46abafc 100644 --- a/packages/core/src/tools/definitions/types.ts +++ b/packages/core/src/tools/definitions/types.ts @@ -49,5 +49,5 @@ export interface CoreToolSet { enter_plan_mode: FunctionDeclaration; exit_plan_mode: (plansDir: string) => FunctionDeclaration; activate_skill: (skillNames: string[]) => FunctionDeclaration; - create_new_topic?: FunctionDeclaration; + update_topic?: FunctionDeclaration; } diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index 540cd10644..8ed973ec28 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -22,7 +22,6 @@ import { ASK_USER_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, // Shared parameter names PARAM_FILE_PATH, PARAM_DIR_PATH, @@ -76,9 +75,10 @@ import { PLAN_MODE_PARAM_REASON, EXIT_PLAN_PARAM_PLAN_PATH, SKILL_PARAM_NAME, + UPDATE_TOPIC_TOOL_NAME, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, } from './definitions/coreTools.js'; export { @@ -99,7 +99,7 @@ export { ASK_USER_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, // Shared parameter names PARAM_FILE_PATH, PARAM_DIR_PATH, @@ -154,8 +154,8 @@ export { EXIT_PLAN_PARAM_PLAN_PATH, SKILL_PARAM_NAME, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, }; export const LS_TOOL_NAME_LEGACY = 'list_directory'; // Just to be safe if anything used the old exported name directly @@ -259,7 +259,7 @@ export const ALL_BUILTIN_TOOL_NAMES = [ GET_INTERNAL_DOCS_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, ] as const; /** @@ -276,6 +276,7 @@ export const PLAN_MODE_TOOLS = [ ASK_USER_TOOL_NAME, ACTIVATE_SKILL_TOOL_NAME, GET_INTERNAL_DOCS_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, 'codebase_investigator', 'cli_help', ] as const; diff --git a/packages/core/src/tools/topicTool.test.ts b/packages/core/src/tools/topicTool.test.ts index 1c64e24343..7c6497a2de 100644 --- a/packages/core/src/tools/topicTool.test.ts +++ b/packages/core/src/tools/topicTool.test.ts @@ -5,14 +5,14 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { TopicState, CreateNewTopicTool } from './topicTool.js'; +import { TopicState, UpdateTopicTool } from './topicTool.js'; import { MessageBus } from '../confirmation-bus/message-bus.js'; import type { PolicyEngine } from '../policy/policy-engine.js'; import { - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, } from './definitions/base-declarations.js'; import type { Config } from '../config/config.js'; @@ -23,47 +23,41 @@ describe('TopicState', () => { state = new TopicState(); }); - it('should store and retrieve topic title', () => { + it('should store and retrieve topic title and intent', () => { expect(state.getTopic()).toBeUndefined(); - const success = state.setTopic('Test Topic'); + expect(state.getIntent()).toBeUndefined(); + const success = state.setTopic('Test Topic', 'Test Intent'); expect(success).toBe(true); expect(state.getTopic()).toBe('Test Topic'); + expect(state.getIntent()).toBe('Test Intent'); }); it('should sanitize newlines and carriage returns', () => { - state.setTopic('Topic\nWith\r\nLines'); + state.setTopic('Topic\nWith\r\nLines', 'Intent\nWith\r\nLines'); expect(state.getTopic()).toBe('Topic With Lines'); + expect(state.getIntent()).toBe('Intent With Lines'); }); it('should trim whitespace', () => { - state.setTopic(' Spaced Topic '); + state.setTopic(' Spaced Topic ', ' Spaced Intent '); expect(state.getTopic()).toBe('Spaced Topic'); + expect(state.getIntent()).toBe('Spaced Intent'); }); - it('should reject empty or whitespace-only titles', () => { - expect(state.setTopic('')).toBe(false); - expect(state.setTopic(' ')).toBe(false); - expect(state.setTopic('\n\n')).toBe(false); + it('should reject empty or whitespace-only inputs', () => { + expect(state.setTopic('', '')).toBe(false); }); - it('should reset topic', () => { - state.setTopic('Test Topic'); + it('should reset topic and intent', () => { + state.setTopic('Test Topic', 'Test Intent'); state.reset(); expect(state.getTopic()).toBeUndefined(); - }); - - it('should be independent across instances', () => { - const state1 = new TopicState(); - const state2 = new TopicState(); - state1.setTopic('Topic 1'); - state2.setTopic('Topic 2'); - expect(state1.getTopic()).toBe('Topic 1'); - expect(state2.getTopic()).toBe('Topic 2'); + expect(state.getIntent()).toBeUndefined(); }); }); -describe('CreateNewTopicTool', () => { - let tool: CreateNewTopicTool; +describe('UpdateTopicTool', () => { + let tool: UpdateTopicTool; let mockMessageBus: MessageBus; let mockConfig: Config; @@ -73,48 +67,67 @@ describe('CreateNewTopicTool', () => { mockConfig = { topicState: new TopicState(), } as unknown as Config; - tool = new CreateNewTopicTool(mockConfig, mockMessageBus); + tool = new UpdateTopicTool(mockConfig, mockMessageBus); }); it('should have correct name and display name', () => { - expect(tool.name).toBe(CREATE_NEW_TOPIC_TOOL_NAME); - expect(tool.displayName).toBe('Create New Topic'); + expect(tool.name).toBe(UPDATE_TOPIC_TOOL_NAME); + expect(tool.displayName).toBe('Update Topic Context'); }); - it('should update TopicState and include current goal on execute', async () => { + it('should update TopicState and include strategic intent on execute', async () => { const invocation = tool.build({ [TOPIC_PARAM_TITLE]: 'New Chapter', - [TOPIC_PARAM_CURRENT_SUMMARY]: 'The goal is to implement X', + [TOPIC_PARAM_SUMMARY]: 'The goal is to implement X. Previously we did Y.', + [TOPIC_PARAM_STRATEGIC_INTENT]: 'Initial Move', }); const result = await invocation.execute(new AbortController().signal); expect(result.llmContent).toContain('Current topic: "New Chapter"'); expect(result.llmContent).toContain( - 'Topic goal: The goal is to implement X', + 'Topic summary: The goal is to implement X. Previously we did Y.', ); + expect(result.llmContent).toContain('Strategic Intent: Initial Move'); expect(mockConfig.topicState.getTopic()).toBe('New Chapter'); + expect(mockConfig.topicState.getIntent()).toBe('Initial Move'); + expect(result.returnDisplay).toContain('## 📂 Topic: **New Chapter**'); + expect(result.returnDisplay).toContain('**Summary:**'); + expect(result.returnDisplay).toContain( + '> [!STRATEGY]\n> **Intent:** Initial Move', + ); }); - it('should include previous summary if provided', async () => { + it('should render only intent for tactical updates (same topic)', async () => { + mockConfig.topicState.setTopic('New Chapter'); + const invocation = tool.build({ [TOPIC_PARAM_TITLE]: 'New Chapter', - [TOPIC_PARAM_CURRENT_SUMMARY]: 'The goal is to implement X', - [TOPIC_PARAM_PREVIOUS_SUMMARY]: 'Finished Y', + [TOPIC_PARAM_STRATEGIC_INTENT]: 'Subsequent Move', }); const result = await invocation.execute(new AbortController().signal); - expect(result.llmContent).toContain('Previous topic summary: Finished Y'); - expect(result.llmContent).toContain('Current topic: "New Chapter"'); + expect(result.returnDisplay).not.toContain('## 📂 Topic:'); + expect(result.returnDisplay).toBe( + '> [!STRATEGY]\n> **Intent:** Subsequent Move', + ); + expect(result.llmContent).toBe('Strategic Intent: Subsequent Move'); }); - it('should return error if title is invalid after sanitization', async () => { - const invocation = tool.build({ - [TOPIC_PARAM_TITLE]: ' \n ', - [TOPIC_PARAM_CURRENT_SUMMARY]: 'Goal', - }); - const result = await invocation.execute(new AbortController().signal); - - expect(result.error).toBeDefined(); + it('should return error if strategic_intent is missing', async () => { + try { + tool.build({ + [TOPIC_PARAM_TITLE]: 'Title', + }); + expect.fail('Should have thrown validation error'); + } catch (e: unknown) { + if (e instanceof Error) { + expect(e.message).toContain( + "must have required property 'strategic_intent'", + ); + } else { + expect.fail('Expected Error instance'); + } + } expect(mockConfig.topicState.getTopic()).toBeUndefined(); }); }); diff --git a/packages/core/src/tools/topicTool.ts b/packages/core/src/tools/topicTool.ts index 9aa9a99763..51c7999fba 100644 --- a/packages/core/src/tools/topicTool.ts +++ b/packages/core/src/tools/topicTool.ts @@ -5,10 +5,10 @@ */ import { - CREATE_NEW_TOPIC_TOOL_NAME, + UPDATE_TOPIC_TOOL_NAME, TOPIC_PARAM_TITLE, - TOPIC_PARAM_PREVIOUS_SUMMARY, - TOPIC_PARAM_CURRENT_SUMMARY, + TOPIC_PARAM_SUMMARY, + TOPIC_PARAM_STRATEGIC_INTENT, } from './definitions/coreTools.js'; import { BaseDeclarativeTool, @@ -16,38 +16,37 @@ import { Kind, type ToolResult, } from './tools.js'; -import { ToolErrorType } from './tool-error.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { debugLogger } from '../utils/debugLogger.js'; -import { getCreateNewTopicDeclaration } from './definitions/dynamic-declaration-helpers.js'; +import { getUpdateTopicDeclaration } from './definitions/dynamic-declaration-helpers.js'; import type { Config } from '../config/config.js'; /** - * Manages the current active topic title for a session. + * Manages the current active topic title and tactical intent for a session. * Hosted within the Config instance for session-scoping. */ export class TopicState { private activeTopicTitle?: string; + private activeIntent?: string; /** - * Sanitizes and sets the topic title. - * @returns true if the title was valid and set, false otherwise. + * Sanitizes and sets the topic title and/or intent. + * @returns true if the input was valid and set, false otherwise. */ - setTopic(title: string): boolean { - if (!title) return false; + setTopic(title?: string, intent?: string): boolean { + const sanitizedTitle = title?.trim().replace(/[\r\n]+/g, ' '); + const sanitizedIntent = intent?.trim().replace(/[\r\n]+/g, ' '); - // 1. Trim whitespace - let sanitized = title.trim(); + if (!sanitizedTitle && !sanitizedIntent) return false; - // 2. Security: Strip newlines and carriage returns to prevent prompt injection/breakout - sanitized = sanitized.replace(/[\r\n]+/g, ' '); - - // 3. Robustness check: Ensure it's not empty after sanitization - if (sanitized.length === 0) { - return false; + if (sanitizedTitle) { + this.activeTopicTitle = sanitizedTitle; + } + + if (sanitizedIntent) { + this.activeIntent = sanitizedIntent; } - this.activeTopicTitle = sanitized; return true; } @@ -55,23 +54,28 @@ export class TopicState { return this.activeTopicTitle; } + getIntent(): string | undefined { + return this.activeIntent; + } + reset(): void { this.activeTopicTitle = undefined; + this.activeIntent = undefined; } } -interface CreateNewTopicParams { - [TOPIC_PARAM_TITLE]: string; - [TOPIC_PARAM_PREVIOUS_SUMMARY]?: string; - [TOPIC_PARAM_CURRENT_SUMMARY]: string; +interface UpdateTopicParams { + [TOPIC_PARAM_TITLE]?: string; + [TOPIC_PARAM_SUMMARY]?: string; + [TOPIC_PARAM_STRATEGIC_INTENT]?: string; } -class CreateNewTopicInvocation extends BaseToolInvocation< - CreateNewTopicParams, +class UpdateTopicInvocation extends BaseToolInvocation< + UpdateTopicParams, ToolResult > { constructor( - params: CreateNewTopicParams, + params: UpdateTopicParams, messageBus: MessageBus, toolName: string, private readonly config: Config, @@ -81,39 +85,51 @@ class CreateNewTopicInvocation extends BaseToolInvocation< getDescription(): string { const title = this.params[TOPIC_PARAM_TITLE]; - return `Create new topic: "${title}"`; + const intent = this.params[TOPIC_PARAM_STRATEGIC_INTENT]; + if (title) { + return `Update topic to: "${title}"`; + } + return `Update tactical intent: "${intent || '...'}"`; } async execute(): Promise { const title = this.params[TOPIC_PARAM_TITLE]; - const previousSummary = this.params[TOPIC_PARAM_PREVIOUS_SUMMARY]; - const currentSummary = this.params[TOPIC_PARAM_CURRENT_SUMMARY]; + const summary = this.params[TOPIC_PARAM_SUMMARY]; + const strategicIntent = this.params[TOPIC_PARAM_STRATEGIC_INTENT]; - const success = this.config.topicState.setTopic(title); + const activeTopic = this.config.topicState.getTopic(); + const isNewTopic = !!( + title && + title.trim() !== '' && + title.trim() !== activeTopic + ); - if (!success) { - return { - llmContent: 'Error: A valid, non-empty topic title is required.', - returnDisplay: 'Error: A valid, non-empty topic title is required.', - error: { - message: 'A valid topic title is required.', - type: ToolErrorType.INVALID_TOOL_PARAMS, - }, - }; - } + this.config.topicState.setTopic(title, strategicIntent); - const setTopic = this.config.topicState.getTopic()!; - debugLogger.log(`[TopicTool] Changing topic to: "${setTopic}"`); + const currentTopic = this.config.topicState.getTopic() || '...'; + const currentIntent = + strategicIntent || this.config.topicState.getIntent() || '...'; - let llmContent = `Current topic: "${setTopic}"\nTopic goal: ${currentSummary}`; - let returnDisplay = `Current topic: **${setTopic}**\n\n**Topic Goal:**\n${currentSummary}`; + debugLogger.log( + `[TopicTool] Update: Topic="${currentTopic}", Intent="${currentIntent}", isNew=${isNewTopic}`, + ); - if (previousSummary) { - llmContent = - `Previous topic summary: ${previousSummary}\n\n` + llmContent; - returnDisplay = - `**Previous Topic Summary:**\n${previousSummary}\n\n---\n\n` + - returnDisplay; + let llmContent = ''; + let returnDisplay = ''; + + if (isNewTopic) { + // Handle New Topic Header & Summary + llmContent = `Current topic: "${currentTopic}"\nTopic summary: ${summary || '...'}`; + returnDisplay = `## 📂 Topic: **${currentTopic}**\n\n**Summary:**\n${summary || '...'}`; + + if (strategicIntent && strategicIntent.trim()) { + llmContent += `\n\nStrategic Intent: ${strategicIntent.trim()}`; + returnDisplay += `\n\n> [!STRATEGY]\n> **Intent:** ${strategicIntent.trim()}`; + } + } else { + // Tactical update only + llmContent = `Strategic Intent: ${currentIntent}`; + returnDisplay = `> [!STRATEGY]\n> **Intent:** ${currentIntent}`; } return { @@ -124,20 +140,20 @@ class CreateNewTopicInvocation extends BaseToolInvocation< } /** - * Tool to create a new semantic topic (chapter) for UI grouping. + * Tool to update semantic topic context and tactical intent for UI grouping and model focus. */ -export class CreateNewTopicTool extends BaseDeclarativeTool< - CreateNewTopicParams, +export class UpdateTopicTool extends BaseDeclarativeTool< + UpdateTopicParams, ToolResult > { constructor( private readonly config: Config, messageBus: MessageBus, ) { - const declaration = getCreateNewTopicDeclaration(); + const declaration = getUpdateTopicDeclaration(); super( - CREATE_NEW_TOPIC_TOOL_NAME, - 'Create New Topic', + UPDATE_TOPIC_TOOL_NAME, + 'Update Topic Context', declaration.description ?? '', Kind.Think, declaration.parametersJsonSchema, @@ -146,10 +162,10 @@ export class CreateNewTopicTool extends BaseDeclarativeTool< } protected createInvocation( - params: CreateNewTopicParams, + params: UpdateTopicParams, messageBus: MessageBus, - ): CreateNewTopicInvocation { - return new CreateNewTopicInvocation( + ): UpdateTopicInvocation { + return new UpdateTopicInvocation( params, messageBus, this.name,