@bubblebrain-ai/bubble 0.0.4 → 0.0.5
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.d.ts +60 -1
- package/dist/agent.js +602 -53
- 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 +48 -9
- package/dist/orchestrator/hooks.d.ts +5 -0
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +8 -1
- package/dist/prompt/environment.js +21 -2
- package/dist/prompt/reminders.d.ts +3 -1
- package/dist/prompt/reminders.js +23 -4
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +6 -7
- package/dist/provider.js +77 -15
- 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.js +2 -0
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +197 -0
- package/dist/tools/edit.js +63 -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/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- 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.js +1 -0
- 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.js +2 -0
- package/dist/tui/display-history.d.ts +8 -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 +712 -267
- 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 +22 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +82 -0
- package/dist/types.d.ts +90 -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();
|
|
@@ -2,7 +2,7 @@ import { classifyTask } from "../agent/task-classifier.js";
|
|
|
2
2
|
import { EvidenceTracker } from "../agent/evidence-tracker.js";
|
|
3
3
|
import { ExecutionGovernor } from "../agent/execution-governor.js";
|
|
4
4
|
import { arbitrateToolCall } from "../agent/tool-arbiter.js";
|
|
5
|
-
import { buildTaskSummaryReminder, buildVerificationReminder, buildWorkflowPhaseReminder } from "../prompt/reminders.js";
|
|
5
|
+
import { buildFinalizeOpportunityReminder, buildTaskSummaryReminder, buildVerificationFailureReminder, buildVerificationReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
|
|
6
6
|
import { reminderForTaskType } from "../prompt/task-reminders.js";
|
|
7
7
|
import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
|
|
8
8
|
export function createDefaultHooks() {
|
|
@@ -76,10 +76,19 @@ export function createDefaultHooks() {
|
|
|
76
76
|
ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
|
|
77
77
|
ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
|
|
78
78
|
if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
|
|
79
|
-
ctx.state
|
|
79
|
+
markCodeChanged(ctx.state);
|
|
80
80
|
}
|
|
81
|
-
else if (ctx.state.codeChanged &&
|
|
82
|
-
ctx.state.
|
|
81
|
+
else if (ctx.state.codeChanged && isVerificationAttempt(ctx.toolCall, ctx.result)) {
|
|
82
|
+
ctx.state.verificationAttempted = true;
|
|
83
|
+
if (isSuccessfulToolResult(ctx.result)) {
|
|
84
|
+
ctx.state.verificationCompleted = true;
|
|
85
|
+
ctx.state.verificationFailed = false;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
ctx.state.verificationCompleted = false;
|
|
89
|
+
ctx.state.verificationFailed = true;
|
|
90
|
+
ctx.state.finalizeReminderQueued = false;
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
if (ctx.toolCall.name === "task") {
|
|
85
94
|
ctx.queueReminder(buildTaskSummaryReminder());
|
|
@@ -102,13 +111,27 @@ export function createDefaultHooks() {
|
|
|
102
111
|
ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
|
|
103
112
|
}
|
|
104
113
|
const changedThisTurn = ctx.toolResults.some((result) => result.metadata?.kind === "write" || result.metadata?.kind === "edit");
|
|
105
|
-
if (changedThisTurn && !ctx.state.verificationCompleted && !ctx.state.verificationReminderQueued) {
|
|
114
|
+
if (changedThisTurn && !ctx.state.verificationAttempted && !ctx.state.verificationCompleted && !ctx.state.verificationReminderQueued) {
|
|
106
115
|
ctx.state.verificationReminderQueued = true;
|
|
107
116
|
ctx.queueReminder(buildVerificationReminder("The previous turn changed files and no verification evidence has been observed yet."));
|
|
108
117
|
}
|
|
118
|
+
if (ctx.state.codeChanged && ctx.state.verificationFailed && !ctx.state.verificationFailureReminderQueued) {
|
|
119
|
+
ctx.state.verificationFailureReminderQueued = true;
|
|
120
|
+
ctx.queueReminder(buildVerificationFailureReminder("A verification command or runtime check was attempted after file changes, but it did not pass."));
|
|
121
|
+
}
|
|
122
|
+
if (ctx.state.codeChanged && ctx.state.verificationCompleted && !ctx.state.finalizeReminderQueued) {
|
|
123
|
+
ctx.state.finalizeReminderQueued = true;
|
|
124
|
+
ctx.queueReminder(buildFinalizeOpportunityReminder("A relevant verification command or runtime check passed after file changes."));
|
|
125
|
+
}
|
|
109
126
|
},
|
|
110
127
|
afterTurn(ctx) {
|
|
111
|
-
if (ctx.state.codeChanged &&
|
|
128
|
+
if (ctx.state.codeChanged && ctx.state.verificationFailed && !ctx.state.verificationFailureReminderSent) {
|
|
129
|
+
ctx.state.verificationFailureReminderSent = true;
|
|
130
|
+
ctx.state.forceContinuationReason = "Files were changed, but the latest verification evidence failed.";
|
|
131
|
+
ctx.queueReminder(buildVerificationFailureReminder(ctx.state.forceContinuationReason));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (ctx.state.codeChanged && !ctx.state.verificationAttempted && !ctx.state.verificationCompleted && !ctx.state.finalVerificationReminderSent) {
|
|
112
135
|
ctx.state.finalVerificationReminderSent = true;
|
|
113
136
|
ctx.state.forceContinuationReason = "Files were changed but no verification evidence was observed before the final answer.";
|
|
114
137
|
ctx.queueReminder(buildVerificationReminder(ctx.state.forceContinuationReason));
|
|
@@ -117,16 +140,30 @@ export function createDefaultHooks() {
|
|
|
117
140
|
},
|
|
118
141
|
];
|
|
119
142
|
}
|
|
143
|
+
function markCodeChanged(state) {
|
|
144
|
+
state.codeChanged = true;
|
|
145
|
+
state.verificationAttempted = false;
|
|
146
|
+
state.verificationCompleted = false;
|
|
147
|
+
state.verificationFailed = false;
|
|
148
|
+
state.verificationReminderQueued = false;
|
|
149
|
+
state.finalVerificationReminderSent = false;
|
|
150
|
+
state.verificationFailureReminderQueued = false;
|
|
151
|
+
state.verificationFailureReminderSent = false;
|
|
152
|
+
state.finalizeReminderQueued = false;
|
|
153
|
+
}
|
|
120
154
|
function isCodeWriteResult(_toolCall, result) {
|
|
121
155
|
if (result.isError || result.status === "blocked" || result.status === "command_error") {
|
|
122
156
|
return false;
|
|
123
157
|
}
|
|
124
158
|
return result.metadata?.kind === "write" || result.metadata?.kind === "edit";
|
|
125
159
|
}
|
|
126
|
-
function
|
|
127
|
-
if (result.isError
|
|
160
|
+
function isSuccessfulToolResult(result) {
|
|
161
|
+
if (result.isError) {
|
|
128
162
|
return false;
|
|
129
163
|
}
|
|
164
|
+
return result.status !== "blocked" && result.status !== "command_error" && result.status !== "timeout";
|
|
165
|
+
}
|
|
166
|
+
function isVerificationAttempt(toolCall, result) {
|
|
130
167
|
if (toolCall.name === "lsp") {
|
|
131
168
|
return true;
|
|
132
169
|
}
|
|
@@ -144,5 +181,7 @@ function isVerificationCommand(command) {
|
|
|
144
181
|
const normalized = command.trim().toLowerCase();
|
|
145
182
|
return /\b(npm|pnpm|yarn|bun)\s+(test|run\s+(test|build|typecheck|lint|check|tsc)|exec\s+tsc)\b/.test(normalized)
|
|
146
183
|
|| /\b(npx|pnpm\s+exec|bunx)\s+(vitest|tsc|eslint|playwright)\b/.test(normalized)
|
|
147
|
-
|| /\b(
|
|
184
|
+
|| /\b(python3?|uv\s+run\s+python3?|poetry\s+run\s+python3?)\s+(-m\s+)?(pytest|unittest|ruff|mypy)\b/.test(normalized)
|
|
185
|
+
|| /\b(make|cmake)\s+(test|check)\b/.test(normalized)
|
|
186
|
+
|| /\b(vitest|tsc|pytest|ruff|mypy|ctest|cargo\s+test|go\s+test|swift\s+test|mvn\s+test|gradle\s+test|\.\/gradlew\s+test)\b/.test(normalized);
|
|
148
187
|
}
|
|
@@ -14,9 +14,14 @@ export interface TurnHookState {
|
|
|
14
14
|
forceTextOnlyReason?: string;
|
|
15
15
|
forceContinuationReason?: string;
|
|
16
16
|
codeChanged?: boolean;
|
|
17
|
+
verificationAttempted?: boolean;
|
|
17
18
|
verificationCompleted?: boolean;
|
|
19
|
+
verificationFailed?: boolean;
|
|
18
20
|
verificationReminderQueued?: boolean;
|
|
19
21
|
finalVerificationReminderSent?: boolean;
|
|
22
|
+
verificationFailureReminderQueued?: boolean;
|
|
23
|
+
verificationFailureReminderSent?: boolean;
|
|
24
|
+
finalizeReminderQueued?: boolean;
|
|
20
25
|
taskBudget?: {
|
|
21
26
|
total: number;
|
|
22
27
|
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 ?? "";
|
|
@@ -9,11 +9,30 @@ 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",
|
|
15
18
|
};
|
|
16
|
-
export const defaultToolNames = [
|
|
19
|
+
export const defaultToolNames = [
|
|
20
|
+
"read",
|
|
21
|
+
"glob",
|
|
22
|
+
"bash",
|
|
23
|
+
"edit",
|
|
24
|
+
"write",
|
|
25
|
+
"grep",
|
|
26
|
+
"lsp",
|
|
27
|
+
"web_search",
|
|
28
|
+
"web_fetch",
|
|
29
|
+
"spawn_agent",
|
|
30
|
+
"wait_agent",
|
|
31
|
+
"send_input",
|
|
32
|
+
"close_agent",
|
|
33
|
+
"question",
|
|
34
|
+
"skill",
|
|
35
|
+
];
|
|
17
36
|
export function buildEnvironmentPrompt(options = {}) {
|
|
18
37
|
const configuredProvider = options.configuredProvider ?? "unknown";
|
|
19
38
|
const configuredModel = options.configuredModel ?? "unknown";
|
|
@@ -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
|
|
@@ -31,3 +31,5 @@ export declare function buildWorkflowPhaseReminder(input: {
|
|
|
31
31
|
}): string;
|
|
32
32
|
export declare function buildTaskSummaryReminder(): string;
|
|
33
33
|
export declare function buildVerificationReminder(reason: string): string;
|
|
34
|
+
export declare function buildVerificationFailureReminder(reason: string): string;
|
|
35
|
+
export declare function buildFinalizeOpportunityReminder(reason: string): string;
|
package/dist/prompt/reminders.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
|
|
7
7
|
* the prompt cache every time something changes.
|
|
8
8
|
*/
|
|
9
9
|
export function wrapInSystemReminder(content) {
|
|
10
|
-
return
|
|
10
|
+
return content.trim();
|
|
11
11
|
}
|
|
12
12
|
export function isPermissionModeReminder(content) {
|
|
13
13
|
if (typeof content !== "string")
|
|
@@ -20,7 +20,7 @@ const PLAN_MODE_ENTER = `
|
|
|
20
20
|
Plan mode is now ACTIVE.
|
|
21
21
|
|
|
22
22
|
Rules while in plan mode:
|
|
23
|
-
- Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch,
|
|
23
|
+
- Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch, spawn_agent, wait_agent, send_input, close_agent, skill, todo_write, tool_search, question, and exit_plan_mode.
|
|
24
24
|
- Writes, edits, and shell commands WILL be rejected by the harness; do not try them.
|
|
25
25
|
- Do not edit files or claim implementation is complete while plan mode is active.
|
|
26
26
|
- Investigate the codebase, then use the question tool to clarify important ambiguities, tradeoffs, requirements, or preference choices that would materially change the plan.
|
|
@@ -55,7 +55,6 @@ export function reminderForMode(mode) {
|
|
|
55
55
|
return wrapInSystemReminder(DEFAULT_ENTER);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
// Backward-compat exports kept in case external code pinned the old names.
|
|
59
58
|
export const PLAN_MODE_ENTER_REMINDER = reminderForMode("plan");
|
|
60
59
|
export const PLAN_MODE_EXIT_REMINDER = reminderForMode("default");
|
|
61
60
|
/**
|
|
@@ -182,3 +181,23 @@ You have changed files in this turn. Run the narrowest meaningful verification c
|
|
|
182
181
|
If verification truly cannot be run, state the concrete blocker and the residual risk.
|
|
183
182
|
`);
|
|
184
183
|
}
|
|
184
|
+
export function buildVerificationFailureReminder(reason) {
|
|
185
|
+
return wrapInSystemReminder(`
|
|
186
|
+
Verification failed after file changes.
|
|
187
|
+
|
|
188
|
+
${reason}
|
|
189
|
+
|
|
190
|
+
Do not finalize as complete while this failure is unresolved. Make one focused fix and rerun the most relevant verification.
|
|
191
|
+
If you cannot fix it, explain the concrete blocker and the residual risk instead of claiming success.
|
|
192
|
+
`);
|
|
193
|
+
}
|
|
194
|
+
export function buildFinalizeOpportunityReminder(reason) {
|
|
195
|
+
return wrapInSystemReminder(`
|
|
196
|
+
Completion checkpoint.
|
|
197
|
+
|
|
198
|
+
${reason}
|
|
199
|
+
|
|
200
|
+
If this satisfies the user's request, provide the final answer now.
|
|
201
|
+
Continue using tools only if there is a concrete remaining requirement, failing check, or missing deliverable.
|
|
202
|
+
`);
|
|
203
|
+
}
|
package/dist/prompt/runtime.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface RuntimePromptOptions {
|
|
|
3
3
|
thinkingLevel?: ThinkingLevel;
|
|
4
4
|
/**
|
|
5
5
|
* Kept for API compatibility. Agent mode is no longer baked into the static
|
|
6
|
-
* system prompt — mode changes are signalled via
|
|
6
|
+
* system prompt — mode changes are signalled via hidden runtime reminders
|
|
7
7
|
* (see src/prompt/reminders.ts) so the base prompt stays stable for caching.
|
|
8
8
|
*/
|
|
9
9
|
mode?: PermissionMode;
|
package/dist/prompt/runtime.js
CHANGED
|
@@ -8,7 +8,7 @@ const defaultGuidelines = [
|
|
|
8
8
|
"Prefer structured search tools over bash for repository searches whenever possible",
|
|
9
9
|
"Do not repeat near-identical searches when they are not producing new evidence",
|
|
10
10
|
"When investigating configuration or security questions, stop once the relevant load path, storage path, and exposure path are identified",
|
|
11
|
-
"Use
|
|
11
|
+
"Use spawn_agent and wait_agent for bounded investigative subproblems instead of letting the main loop churn on repeated exploratory searches",
|
|
12
12
|
"After code edits, run the narrowest meaningful verification command or explain why verification is not possible",
|
|
13
13
|
"When finishing a coding task, report what changed, where it changed, verification results, and remaining risk",
|
|
14
14
|
"Be concise in your responses",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function stripProviderProtocolArtifacts(text: string): string;
|
|
2
|
+
export declare function isOnlyProviderProtocolArtifacts(text: string): boolean;
|
|
3
|
+
export interface ProviderProtocolArtifactFilter {
|
|
4
|
+
push(text: string): string;
|
|
5
|
+
flush(): string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createProviderProtocolArtifactFilter(): ProviderProtocolArtifactFilter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Models with non-OpenAI chat templates (GLM-4.5/4.6, DeepSeek, some Kimi builds)
|
|
2
|
+
// emit tool-call delimiters as inline assistant text instead of as structured
|
|
3
|
+
// tool_calls deltas. The shapes vary — `<|tool_call|>`, `<||DSML||tool_calls>`,
|
|
4
|
+
// `<||DSML||invoke name="x">`, closing `</||DSML||tool_calls>`, etc. — but
|
|
5
|
+
// they always share the pattern of a tag whose name is wrapped in `|` or `|`.
|
|
6
|
+
// If we let any of that text reach the consumer it pollutes the streamed
|
|
7
|
+
// assistant text and, downstream, the subagent's summary field.
|
|
8
|
+
const TOOL_PROTOCOL_PATTERNS = [
|
|
9
|
+
// Generic: opening or closing tag whose name is wrapped in `|` or `|`,
|
|
10
|
+
// optionally with attributes after the closing pipe (e.g. `invoke name="x"`).
|
|
11
|
+
/<\/?\s*[||]+[^<>]*?[||]+[^<>]*>/g,
|
|
12
|
+
// Plain ASCII variants without attributes.
|
|
13
|
+
/<\/?\|tool_calls?\|>/gi,
|
|
14
|
+
];
|
|
15
|
+
export function stripProviderProtocolArtifacts(text) {
|
|
16
|
+
let out = text;
|
|
17
|
+
for (const pattern of TOOL_PROTOCOL_PATTERNS) {
|
|
18
|
+
out = out.replace(pattern, "");
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
export function isOnlyProviderProtocolArtifacts(text) {
|
|
23
|
+
return !!text.trim() && stripProviderProtocolArtifacts(text).trim().length === 0;
|
|
24
|
+
}
|
|
25
|
+
export function createProviderProtocolArtifactFilter() {
|
|
26
|
+
let pending = "";
|
|
27
|
+
return {
|
|
28
|
+
push(text) {
|
|
29
|
+
pending = stripProviderProtocolArtifacts(pending + text);
|
|
30
|
+
const keep = trailingPossibleMarkerLength(pending);
|
|
31
|
+
const emit = pending.slice(0, pending.length - keep);
|
|
32
|
+
pending = pending.slice(pending.length - keep);
|
|
33
|
+
return emit;
|
|
34
|
+
},
|
|
35
|
+
flush() {
|
|
36
|
+
const out = stripProviderProtocolArtifacts(pending);
|
|
37
|
+
pending = "";
|
|
38
|
+
return out;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Hold back a trailing fragment if it could be the start of a pipe-wrapped tag
|
|
43
|
+
// whose closing `>` hasn't arrived yet. Without this guard, a stream that flushes
|
|
44
|
+
// mid-tag (`<` ... `|DSML|tool_calls`) would emit the partial tag as text, then
|
|
45
|
+
// emit the rest later — the stripping regex only matches complete tags.
|
|
46
|
+
function trailingPossibleMarkerLength(text) {
|
|
47
|
+
const lastLt = text.lastIndexOf("<");
|
|
48
|
+
if (lastLt === -1)
|
|
49
|
+
return 0;
|
|
50
|
+
const tail = text.slice(lastLt);
|
|
51
|
+
if (tail.includes(">"))
|
|
52
|
+
return 0;
|
|
53
|
+
// Hold back only when the trailing fragment looks like the start of a protocol
|
|
54
|
+
// tag. Anything else (e.g. `if (x < y)` in source) flushes immediately.
|
|
55
|
+
if (/^<\/?$/.test(tail))
|
|
56
|
+
return tail.length;
|
|
57
|
+
if (/^<\/?\s*[||]/.test(tail))
|
|
58
|
+
return tail.length;
|
|
59
|
+
return 0;
|
|
60
|
+
}
|