@bubblebrain-ai/bubble 0.0.3 → 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.
Files changed (97) hide show
  1. package/README.md +8 -3
  2. package/dist/agent/budget-ledger.d.ts +20 -0
  3. package/dist/agent/budget-ledger.js +51 -0
  4. package/dist/agent/execution-governor.d.ts +14 -0
  5. package/dist/agent/execution-governor.js +172 -14
  6. package/dist/agent/profiles.d.ts +59 -0
  7. package/dist/agent/profiles.js +460 -0
  8. package/dist/agent/subagent-control.d.ts +52 -0
  9. package/dist/agent/subagent-control.js +38 -0
  10. package/dist/agent/task-classifier.d.ts +1 -1
  11. package/dist/agent/task-classifier.js +60 -0
  12. package/dist/agent/tool-intent.d.ts +14 -0
  13. package/dist/agent/tool-intent.js +125 -1
  14. package/dist/agent.d.ts +60 -1
  15. package/dist/agent.js +606 -53
  16. package/dist/bin.d.ts +2 -0
  17. package/dist/bin.js +45 -0
  18. package/dist/context/budget.js +1 -0
  19. package/dist/context/compact-llm.js +7 -6
  20. package/dist/context/compact.js +6 -6
  21. package/dist/context/projector.d.ts +3 -3
  22. package/dist/context/projector.js +32 -18
  23. package/dist/context/prune.d.ts +2 -2
  24. package/dist/context/prune.js +1 -4
  25. package/dist/main.d.ts +1 -1
  26. package/dist/main.js +13 -6
  27. package/dist/mcp/manager.js +1 -0
  28. package/dist/orchestrator/default-hooks.js +92 -1
  29. package/dist/orchestrator/hooks.d.ts +10 -0
  30. package/dist/prompt/compose.d.ts +1 -0
  31. package/dist/prompt/compose.js +20 -1
  32. package/dist/prompt/environment.js +21 -2
  33. package/dist/prompt/provider-prompts/deepseek.d.ts +1 -0
  34. package/dist/prompt/provider-prompts/deepseek.js +8 -0
  35. package/dist/prompt/provider-prompts/glm.d.ts +1 -0
  36. package/dist/prompt/provider-prompts/glm.js +7 -0
  37. package/dist/prompt/provider-prompts/kimi.d.ts +1 -0
  38. package/dist/prompt/provider-prompts/kimi.js +7 -0
  39. package/dist/prompt/reminders.d.ts +5 -1
  40. package/dist/prompt/reminders.js +51 -6
  41. package/dist/prompt/runtime.d.ts +1 -1
  42. package/dist/prompt/runtime.js +16 -3
  43. package/dist/prompt/task-reminders.d.ts +2 -0
  44. package/dist/prompt/task-reminders.js +56 -0
  45. package/dist/provider-artifacts.d.ts +7 -0
  46. package/dist/provider-artifacts.js +60 -0
  47. package/dist/provider.d.ts +6 -7
  48. package/dist/provider.js +77 -15
  49. package/dist/session-log.js +3 -1
  50. package/dist/slash-commands/commands.js +2 -3
  51. package/dist/system-prompt.d.ts +2 -0
  52. package/dist/tools/agent-lifecycle.d.ts +6 -0
  53. package/dist/tools/agent-lifecycle.js +355 -0
  54. package/dist/tools/bash.js +12 -7
  55. package/dist/tools/edit-apply.d.ts +25 -0
  56. package/dist/tools/edit-apply.js +197 -0
  57. package/dist/tools/edit.js +64 -52
  58. package/dist/tools/exit-plan-mode.js +3 -1
  59. package/dist/tools/file-mutation-queue.d.ts +1 -0
  60. package/dist/tools/file-mutation-queue.js +32 -0
  61. package/dist/tools/glob.js +1 -0
  62. package/dist/tools/grep.js +1 -0
  63. package/dist/tools/index.d.ts +1 -1
  64. package/dist/tools/index.js +3 -3
  65. package/dist/tools/lsp.js +2 -0
  66. package/dist/tools/memory.js +2 -0
  67. package/dist/tools/question.js +2 -0
  68. package/dist/tools/read.js +1 -0
  69. package/dist/tools/skill.js +1 -0
  70. package/dist/tools/task.js +1 -0
  71. package/dist/tools/todo.js +1 -0
  72. package/dist/tools/tool-search.js +2 -1
  73. package/dist/tools/web-fetch.js +1 -0
  74. package/dist/tools/web-search.js +1 -0
  75. package/dist/tools/write.js +10 -1
  76. package/dist/tui/display-history.d.ts +8 -1
  77. package/dist/tui/image-paste.d.ts +41 -0
  78. package/dist/tui/image-paste.js +217 -0
  79. package/dist/tui/markdown-inline.d.ts +22 -0
  80. package/dist/tui/markdown-inline.js +68 -0
  81. package/dist/tui/render-signature.d.ts +1 -0
  82. package/dist/tui/render-signature.js +7 -0
  83. package/dist/tui/run.js +814 -269
  84. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  85. package/dist/tui/tool-renderers/fallback.js +75 -0
  86. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  87. package/dist/tui/tool-renderers/registry.js +11 -0
  88. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  89. package/dist/tui/tool-renderers/subagent.js +114 -0
  90. package/dist/tui/tool-renderers/types.d.ts +36 -0
  91. package/dist/tui/tool-renderers/types.js +1 -0
  92. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  93. package/dist/tui/tool-renderers/write-preview.js +22 -0
  94. package/dist/tui/tool-renderers/write.d.ts +6 -0
  95. package/dist/tui/tool-renderers/write.js +82 -0
  96. package/dist/types.d.ts +90 -10
  97. package/package.json +3 -3
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ const mainPath = fileURLToPath(new URL("./main.js", import.meta.url));
5
+ if (process.versions.bun) {
6
+ await import("./main.js");
7
+ }
8
+ else {
9
+ const bunCheck = spawnSync("bun", ["--version"], { stdio: "ignore" });
10
+ if (bunCheck.error) {
11
+ console.error([
12
+ "Bubble requires Bun to run.",
13
+ "",
14
+ "Install Bun, then run bubble again:",
15
+ " curl -fsSL https://bun.sh/install | bash",
16
+ "",
17
+ "After installation, restart your terminal if the bun command is not found.",
18
+ ].join("\n"));
19
+ process.exit(1);
20
+ }
21
+ const child = spawn("bun", [mainPath, ...process.argv.slice(2)], {
22
+ stdio: "inherit",
23
+ env: process.env,
24
+ });
25
+ child.on("error", (error) => {
26
+ console.error(`Failed to start Bubble with Bun: ${error.message}`);
27
+ process.exit(1);
28
+ });
29
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
30
+ process.on(signal, () => {
31
+ if (!child.killed) {
32
+ child.kill(signal);
33
+ }
34
+ });
35
+ }
36
+ const result = await new Promise((resolve) => {
37
+ child.on("exit", (code, signal) => resolve({ code, signal }));
38
+ });
39
+ if (result.signal) {
40
+ process.kill(process.pid, result.signal);
41
+ }
42
+ else {
43
+ process.exit(result.code ?? 1);
44
+ }
45
+ }
@@ -6,6 +6,7 @@ export const MIN_WINDOW_FOR_RESERVE = 40_000;
6
6
  export function estimateMessageTokens(message) {
7
7
  switch (message.role) {
8
8
  case "system":
9
+ case "meta":
9
10
  case "tool":
10
11
  return estimateTextTokens(message.content);
11
12
  case "assistant":
@@ -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 systemMessages = messages.filter((m) => m.role === "system");
48
- const nonSystemMessages = messages.filter((m) => m.role !== "system");
49
- const turnStartIndexes = nonSystemMessages
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 = nonSystemMessages.slice(0, keepStartIndex);
60
- const keptMessages = nonSystemMessages.slice(keepStartIndex);
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
- ...systemMessages,
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
  }
@@ -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 systemMessages = messages.filter((message) => message.role === "system");
47
- const nonSystemMessages = messages.filter((message) => message.role !== "system");
48
- const turnStartIndexes = nonSystemMessages
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 = nonSystemMessages.slice(0, keepStartIndex);
59
- const keptMessages = nonSystemMessages.slice(keepStartIndex);
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
- ...systemMessages.map((message) => cloneMessage(message)),
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): Message[];
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: Message[]): Message[];
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 projected = [];
7
- let systemBuffer = [];
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
- systemBuffer.push(message.content);
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
- projected.push(cloneMessage(message));
22
+ projectedBody.push(cloneMessage(message));
27
23
  }
