From 7bc55f4db4357a82cd67c5888c25f70a4e3e8c21 Mon Sep 17 00:00:00 2001 From: Abhijit Balaji Date: Thu, 19 Mar 2026 13:23:05 -0700 Subject: [PATCH] feat(core): always allow topic tool and refine reasoning mandate - Added explicit policy rules to always allow 'create_new_topic' in all modes. - Updated topic tool output to use 'Current topic' phrasing for clarity. - Replaced the 'Thinking Protocol' with a mandate for internal reasoning to prevent literal thought block output in the UI. - Added and updated unit tests to verify policy and tool behavior. --- packages/core/src/policy/policies/plan.toml | 7 ++ .../core/src/policy/policies/read-only.toml | 6 ++ packages/core/src/policy/topic-policy.test.ts | 67 +++++++++++++++++++ packages/core/src/prompts/snippets.ts | 8 +-- packages/core/src/tools/topicTool.test.ts | 2 +- packages/core/src/tools/topicTool.ts | 4 +- 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/policy/topic-policy.test.ts diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index 5a7ee6e59f..bcd687c409 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -101,6 +101,13 @@ decision = "allow" priority = 70 modes = ["plan"] +# Topic grouping tool is innocuous and used for UI organization. +[[rule]] +toolName = "create_new_topic" +decision = "allow" +priority = 70 +modes = ["plan"] + [[rule]] toolName = ["ask_user", "save_memory"] decision = "ask_user" diff --git a/packages/core/src/policy/policies/read-only.toml b/packages/core/src/policy/policies/read-only.toml index 8435e49d0b..dacc027859 100644 --- a/packages/core/src/policy/policies/read-only.toml +++ b/packages/core/src/policy/policies/read-only.toml @@ -55,4 +55,10 @@ priority = 50 [[rule]] toolName = ["codebase_investigator", "cli_help", "get_internal_docs"] decision = "allow" +priority = 50 + +# Topic grouping tool is innocuous and used for UI organization. +[[rule]] +toolName = "create_new_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 new file mode 100644 index 0000000000..e0300ed443 --- /dev/null +++ b/packages/core/src/policy/topic-policy.test.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +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'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('Topic Tool Policy', () => { + async function loadDefaultPolicies() { + const policiesDir = path.resolve(__dirname, 'policies'); + const getPolicyTier = () => 1; // Default tier + const result = await loadPoliciesFromToml([policiesDir], getPolicyTier); + return result.rules; + } + + it('should allow create_new_topic in DEFAULT mode', async () => { + const rules = await loadDefaultPolicies(); + const engine = new PolicyEngine({ + rules, + approvalMode: ApprovalMode.DEFAULT, + }); + + const result = await engine.check( + { name: CREATE_NEW_TOPIC_TOOL_NAME }, + undefined, + ); + expect(result.decision).toBe(PolicyDecision.ALLOW); + }); + + it('should allow create_new_topic in PLAN mode', async () => { + const rules = await loadDefaultPolicies(); + const engine = new PolicyEngine({ + rules, + approvalMode: ApprovalMode.PLAN, + }); + + const result = await engine.check( + { name: CREATE_NEW_TOPIC_TOOL_NAME }, + undefined, + ); + expect(result.decision).toBe(PolicyDecision.ALLOW); + }); + + it('should allow create_new_topic in YOLO mode', async () => { + const rules = await loadDefaultPolicies(); + const engine = new PolicyEngine({ + rules, + approvalMode: ApprovalMode.YOLO, + }); + + const result = await engine.check( + { name: CREATE_NEW_TOPIC_TOOL_NAME }, + undefined, + ); + expect(result.decision).toBe(PolicyDecision.ALLOW); + }); +}); diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 0be80dc205..4e0cd8854c 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -591,10 +591,10 @@ function mandateTopicUpdateModel(): string { - **Silent Mode:** No conversational filler, no "I will now...", and no summaries between tools. - Only the tool execution is permitted to define the state. Everything in between must be silent. -- **3. Thinking Protocol:** - - Use internal thought blocks to keep track of what tools you have called, plan your next steps, and reason about the task. - - Without reasoning and tracking in thought blocks, you may lose context. - - Always use the required syntax for thought blocks to ensure they remain hidden from the user interface. +- **3. Internal Reasoning:** + - You MUST reason about your plan, track tool calls, and strategize internally before executing tools. + - This reasoning process must remain internal. You are strictly FORBIDDEN from including your reasoning, "thoughts," or explanations in your text response. + - Between tool calls, your text output MUST remain completely empty (Zero-Noise). - **4. Completion:** - Only when the entire task is finalized do you provide a **Final Summary**. diff --git a/packages/core/src/tools/topicTool.test.ts b/packages/core/src/tools/topicTool.test.ts index 1141b6e1c4..2d22359393 100644 --- a/packages/core/src/tools/topicTool.test.ts +++ b/packages/core/src/tools/topicTool.test.ts @@ -59,7 +59,7 @@ describe('CreateNewTopicTool', () => { const invocation = tool.build({ [TOPIC_PARAM_TITLE]: 'New Chapter' }); const result = await invocation.execute(new AbortController().signal); - expect(result.llmContent).toContain('New Chapter'); + expect(result.llmContent).toBe('Current topic: "New Chapter"'); expect(TopicManager.getInstance().getTopic()).toBe('New Chapter'); }); diff --git a/packages/core/src/tools/topicTool.ts b/packages/core/src/tools/topicTool.ts index af1f5f62d2..ed7e7cab2e 100644 --- a/packages/core/src/tools/topicTool.ts +++ b/packages/core/src/tools/topicTool.ts @@ -78,8 +78,8 @@ class CreateNewTopicInvocation extends BaseToolInvocation< TopicManager.getInstance().setTopic(title.trim()); return { - llmContent: `Topic changed to: "${title.trim()}"`, - returnDisplay: `Topic changed to: **${title.trim()}**`, + llmContent: `Current topic: "${title.trim()}"`, + returnDisplay: `Current topic: **${title.trim()}**`, }; } }