@bubblebrain-ai/bubble 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.js +1 -1
- package/dist/agent/profiles.d.ts +59 -0
- package/dist/agent/profiles.js +460 -0
- package/dist/agent/subagent-control.d.ts +52 -0
- package/dist/agent/subagent-control.js +38 -0
- package/dist/agent/task-size.d.ts +9 -0
- package/dist/agent/task-size.js +33 -0
- package/dist/agent/tool-intent.d.ts +1 -0
- package/dist/agent/tool-intent.js +1 -1
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +648 -55
- package/dist/context/budget.js +1 -0
- package/dist/context/compact-llm.js +7 -6
- package/dist/context/compact.js +6 -6
- package/dist/context/projector.d.ts +3 -3
- package/dist/context/projector.js +32 -18
- package/dist/context/prune.d.ts +2 -2
- package/dist/context/prune.js +1 -4
- package/dist/main.js +12 -5
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +85 -35
- package/dist/orchestrator/hooks.d.ts +5 -3
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +11 -1
- package/dist/prompt/environment.js +23 -2
- package/dist/prompt/provider-prompts/deepseek.js +1 -2
- package/dist/prompt/provider-prompts/kimi.js +1 -2
- package/dist/prompt/reminders.d.ts +21 -2
- package/dist/prompt/reminders.js +53 -8
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +17 -23
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +16 -8
- package/dist/provider.js +149 -34
- package/dist/session-log.js +3 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +355 -0
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +3 -1
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +228 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +75 -56
- package/dist/tools/exit-plan-mode.js +3 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +32 -0
- package/dist/tools/file-state.d.ts +25 -0
- package/dist/tools/file-state.js +52 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +9 -7
- package/dist/tools/lsp.js +2 -0
- package/dist/tools/memory.js +2 -0
- package/dist/tools/question.js +2 -0
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.js +6 -1
- package/dist/tools/skill.js +1 -0
- package/dist/tools/task.js +1 -0
- package/dist/tools/todo.js +1 -0
- package/dist/tools/tool-search.js +2 -1
- package/dist/tools/web-fetch.js +1 -0
- package/dist/tools/web-search.js +1 -0
- package/dist/tools/write.d.ts +4 -3
- package/dist/tools/write.js +135 -54
- package/dist/tui/display-history.d.ts +10 -1
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.js +811 -274
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +114 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/types.d.ts +105 -10
- package/package.json +1 -1
package/dist/context/budget.js
CHANGED
|
@@ -44,9 +44,9 @@ content, write "None".
|
|
|
44
44
|
- The single most natural next action, if obvious.`;
|
|
45
45
|
export async function compactMessagesWithLLM(messages, options) {
|
|
46
46
|
const keepRecentTurns = options.keepRecentTurns ?? 2;
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const turnStartIndexes =
|
|
47
|
+
const preservedContextMessages = messages.filter((m) => m.role === "system" || m.role === "meta");
|
|
48
|
+
const conversationalMessages = messages.filter((m) => m.role !== "system" && m.role !== "meta");
|
|
49
|
+
const turnStartIndexes = conversationalMessages
|
|
50
50
|
.map((m, i) => (m.role === "user" ? i : -1))
|
|
51
51
|
.filter((i) => i >= 0);
|
|
52
52
|
if (turnStartIndexes.length <= keepRecentTurns) {
|
|
@@ -56,8 +56,8 @@ export async function compactMessagesWithLLM(messages, options) {
|
|
|
56
56
|
if (keepStartIndex <= 0) {
|
|
57
57
|
return { compacted: false };
|
|
58
58
|
}
|
|
59
|
-
const oldMessages =
|
|
60
|
-
const keptMessages =
|
|
59
|
+
const oldMessages = conversationalMessages.slice(0, keepStartIndex);
|
|
60
|
+
const keptMessages = conversationalMessages.slice(keepStartIndex);
|
|
61
61
|
let summary;
|
|
62
62
|
try {
|
|
63
63
|
summary = await generateSummary(oldMessages, options);
|
|
@@ -72,7 +72,7 @@ export async function compactMessagesWithLLM(messages, options) {
|
|
|
72
72
|
compacted: true,
|
|
73
73
|
summary,
|
|
74
74
|
messages: [
|
|
75
|
-
...
|
|
75
|
+
...preservedContextMessages,
|
|
76
76
|
{ role: "system", content: `Previous conversation summary:\n${summary}` },
|
|
77
77
|
...keptMessages,
|
|
78
78
|
],
|
|
@@ -112,6 +112,7 @@ function serializeTranscript(messages) {
|
|
|
112
112
|
lines.push(`[tool] ${truncate(message.content, 800)}`);
|
|
113
113
|
break;
|
|
114
114
|
case "system":
|
|
115
|
+
case "meta":
|
|
115
116
|
break;
|
|
116
117
|
}
|
|
117
118
|
}
|
package/dist/context/compact.js
CHANGED
|
@@ -43,9 +43,9 @@ export function compactSessionEntries(entries, options = {}) {
|
|
|
43
43
|
export function compactMessages(messages, options = {}) {
|
|
44
44
|
const keepRecentTurns = options.keepRecentTurns ?? 2;
|
|
45
45
|
const maxSummaryItems = options.maxSummaryItems ?? 4;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const turnStartIndexes =
|
|
46
|
+
const preservedContextMessages = messages.filter((message) => message.role === "system" || message.role === "meta");
|
|
47
|
+
const conversationalMessages = messages.filter((message) => message.role !== "system" && message.role !== "meta");
|
|
48
|
+
const turnStartIndexes = conversationalMessages
|
|
49
49
|
.map((message, index) => (message.role === "user" ? index : -1))
|
|
50
50
|
.filter((index) => index >= 0);
|
|
51
51
|
if (turnStartIndexes.length <= keepRecentTurns) {
|
|
@@ -55,14 +55,14 @@ export function compactMessages(messages, options = {}) {
|
|
|
55
55
|
if (keepStartIndex <= 0) {
|
|
56
56
|
return { compacted: false };
|
|
57
57
|
}
|
|
58
|
-
const oldMessages =
|
|
59
|
-
const keptMessages =
|
|
58
|
+
const oldMessages = conversationalMessages.slice(0, keepStartIndex);
|
|
59
|
+
const keptMessages = conversationalMessages.slice(keepStartIndex);
|
|
60
60
|
const summary = buildMessageSummary(oldMessages, maxSummaryItems);
|
|
61
61
|
if (!summary) {
|
|
62
62
|
return { compacted: false };
|
|
63
63
|
}
|
|
64
64
|
const compactedMessages = [
|
|
65
|
-
...
|
|
65
|
+
...preservedContextMessages.map((message) => cloneMessage(message)),
|
|
66
66
|
{
|
|
67
67
|
role: "system",
|
|
68
68
|
content: `Previous conversation summary:\n${summary}`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message } from "../types.js";
|
|
1
|
+
import type { Message, ProviderMessage } from "../types.js";
|
|
2
2
|
export interface ProjectionOptions {
|
|
3
3
|
mode?: "full" | "pruned" | "budgeted";
|
|
4
4
|
providerId?: string;
|
|
@@ -6,7 +6,7 @@ export interface ProjectionOptions {
|
|
|
6
6
|
usageAnchorTokens?: number;
|
|
7
7
|
anchorMessageCount?: number;
|
|
8
8
|
}
|
|
9
|
-
export declare function projectMessages(messages: Message[], options?: ProjectionOptions):
|
|
9
|
+
export declare function projectMessages(messages: Message[], options?: ProjectionOptions): ProviderMessage[];
|
|
10
10
|
/**
|
|
11
11
|
* Ensures every assistant `tool_calls` is followed (in order) by tool messages
|
|
12
12
|
* responding to each tool_call_id, with no foreign messages interleaved.
|
|
@@ -23,4 +23,4 @@ export declare function projectMessages(messages: Message[], options?: Projectio
|
|
|
23
23
|
* synthesize placeholder tool messages for any tool_call_id with no captured
|
|
24
24
|
* result. Other messages keep their original order.
|
|
25
25
|
*/
|
|
26
|
-
export declare function repairToolCallChains(messages:
|
|
26
|
+
export declare function repairToolCallChains(messages: ProviderMessage[]): ProviderMessage[];
|
|
@@ -3,29 +3,33 @@ import { compactMessages } from "./compact.js";
|
|
|
3
3
|
import { pruneMessages } from "./prune.js";
|
|
4
4
|
export function projectMessages(messages, options = {}) {
|
|
5
5
|
const mode = options.mode ?? "full";
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const flushSystemBuffer = () => {
|
|
9
|
-
if (systemBuffer.length === 0)
|
|
10
|
-
return;
|
|
11
|
-
projected.push({
|
|
12
|
-
role: "system",
|
|
13
|
-
content: systemBuffer.join("\n\n"),
|
|
14
|
-
});
|
|
15
|
-
systemBuffer = [];
|
|
16
|
-
};
|
|
6
|
+
const projectedBody = [];
|
|
7
|
+
const systemContext = [];
|
|
17
8
|
for (const message of messages) {
|
|
18
9
|
if (message.role === "system") {
|
|
19
|
-
|
|
10
|
+
systemContext.push(message.content);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (message.role === "meta") {
|
|
14
|
+
if (message.includeInLlm !== false) {
|
|
15
|
+
systemContext.push(formatMetaMessage(message));
|
|
16
|
+
}
|
|
20
17
|
continue;
|
|
21
18
|
}
|
|
22
|
-
flushSystemBuffer();
|
|
23
19
|
if (message.role === "assistant" && isEmptyAssistantMessage(message)) {
|
|
24
20
|
continue;
|
|
25
21
|
}
|
|
26
|
-
|
|
22
|
+
projectedBody.push(cloneMessage(message));
|
|
27
23
|
}
|
|
28
|
-
|
|
24
|
+
const projected = [
|
|
25
|
+
...(systemContext.length > 0
|
|
26
|
+
? [{
|
|
27
|
+
role: "system",
|
|
28
|
+
content: systemContext.join("\n\n"),
|
|
29
|
+
}]
|
|
30
|
+
: []),
|
|
31
|
+
...projectedBody,
|
|
32
|
+
];
|
|
29
33
|
const repaired = repairToolCallChains(projected);
|
|
30
34
|
if (mode === "pruned") {
|
|
31
35
|
return pruneMessages(repaired);
|
|
@@ -48,12 +52,13 @@ export function projectMessages(messages, options = {}) {
|
|
|
48
52
|
if (!compacted.compacted || !compacted.messages) {
|
|
49
53
|
return pruned;
|
|
50
54
|
}
|
|
51
|
-
const
|
|
55
|
+
const compactedMessages = compacted.messages;
|
|
56
|
+
const afterFirstPass = getContextBudget(options.providerId, options.modelId, compactedMessages);
|
|
52
57
|
if (!afterFirstPass.shouldCompact) {
|
|
53
|
-
return repairToolCallChains(
|
|
58
|
+
return repairToolCallChains(compactedMessages);
|
|
54
59
|
}
|
|
55
60
|
const tighter = compactMessages(pruned, { keepRecentTurns: 1 });
|
|
56
|
-
const finalMessages = tighter.compacted && tighter.messages ? tighter.messages :
|
|
61
|
+
const finalMessages = (tighter.compacted && tighter.messages ? tighter.messages : compactedMessages);
|
|
57
62
|
return repairToolCallChains(finalMessages);
|
|
58
63
|
}
|
|
59
64
|
return repaired;
|
|
@@ -130,6 +135,15 @@ function isEmptyAssistantMessage(message) {
|
|
|
130
135
|
const hasToolCalls = !!message.toolCalls && message.toolCalls.length > 0;
|
|
131
136
|
return !hasContent && !hasReasoning && !hasToolCalls;
|
|
132
137
|
}
|
|
138
|
+
function formatMetaMessage(message) {
|
|
139
|
+
switch (message.kind) {
|
|
140
|
+
case "system-reminder":
|
|
141
|
+
return `Runtime reminder:\n${message.content}`;
|
|
142
|
+
case "runtime-context":
|
|
143
|
+
default:
|
|
144
|
+
return `Runtime context:\n${message.content}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
133
147
|
function cloneMessage(message) {
|
|
134
148
|
if (message.role === "assistant") {
|
|
135
149
|
return {
|
package/dist/context/prune.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Message } from "../types.js";
|
|
2
|
-
export declare function pruneMessages(messages:
|
|
2
|
+
export declare function pruneMessages<T extends Message>(messages: T[]): T[];
|
|
3
3
|
/**
|
|
4
4
|
* Aggressive variant of pruneMessages: drops the content of every prunable
|
|
5
5
|
* tool output except the latest unresolved tool turn that the model still
|
|
6
6
|
* needs to reason over. Used as a last-resort microcompact pass when a
|
|
7
7
|
* standard prune hasn't reclaimed enough budget.
|
|
8
8
|
*/
|
|
9
|
-
export declare function aggressivePruneMessages(messages:
|
|
9
|
+
export declare function aggressivePruneMessages<T extends Message>(messages: T[]): T[];
|
package/dist/context/prune.js
CHANGED
|
@@ -96,10 +96,7 @@ export function aggressivePruneMessages(messages) {
|
|
|
96
96
|
function collectProtectedToolCallIds(messages) {
|
|
97
97
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
98
98
|
const message = messages[index];
|
|
99
|
-
if (message.role === "tool" || message.role === "system") {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (message.role === "user" && message.isMeta) {
|
|
99
|
+
if (message.role === "tool" || message.role === "system" || message.role === "meta") {
|
|
103
100
|
continue;
|
|
104
101
|
}
|
|
105
102
|
if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Agent } from "./agent.js";
|
|
7
|
+
import { BudgetLedger } from "./agent/budget-ledger.js";
|
|
7
8
|
import { parseArgs, printHelp } from "./cli.js";
|
|
8
9
|
import { UserConfig } from "./config.js";
|
|
9
10
|
import { createProviderInstance, createUnavailableProvider } from "./provider.js";
|
|
@@ -185,6 +186,8 @@ async function main() {
|
|
|
185
186
|
: (sessionThinkingLevel ?? args.thinkingLevel ?? configuredThinkingLevel ?? "off");
|
|
186
187
|
const restoredTodos = sessionManager?.getTodos() ?? [];
|
|
187
188
|
const initialMode = args.mode ?? "default";
|
|
189
|
+
const skillSummaries = skillRegistry.summaries();
|
|
190
|
+
const memoryPrompt = buildMemoryPrompt(args.cwd);
|
|
188
191
|
const systemPrompt = buildSystemPrompt({
|
|
189
192
|
agentName: "Bubble",
|
|
190
193
|
configuredProvider: activeProviderId || "none",
|
|
@@ -194,9 +197,10 @@ async function main() {
|
|
|
194
197
|
mode: initialMode,
|
|
195
198
|
workingDir: args.cwd,
|
|
196
199
|
tools: tools.map((tool) => tool.name),
|
|
197
|
-
skills:
|
|
198
|
-
memoryPrompt
|
|
200
|
+
skills: skillSummaries,
|
|
201
|
+
memoryPrompt,
|
|
199
202
|
});
|
|
203
|
+
const budgetLedger = new BudgetLedger();
|
|
200
204
|
const agent = new Agent({
|
|
201
205
|
provider: activeProvider
|
|
202
206
|
? createProvider(activeProviderId, activeProvider.apiKey, activeProvider.baseURL)
|
|
@@ -215,9 +219,9 @@ async function main() {
|
|
|
215
219
|
return;
|
|
216
220
|
if (message.role === "system")
|
|
217
221
|
return;
|
|
218
|
-
//
|
|
222
|
+
// Runtime meta messages are ephemeral; don't persist them —
|
|
219
223
|
// they will be re-injected as needed on resume based on the current mode.
|
|
220
|
-
if (message.role === "
|
|
224
|
+
if (message.role === "meta")
|
|
221
225
|
return;
|
|
222
226
|
sessionManager.appendMessage(message);
|
|
223
227
|
if (message.role === "assistant") {
|
|
@@ -240,6 +244,9 @@ async function main() {
|
|
|
240
244
|
onModeUpdate: (mode) => {
|
|
241
245
|
sessionManager?.appendMarker("mode_switch", mode);
|
|
242
246
|
},
|
|
247
|
+
budgetLedger,
|
|
248
|
+
skills: skillSummaries,
|
|
249
|
+
memoryPrompt,
|
|
243
250
|
});
|
|
244
251
|
agentRef = agent;
|
|
245
252
|
if (sessionManager) {
|
|
@@ -281,7 +288,7 @@ async function main() {
|
|
|
281
288
|
const history = sessionManager.getMessages();
|
|
282
289
|
if (history.length > 0) {
|
|
283
290
|
agent.messages = [{ role: "system", content: systemPrompt }, ...history];
|
|
284
|
-
// Reassigning agent.messages drops any
|
|
291
|
+
// Reassigning agent.messages drops any runtime meta reminder injected during
|
|
285
292
|
// construction. Re-inject if the agent is starting in plan mode.
|
|
286
293
|
if (agent.mode === "plan") {
|
|
287
294
|
agent.injectModeReminder();
|
package/dist/mcp/manager.js
CHANGED
|
@@ -164,6 +164,7 @@ function buildToolEntry(serverName, tool, getClient) {
|
|
|
164
164
|
description,
|
|
165
165
|
parameters,
|
|
166
166
|
readOnly: false, // Conservative default; user can allow-list.
|
|
167
|
+
effect: "unknown",
|
|
167
168
|
deferred: true, // Load schema on demand via tool_search to keep context small.
|
|
168
169
|
async execute(args) {
|
|
169
170
|
const client = getClient();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { classifyTask } from "../agent/task-classifier.js";
|
|
2
|
+
import { classifyTaskSize } from "../agent/task-size.js";
|
|
2
3
|
import { EvidenceTracker } from "../agent/evidence-tracker.js";
|
|
3
4
|
import { ExecutionGovernor } from "../agent/execution-governor.js";
|
|
4
5
|
import { arbitrateToolCall } from "../agent/tool-arbiter.js";
|
|
5
|
-
import {
|
|
6
|
+
import { buildEditRetryEscalationReminder, buildRedundantReadReminder, buildSmallTaskHint, buildTaskSummaryReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
|
|
6
7
|
import { reminderForTaskType } from "../prompt/task-reminders.js";
|
|
7
8
|
import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
|
|
8
9
|
export function createDefaultHooks() {
|
|
@@ -16,6 +17,13 @@ export function createDefaultHooks() {
|
|
|
16
17
|
if (taskReminder) {
|
|
17
18
|
ctx.queueReminder(taskReminder);
|
|
18
19
|
}
|
|
20
|
+
// Small-task hint: counterweight to the default protocol's exploration
|
|
21
|
+
// bias, only fires once per run on focused one-shot requests like
|
|
22
|
+
// "写个 HTML 介绍元旦". Don't issue for the same input twice.
|
|
23
|
+
if (!ctx.state.smallTaskHintSent && classifyTaskSize(ctx.input) === "small") {
|
|
24
|
+
ctx.state.smallTaskHintSent = true;
|
|
25
|
+
ctx.queueReminder(buildSmallTaskHint());
|
|
26
|
+
}
|
|
19
27
|
if (taskType === "security_investigation") {
|
|
20
28
|
ctx.state.evidenceTracker = new EvidenceTracker();
|
|
21
29
|
ctx.state.workflowPhase = "investigate";
|
|
@@ -75,12 +83,58 @@ export function createDefaultHooks() {
|
|
|
75
83
|
}
|
|
76
84
|
ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
|
|
77
85
|
ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
|
|
78
|
-
if
|
|
79
|
-
|
|
86
|
+
// Edit/write retry-escalation: if the same tool with the same args
|
|
87
|
+
// failed twice in a row, models — especially thinking-heavy ones —
|
|
88
|
+
// can spiral on "identical content" / "not found" errors. Nudge them
|
|
89
|
+
// to change strategy.
|
|
90
|
+
if ((ctx.toolCall.name === "edit" || ctx.toolCall.name === "write") && ctx.result.isError) {
|
|
91
|
+
const hash = hashEditCall(ctx.toolCall);
|
|
92
|
+
const history = ctx.state.recentEditFailures ?? (ctx.state.recentEditFailures = []);
|
|
93
|
+
history.push(hash);
|
|
94
|
+
// Keep last 4 entries.
|
|
95
|
+
if (history.length > 4)
|
|
96
|
+
history.shift();
|
|
97
|
+
const len = history.length;
|
|
98
|
+
if (len >= 2 && history[len - 1] === history[len - 2] && !ctx.state.editRetryReminderSent) {
|
|
99
|
+
ctx.state.editRetryReminderSent = true;
|
|
100
|
+
const summary = ctx.result.content.split("\n")[0] || "";
|
|
101
|
+
ctx.queueReminder(buildEditRetryEscalationReminder(`Last failure: ${ctx.toolCall.name} on the same target with identical arguments. ${summary}`));
|
|
102
|
+
}
|
|
80
103
|
}
|
|
81
|
-
else if (ctx.
|
|
82
|
-
|
|
104
|
+
else if ((ctx.toolCall.name === "edit" || ctx.toolCall.name === "write") && !ctx.result.isError) {
|
|
105
|
+
// Successful mutation resets the dedup state so a later, unrelated
|
|
106
|
+
// failure won't fire the reminder spuriously.
|
|
107
|
+
ctx.state.recentEditFailures = [];
|
|
108
|
+
ctx.state.editRetryReminderSent = false;
|
|
109
|
+
}
|
|
110
|
+
// Redundant-Read detection: same file path read twice within this turn.
|
|
111
|
+
// Soft single-shot reminder, governor handles cumulative read budgets.
|
|
112
|
+
if (ctx.toolCall.name === "read" && !ctx.result.isError) {
|
|
113
|
+
const rawPath = ctx.toolCall.parsedArgs?.path ?? ctx.toolCall.parsedArgs?.file_path;
|
|
114
|
+
const path = typeof rawPath === "string" ? rawPath : undefined;
|
|
115
|
+
if (path) {
|
|
116
|
+
const seen = ctx.state.recentReadPaths ?? (ctx.state.recentReadPaths = []);
|
|
117
|
+
const flagged = ctx.state.redundantReadReminded ?? (ctx.state.redundantReadReminded = new Set());
|
|
118
|
+
if (seen.includes(path) && !flagged.has(path)) {
|
|
119
|
+
flagged.add(path);
|
|
120
|
+
ctx.queueReminder(buildRedundantReadReminder(path));
|
|
121
|
+
}
|
|
122
|
+
seen.push(path);
|
|
123
|
+
if (seen.length > 16)
|
|
124
|
+
seen.shift();
|
|
125
|
+
}
|
|
83
126
|
}
|
|
127
|
+
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
128
|
+
markCodeChanged(ctx.state);
|
|
129
|
+
}
|
|
130
|
+
// Removed: active verification tracking. The previous design nagged the
|
|
131
|
+
// model every turn until it ran a recognised verification command, and
|
|
132
|
+
// narrowly accepted only test/lint commands — which meant ad-hoc python
|
|
133
|
+
// checks did not count, the nag never cleared, and reasoning models
|
|
134
|
+
// (DeepSeek v4-pro with hex-blindness) spiraled trying to "prove" the
|
|
135
|
+
// edit was correct. CC's approach is the opposite: verify when there
|
|
136
|
+
// is something real to verify, say so explicitly when there isn't, and
|
|
137
|
+
// trust the model to judge. We follow that.
|
|
84
138
|
if (ctx.toolCall.name === "task") {
|
|
85
139
|
ctx.queueReminder(buildTaskSummaryReminder());
|
|
86
140
|
}
|
|
@@ -101,48 +155,44 @@ export function createDefaultHooks() {
|
|
|
101
155
|
if (ctx.state.governor?.snapshot().searchFrozen && allSearchResultsWereLowSignal) {
|
|
102
156
|
ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
|
|
103
157
|
}
|
|
104
|
-
|
|
105
|
-
if (changedThisTurn && !ctx.state.verificationCompleted && !ctx.state.verificationReminderQueued) {
|
|
106
|
-
ctx.state.verificationReminderQueued = true;
|
|
107
|
-
ctx.queueReminder(buildVerificationReminder("The previous turn changed files and no verification evidence has been observed yet."));
|
|
108
|
-
}
|
|
158
|
+
// Verification reminders intentionally removed. See afterToolCall.
|
|
109
159
|
},
|
|
110
|
-
afterTurn(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
ctx.state.forceContinuationReason = "Files were changed but no verification evidence was observed before the final answer.";
|
|
114
|
-
ctx.queueReminder(buildVerificationReminder(ctx.state.forceContinuationReason));
|
|
115
|
-
}
|
|
160
|
+
afterTurn() {
|
|
161
|
+
// Verification force-continuation removed. The model decides whether
|
|
162
|
+
// verification is meaningful for the task, per the system prompt.
|
|
116
163
|
},
|
|
117
164
|
},
|
|
118
165
|
];
|
|
119
166
|
}
|
|
167
|
+
function markCodeChanged(state) {
|
|
168
|
+
state.codeChanged = true;
|
|
169
|
+
}
|
|
120
170
|
function isCodeWriteResult(_toolCall, result) {
|
|
121
171
|
if (result.isError || result.status === "blocked" || result.status === "command_error") {
|
|
122
172
|
return false;
|
|
123
173
|
}
|
|
124
174
|
return result.metadata?.kind === "write" || result.metadata?.kind === "edit";
|
|
125
175
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
176
|
+
function hashEditCall(toolCall) {
|
|
177
|
+
// Cheap fingerprint that identifies "same edit/write call". JSON of the
|
|
178
|
+
// sorted parsed args is good enough — we only need stable equality between
|
|
179
|
+
// identical calls, not cryptographic strength.
|
|
180
|
+
try {
|
|
181
|
+
return `${toolCall.name}:${stableStringify(toolCall.parsedArgs)}`;
|
|
129
182
|
}
|
|
130
|
-
|
|
131
|
-
return
|
|
183
|
+
catch {
|
|
184
|
+
return `${toolCall.name}:${toolCall.arguments}`;
|
|
132
185
|
}
|
|
133
|
-
if (toolCall.name !== "bash") {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
const command = typeof result.metadata?.command === "string"
|
|
137
|
-
? result.metadata.command
|
|
138
|
-
: typeof toolCall.parsedArgs.command === "string"
|
|
139
|
-
? toolCall.parsedArgs.command
|
|
140
|
-
: "";
|
|
141
|
-
return isVerificationCommand(command);
|
|
142
186
|
}
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
function stableStringify(value) {
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
190
|
+
}
|
|
191
|
+
if (value && typeof value === "object") {
|
|
192
|
+
const entries = Object.keys(value)
|
|
193
|
+
.sort()
|
|
194
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
195
|
+
return `{${entries.join(",")}}`;
|
|
196
|
+
}
|
|
197
|
+
return JSON.stringify(value ?? null);
|
|
148
198
|
}
|
|
@@ -14,9 +14,11 @@ export interface TurnHookState {
|
|
|
14
14
|
forceTextOnlyReason?: string;
|
|
15
15
|
forceContinuationReason?: string;
|
|
16
16
|
codeChanged?: boolean;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
smallTaskHintSent?: boolean;
|
|
18
|
+
recentEditFailures?: string[];
|
|
19
|
+
editRetryReminderSent?: boolean;
|
|
20
|
+
recentReadPaths?: string[];
|
|
21
|
+
redundantReadReminded?: Set<string>;
|
|
20
22
|
taskBudget?: {
|
|
21
23
|
total: number;
|
|
22
24
|
spent: number;
|
package/dist/prompt/compose.d.ts
CHANGED
|
@@ -8,5 +8,6 @@ export interface ComposeSystemPromptOptions extends EnvironmentPromptOptions {
|
|
|
8
8
|
mode?: PermissionMode;
|
|
9
9
|
skills?: SkillSummary[];
|
|
10
10
|
memoryPrompt?: string;
|
|
11
|
+
agentProfilePrompt?: string;
|
|
11
12
|
}
|
|
12
13
|
export declare function composeSystemPrompt(options?: ComposeSystemPromptOptions): string;
|
package/dist/prompt/compose.js
CHANGED
|
@@ -27,7 +27,14 @@ export function composeSystemPrompt(options = {}) {
|
|
|
27
27
|
guidelines: buildGuidelines(options.tools ?? defaultToolNames, options.guidelines ?? []),
|
|
28
28
|
});
|
|
29
29
|
const skillsPrompt = buildSkillsPrompt(options.skills ?? []);
|
|
30
|
-
return [
|
|
30
|
+
return [
|
|
31
|
+
providerPrompt,
|
|
32
|
+
environmentPrompt,
|
|
33
|
+
runtimePrompt,
|
|
34
|
+
options.agentProfilePrompt,
|
|
35
|
+
options.memoryPrompt,
|
|
36
|
+
skillsPrompt,
|
|
37
|
+
].filter(Boolean).join("\n\n");
|
|
31
38
|
}
|
|
32
39
|
function buildProviderPrompt(agentName, providerId, modelId, modelName) {
|
|
33
40
|
const provider = providerId ?? "";
|
|
@@ -72,6 +79,9 @@ function buildGuidelines(tools, extraGuidelines) {
|
|
|
72
79
|
if (tools.includes("question")) {
|
|
73
80
|
add("When the user is explicitly discussing, brainstorming, or shaping an approach instead of asking for immediate execution, use the question tool for targeted clarification or preference choices when it would materially improve the discussion; do not use it for generic permission-to-proceed questions");
|
|
74
81
|
}
|
|
82
|
+
if (tools.includes("todo_write")) {
|
|
83
|
+
add("Use todo_write to plan any task that needs three or more concrete steps before you start. Mark each item completed as soon as it is done; do not batch updates");
|
|
84
|
+
}
|
|
75
85
|
for (const item of extraGuidelines) {
|
|
76
86
|
add(item);
|
|
77
87
|
}
|
|
@@ -9,11 +9,32 @@ export const defaultToolSnippets = {
|
|
|
9
9
|
lsp: "Use the language server for code navigation, symbols, call hierarchy, and type-aware lookup",
|
|
10
10
|
web_search: "Search the public web for current information",
|
|
11
11
|
web_fetch: "Fetch and extract the contents of a specific webpage",
|
|
12
|
-
|
|
12
|
+
spawn_agent: "Start a child subagent thread and return its agent id plus nickname",
|
|
13
|
+
wait_agent: "Wait for one or more spawned subagents to finish",
|
|
14
|
+
send_input: "Send follow-up input to an existing subagent thread",
|
|
15
|
+
close_agent: "Close or cancel a spawned subagent thread",
|
|
13
16
|
question: "Ask the user structured questions when clarification or preference choices would materially improve the work",
|
|
14
17
|
skill: "Load a named skill with specialized instructions and bundled resources",
|
|
18
|
+
todo_write: "Plan and track multi-step work. Mark each task completed as soon as it is done — do not batch.",
|
|
15
19
|
};
|
|
16
|
-
export const defaultToolNames = [
|
|
20
|
+
export const defaultToolNames = [
|
|
21
|
+
"read",
|
|
22
|
+
"glob",
|
|
23
|
+
"bash",
|
|
24
|
+
"edit",
|
|
25
|
+
"write",
|
|
26
|
+
"grep",
|
|
27
|
+
"lsp",
|
|
28
|
+
"web_search",
|
|
29
|
+
"web_fetch",
|
|
30
|
+
"spawn_agent",
|
|
31
|
+
"wait_agent",
|
|
32
|
+
"send_input",
|
|
33
|
+
"close_agent",
|
|
34
|
+
"question",
|
|
35
|
+
"skill",
|
|
36
|
+
"todo_write",
|
|
37
|
+
];
|
|
17
38
|
export function buildEnvironmentPrompt(options = {}) {
|
|
18
39
|
const configuredProvider = options.configuredProvider ?? "unknown";
|
|
19
40
|
const configuredModel = options.configuredModel ?? "unknown";
|
|
@@ -3,6 +3,5 @@ export function buildDeepSeekProviderPrompt(agentName) {
|
|
|
3
3
|
|
|
4
4
|
Prefer short plans followed by concrete tool use. Avoid broad speculation.
|
|
5
5
|
After each tool result, update your understanding before choosing the next action.
|
|
6
|
-
Do not repeat equivalent searches unless the previous result changed the search space
|
|
7
|
-
When provider/API behavior is involved, inspect serialization and request-shape code before changing generic agent logic.`;
|
|
6
|
+
Do not repeat equivalent searches unless the previous result changed the search space.`;
|
|
8
7
|
}
|
|
@@ -2,6 +2,5 @@ export function buildKimiProviderPrompt(agentName) {
|
|
|
2
2
|
return `You are ${agentName}, a terminal coding agent running on a Kimi/Moonshot model.
|
|
3
3
|
|
|
4
4
|
Keep tool use disciplined: pursue one concrete hypothesis at a time, read results carefully, and converge after evidence is sufficient.
|
|
5
|
-
Do not fan out into many parallel search directions unless the task truly requires it
|
|
6
|
-
For tool-call or reasoning-mode issues, inspect message history serialization before changing unrelated agent behavior.`;
|
|
5
|
+
Do not fan out into many parallel search directions unless the task truly requires it.`;
|
|
7
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System reminders - short, runtime-variable instructions injected into the
|
|
3
|
-
* message stream as
|
|
3
|
+
* message stream as hidden meta messages.
|
|
4
4
|
*
|
|
5
5
|
* Rationale: the static system prompt is stable and cacheable. Mode transitions
|
|
6
6
|
* and other ephemeral state are signaled via reminders so we do not invalidate
|
|
@@ -30,4 +30,23 @@ export declare function buildWorkflowPhaseReminder(input: {
|
|
|
30
30
|
pending: string[];
|
|
31
31
|
}): string;
|
|
32
32
|
export declare function buildTaskSummaryReminder(): string;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Fired when the same edit/write tool call (identical tool name + args) has
|
|
35
|
+
* just failed for the second time in a row. Models — especially thinking-heavy
|
|
36
|
+
* ones — can otherwise spiral on `No changes made: identical content` or
|
|
37
|
+
* `oldText not found` because their internal reasoning convinces them they
|
|
38
|
+
* are typing the change correctly even though the JSON args arrive identical.
|
|
39
|
+
* This nudge forces a strategy change.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildEditRetryEscalationReminder(reason: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Fired the FIRST time the model re-reads a file it already read in this turn.
|
|
44
|
+
* Soft — does not freeze the tool. Just prevents a 3rd / 4th re-read.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildRedundantReadReminder(path: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Injected once at task start when the user's input looks like a small,
|
|
49
|
+
* focused task (e.g. "write an HTML page about X"). Counterweight to the
|
|
50
|
+
* default protocol which biases toward thorough exploration.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildSmallTaskHint(): string;
|