28
- flushSystemBuffer();
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 afterFirstPass = getContextBudget(options.providerId, options.modelId, compacted.messages);
55
+ const compactedMessages = compacted.messages;
56
+ const afterFirstPass = getContextBudget(options.providerId, options.modelId, compactedMessages);
52
57
  if (!afterFirstPass.shouldCompact) {
53
- return repairToolCallChains(compacted.messages);
58
+ return repairToolCallChains(compactedMessages);
54
59
  }
55
60
  const tighter = compactMessages(pruned, { keepRecentTurns: 1 });
56
- const finalMessages = tighter.compacted && tighter.messages ? tighter.messages : compacted.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 {
@@ -1,9 +1,9 @@
1
1
  import type { Message } from "../types.js";
2
- export declare function pruneMessages(messages: Message[]): Message[];
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: Message[]): Message[];
9
+ export declare function aggressivePruneMessages<T extends Message>(messages: T[]): T[];
@@ -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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  /**
3
3
  * Main entry point - assembles all layers and runs the agent.
4
4
  */
package/dist/main.js CHANGED
@@ -1,9 +1,10 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  /**
3
3
  * Main entry point - assembles all layers and runs the agent.
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: skillRegistry.summaries(),
198
- memoryPrompt: buildMemoryPrompt(args.cwd),
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
- // <system-reminder> injections are runtime/ephemeral; don't persist them —
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 === "user" && message.isMeta)
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 <system-reminder> we injected during
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();
@@ -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,8 @@ 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, buildWorkflowPhaseReminder } from "../prompt/reminders.js";
5
+ import { buildFinalizeOpportunityReminder, buildTaskSummaryReminder, buildVerificationFailureReminder, buildVerificationReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
6
+ import { reminderForTaskType } from "../prompt/task-reminders.js";
6
7
  import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
7
8
  export function createDefaultHooks() {
8
9
  return [
@@ -11,6 +12,10 @@ export function createDefaultHooks() {
11
12
  const taskType = classifyTask(ctx.input);
12
13
  ctx.state.taskType = taskType;
13
14
  ctx.state.governor = new ExecutionGovernor(taskType);
15
+ const taskReminder = reminderForTaskType(taskType);
16
+ if (taskReminder) {
17
+ ctx.queueReminder(taskReminder);
18
+ }
14
19
  if (taskType === "security_investigation") {
15
20
  ctx.state.evidenceTracker = new EvidenceTracker();
16
21
  ctx.state.workflowPhase = "investigate";
@@ -70,6 +75,21 @@ export function createDefaultHooks() {
70
75
  }
71
76
  ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
72
77
  ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
78
+ if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
79
+ markCodeChanged(ctx.state);
80
+ }
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
+ }
92
+ }
73
93
  if (ctx.toolCall.name === "task") {
74
94
  ctx.queueReminder(buildTaskSummaryReminder());
75
95
  }
@@ -90,7 +110,78 @@ export function createDefaultHooks() {
90
110
  if (ctx.state.governor?.snapshot().searchFrozen && allSearchResultsWereLowSignal) {
91
111
  ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
92
112
  }
113
+ const changedThisTurn = ctx.toolResults.some((result) => result.metadata?.kind === "write" || result.metadata?.kind === "edit");
114
+ if (changedThisTurn && !ctx.state.verificationAttempted && !ctx.state.verificationCompleted && !ctx.state.verificationReminderQueued) {
115
+ ctx.state.verificationReminderQueued = true;
116
+ ctx.queueReminder(buildVerificationReminder("The previous turn changed files and no verification evidence has been observed yet."));
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
+ }
126
+ },
127
+ afterTurn(ctx) {
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) {
135
+ ctx.state.finalVerificationReminderSent = true;
136
+ ctx.state.forceContinuationReason = "Files were changed but no verification evidence was observed before the final answer.";
137
+ ctx.queueReminder(buildVerificationReminder(ctx.state.forceContinuationReason));
138
+ }
93
139
  },
94
140
  },
95
141
  ];
96
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
+ }
154
+ function isCodeWriteResult(_toolCall, result) {
155
+ if (result.isError || result.status === "blocked" || result.status === "command_error") {
156
+ return false;
157
+ }
158
+ return result.metadata?.kind === "write" || result.metadata?.kind === "edit";
159
+ }
160
+ function isSuccessfulToolResult(result) {
161
+ if (result.isError) {
162
+ return false;
163
+ }
164
+ return result.status !== "blocked" && result.status !== "command_error" && result.status !== "timeout";
165
+ }
166
+ function isVerificationAttempt(toolCall, result) {
167
+ if (toolCall.name === "lsp") {
168
+ return true;
169
+ }
170
+ if (toolCall.name !== "bash") {
171
+ return false;
172
+ }
173
+ const command = typeof result.metadata?.command === "string"
174
+ ? result.metadata.command
175
+ : typeof toolCall.parsedArgs.command === "string"
176
+ ? toolCall.parsedArgs.command
177
+ : "";
178
+ return isVerificationCommand(command);
179
+ }
180
+ function isVerificationCommand(command) {
181
+ const normalized = command.trim().toLowerCase();
182
+ return /\b(npm|pnpm|yarn|bun)\s+(test|run\s+(test|build|typecheck|lint|check|tsc)|exec\s+tsc)\b/.test(normalized)
183
+ || /\b(npx|pnpm\s+exec|bunx)\s+(vitest|tsc|eslint|playwright)\b/.test(normalized)
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);
187
+ }
@@ -12,6 +12,16 @@ export interface TurnHookState {
12
12
  workflowKey?: string;
13
13
  turnCount?: number;
14
14
  forceTextOnlyReason?: string;
15
+ forceContinuationReason?: string;
16
+ codeChanged?: boolean;
17
+ verificationAttempted?: boolean;
18
+ verificationCompleted?: boolean;
19
+ verificationFailed?: boolean;
20
+ verificationReminderQueued?: boolean;
21
+ finalVerificationReminderSent?: boolean;
22
+ verificationFailureReminderQueued?: boolean;
23
+ verificationFailureReminderSent?: boolean;
24
+ finalizeReminderQueued?: boolean;
15
25
  taskBudget?: {
16
26
  total: number;
17
27
  spent: number;
@@ -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;
@@ -1,8 +1,11 @@
1
1
  import { buildAnthropicProviderPrompt } from "./provider-prompts/anthropic.js";
2
2
  import { buildCodexProviderPrompt } from "./provider-prompts/codex.js";
3
3
  import { buildDefaultProviderPrompt } from "./provider-prompts/default.js";
4
+ import { buildDeepSeekProviderPrompt } from "./provider-prompts/deepseek.js";
4
5
  import { buildGeminiProviderPrompt } from "./provider-prompts/gemini.js";
6
+ import { buildGlmProviderPrompt } from "./provider-prompts/glm.js";
5
7
  import { buildGptProviderPrompt } from "./provider-prompts/gpt.js";
8
+ import { buildKimiProviderPrompt } from "./provider-prompts/kimi.js";
6
9
  import { buildEnvironmentPrompt, defaultToolNames } from "./environment.js";
7
10
  import { buildRuntimePrompt } from "./runtime.js";
8
11
  import { buildSkillsPrompt } from "./skills.js";
@@ -24,7 +27,14 @@ export function composeSystemPrompt(options = {}) {
24
27
  guidelines: buildGuidelines(options.tools ?? defaultToolNames, options.guidelines ?? []),
25
28
  });
26
29
  const skillsPrompt = buildSkillsPrompt(options.skills ?? []);
27
- return [providerPrompt, environmentPrompt, runtimePrompt, options.memoryPrompt, skillsPrompt].filter(Boolean).join("\n\n");
30
+ return [
31
+ providerPrompt,
32
+ environmentPrompt,
33
+ runtimePrompt,
34
+ options.agentProfilePrompt,
35
+ options.memoryPrompt,
36
+ skillsPrompt,
37
+ ].filter(Boolean).join("\n\n");
28
38
  }
29
39
  function buildProviderPrompt(agentName, providerId, modelId, modelName) {
30
40
  const provider = providerId ?? "";
@@ -39,6 +49,15 @@ function buildProviderPrompt(agentName, providerId, modelId, modelName) {
39
49
  if (provider === "openai-codex" || model.includes("codex") || model.startsWith("gpt-5")) {
40
50
  return buildCodexProviderPrompt(agentName);
41
51
  }
52
+ if (provider === "deepseek" || model.startsWith("deepseek")) {
53
+ return buildDeepSeekProviderPrompt(agentName);
54
+ }
55
+ if (["moonshot-cn", "moonshot-intl", "kimi-for-coding"].includes(provider) || model.startsWith("kimi") || model.startsWith("k2.")) {
56
+ return buildKimiProviderPrompt(agentName);
57
+ }
58
+ if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(provider) || model.startsWith("glm")) {
59
+ return buildGlmProviderPrompt(agentName);
60
+ }
42
61
  if (provider === "openai" || provider === "openrouter" || model.startsWith("gpt") || model.startsWith("o1")) {
43
62
  return buildGptProviderPrompt(agentName);
44
63
  }
@@ -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
- task: "Delegate a bounded investigative subtask to a read-only sub-agent",
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 = ["read", "glob", "bash", "edit", "write", "grep", "lsp", "web_search", "web_fetch", "task", "question", "skill"];
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";
@@ -0,0 +1 @@
1
+ export declare function buildDeepSeekProviderPrompt(agentName: string): string;
@@ -0,0 +1,8 @@
1
+ export function buildDeepSeekProviderPrompt(agentName) {
2
+ return `You are ${agentName}, a direct coding agent running on a DeepSeek model.
3
+
4
+ Prefer short plans followed by concrete tool use. Avoid broad speculation.
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.`;
8
+ }
@@ -0,0 +1 @@
1
+ export declare function buildGlmProviderPrompt(agentName: string): string;
@@ -0,0 +1,7 @@
1
+ export function buildGlmProviderPrompt(agentName) {
2
+ return `You are ${agentName}, a pragmatic coding agent running on a GLM/Z.AI model.
3
+
4
+ Be specific and evidence-driven. Prefer source inspection and verification over generic recommendations.
5
+ When debugging, identify the failing boundary before editing.
6
+ When implementing, finish the requested change end to end and verify the relevant path.`;
7
+ }
@@ -0,0 +1 @@
1
+ export declare function buildKimiProviderPrompt(agentName: string): string;
@@ -0,0 +1,7 @@
1
+ export function buildKimiProviderPrompt(agentName) {
2
+ return `You are ${agentName}, a terminal coding agent running on a Kimi/Moonshot model.
3
+
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.`;
7
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * System reminders - short, runtime-variable instructions injected into the
3
- * message stream as <system-reminder>-wrapped user messages with isMeta=true.
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
@@ -22,6 +22,7 @@ export declare function buildDeferredToolsReminder(names: string[]): string;
22
22
  export declare function buildInvestigationReminder(): string;
23
23
  export declare function buildLoopWarningReminder(reason: string): string;
24
24
  export declare function buildSearchFreezeReminder(reason: string): string;
25
+ export declare function buildExplorationFreezeReminder(reason: string): string;
25
26
  export declare function buildToolFreezeReminder(reason: string): string;
26
27
  export declare function buildWorkflowPhaseReminder(input: {
27
28
  phase: "investigate" | "correlate" | "conclude";
@@ -29,3 +30,6 @@ export declare function buildWorkflowPhaseReminder(input: {
29
30
  pending: string[];
30
31
  }): string;
31
32
  export declare function buildTaskSummaryReminder(): string;
33
+ export declare function buildVerificationReminder(reason: string): string;
34
+ export declare function buildVerificationFailureReminder(reason: string): string;
35
+ export declare function buildFinalizeOpportunityReminder(reason: string): string;