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.
This commit is contained in:
Abhijit Balaji
2026-03-19 13:23:05 -07:00
parent 731433499b
commit 7bc55f4db4
6 changed files with 87 additions and 7 deletions
@@ -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"
@@ -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
@@ -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);
});
});
+4 -4
View File
@@ -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**.
+1 -1
View File
@@ -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');
});
+2 -2
View File
@@ -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()}**`,
};
}
}