@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
@@ -1,13 +1,13 @@
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
7
7
  * the prompt cache every time something changes.
8
8
  */
9
9
  export function wrapInSystemReminder(content) {
10
- return `<system-reminder>\n${content.trim()}\n</system-reminder>`;
10
+ return content.trim();
11
11
  }
12
12
  export function isPermissionModeReminder(content) {
13
13
  if (typeof content !== "string")
@@ -20,8 +20,9 @@ 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, task, skill, todo_write, tool_search, question, and exit_plan_mode.
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
+ - Do not edit files or claim implementation is complete while plan mode is active.
25
26
  - Investigate the codebase, then use the question tool to clarify important ambiguities, tradeoffs, requirements, or preference choices that would materially change the plan.
26
27
  - Call exit_plan_mode with a concrete step-by-step plan after the important questions are resolved.
27
28
  - Do not use the question tool to ask whether the plan is approved; exit_plan_mode is the approval step.
@@ -34,11 +35,13 @@ Permission mode is now: bypassPermissions.
34
35
  ALL tool calls auto-approve with no user confirmation. The user has explicitly opted into this.
35
36
  Proceed with extra care — explain risky actions in the chat BEFORE performing them, and
36
37
  prefer reversible operations when possible.
38
+ Do not perform destructive operations, credential exposure, or unrelated reversions just because approvals are bypassed.
37
39
  `;
38
40
  const DEFAULT_ENTER = `
39
41
  Permission mode is now: default Build mode.
40
42
 
41
43
  File edits and writes auto-approve. Bash commands and other destructive tools still require explicit approval unless allowed by rules.
44
+ Execute the requested change end to end; do not stop at analysis unless blocked or the user explicitly asks for discussion only.
42
45
  `;
43
46
  /** Picks the correct reminder text for a transition TO a given mode. */
44
47
  export function reminderForMode(mode) {
@@ -52,7 +55,6 @@ export function reminderForMode(mode) {
52
55
  return wrapInSystemReminder(DEFAULT_ENTER);
53
56
  }
54
57
  }
55
- // Backward-compat exports kept in case external code pinned the old names.
56
58
  export const PLAN_MODE_ENTER_REMINDER = reminderForMode("plan");
57
59
  export const PLAN_MODE_EXIT_REMINDER = reminderForMode("default");
58
60
  /**
@@ -86,11 +88,11 @@ Stop once these categories are covered. Do not keep repeating near-identical sea
86
88
  }
87
89
  export function buildLoopWarningReminder(reason) {
88
90
  return wrapInSystemReminder(`
89
- Search loop warning.
91
+ Tool loop warning.
90
92
 
91
93
  ${reason}
92
94
 
93
- Do not repeat near-identical grep/bash searches unless you are changing the path or testing a genuinely new hypothesis.
95
+ Do not repeat near-identical reads or searches unless you are changing the path or testing a genuinely new hypothesis.
94
96
  If current evidence is sufficient, summarize your findings now.
95
97
  `);
96
98
  }
@@ -104,6 +106,19 @@ Do not continue blind keyword searching. Use the evidence already gathered to re
104
106
  You may still read specific files if you already know where the relevant configuration or persistence logic lives.
105
107
  `);
106
108
  }
109
+ export function buildExplorationFreezeReminder(reason) {
110
+ return wrapInSystemReminder(`
111
+ Implementation phase has advanced from exploration to modification.
112
+
113
+ Reason: ${reason}
114
+
115
+ You have enough context to act. Do not continue reading, searching, or delegating exploration.
116
+ Choose one of:
117
+ 1. Use edit/write to make the requested change.
118
+ 2. If no safe change can be made from the gathered context, explain the concrete blocker.
119
+ 3. If files were already changed, run the narrowest meaningful verification or finish with the result.
120
+ `);
121
+ }
107
122
  export function buildToolFreezeReminder(reason) {
108
123
  return wrapInSystemReminder(`
109
124
  CRITICAL - MAXIMUM STEPS REACHED
@@ -156,3 +171,33 @@ Treat the task output as a bounded subtask result:
156
171
  - do not re-run the same exploratory search unless the subtask uncovered a concrete contradiction
157
172
  `);
158
173
  }
174
+ export function buildVerificationReminder(reason) {
175
+ return wrapInSystemReminder(`
176
+ Verification required before final answer.
177
+
178
+ ${reason}
179
+
180
+ You have changed files in this turn. Run the narrowest meaningful verification command or runtime check before finalizing.
181
+ If verification truly cannot be run, state the concrete blocker and the residual risk.
182
+ `);
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
+ }
@@ -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 <system-reminder> injections
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;
@@ -1,18 +1,31 @@
1
1
  const defaultGuidelines = [
2
+ "Inspect relevant files, command output, or runtime state before making claims about code behavior",
3
+ "Separate confirmed facts from inference when the evidence is incomplete",
4
+ "Prefer runtime and call-chain evidence over README text or configuration names for behavior questions",
2
5
  "Before editing or writing files, read them first if they exist",
3
6
  "Use edit for targeted changes to existing files; use write for creating new files",
4
- "Be concise in your responses",
5
- "Show file paths clearly when working with files",
7
+ "Edit only the files required for the requested change",
6
8
  "Prefer structured search tools over bash for repository searches whenever possible",
7
9
  "Do not repeat near-identical searches when they are not producing new evidence",
8
10
  "When investigating configuration or security questions, stop once the relevant load path, storage path, and exposure path are identified",
9
- "Use the task tool for bounded investigative subproblems instead of letting the main loop churn on repeated exploratory searches",
11
+ "Use spawn_agent and wait_agent for bounded investigative subproblems instead of letting the main loop churn on repeated exploratory searches",
12
+ "After code edits, run the narrowest meaningful verification command or explain why verification is not possible",
13
+ "When finishing a coding task, report what changed, where it changed, verification results, and remaining risk",
14
+ "Be concise in your responses",
10
15
  ];
11
16
  export function buildRuntimePrompt(options = {}) {
12
17
  const thinkingLevel = options.thinkingLevel ?? "off";
13
18
  const guidelines = dedupe(defaultGuidelines, options.guidelines ?? []);
14
19
  return `Current thinking level: ${thinkingLevel}
15
20
 
21
+ Execution protocol:
22
+ 1. Understand the user's requested outcome and current constraints.
23
+ 2. Inspect the relevant files or state before making claims or edits.
24
+ 3. Choose the smallest coherent change that solves the actual problem.
25
+ 4. Edit only the necessary files.
26
+ 5. Verify with the narrowest meaningful command or runtime check when possible.
27
+ 6. Finish with changed files, verification results, and unresolved risk.
28
+
16
29
  Guidelines:
17
30
  ${guidelines.map((item) => `- ${item}`).join("\n")}`;
18
31
  }
@@ -0,0 +1,2 @@
1
+ import type { TaskType } from "../agent/task-classifier.js";
2
+ export declare function reminderForTaskType(taskType: TaskType): string | undefined;
@@ -0,0 +1,56 @@
1
+ import { wrapInSystemReminder } from "./reminders.js";
2
+ export function reminderForTaskType(taskType) {
3
+ switch (taskType) {
4
+ case "debugging":
5
+ return wrapInSystemReminder(`
6
+ Debugging workflow:
7
+ - Reproduce or identify the failing boundary before editing.
8
+ - Trace input, transformation, and output paths.
9
+ - Prefer fixing the mechanism over raising thresholds or adding superficial fallbacks.
10
+ - Verify the specific failure path after the change.
11
+ `);
12
+ case "implementation":
13
+ return wrapInSystemReminder(`
14
+ Implementation workflow:
15
+ - Do not stop at a proposal when the user asked for a change.
16
+ - Inspect the relevant files first, then make the smallest coherent edit.
17
+ - Keep unrelated files and behavior out of scope.
18
+ - Run a narrow verification command or explain why it cannot be run.
19
+ `);
20
+ case "code_review":
21
+ return wrapInSystemReminder(`
22
+ Code review workflow:
23
+ - Lead with concrete findings, ordered by severity.
24
+ - Reference file paths and line numbers when possible.
25
+ - Prioritize bugs, regressions, missing tests, security, and user-visible risk.
26
+ - Keep summaries secondary to findings.
27
+ `);
28
+ case "code_explanation":
29
+ return wrapInSystemReminder(`
30
+ Code explanation workflow:
31
+ - Answer the direct question first.
32
+ - Ground claims in concrete files, functions, and call paths.
33
+ - Distinguish current source evidence from inference.
34
+ - Avoid proposing changes unless the user asks for them.
35
+ `);
36
+ case "repo_orientation":
37
+ return wrapInSystemReminder(`
38
+ Repository orientation workflow:
39
+ - Start with the repo purpose and main execution paths.
40
+ - Inspect README/package metadata plus core runtime files before summarizing.
41
+ - Keep the first pass read-only unless the user asks for changes or runtime verification.
42
+ `);
43
+ case "product_discussion":
44
+ return wrapInSystemReminder(`
45
+ Product discussion workflow:
46
+ - Clarify the product goal, user workflow, and tradeoffs before suggesting implementation.
47
+ - Give direct product judgment when the user asks for direction.
48
+ - Avoid drifting into code changes unless the user explicitly asks to execute.
49
+ `);
50
+ case "security_investigation":
51
+ case "code_search":
52
+ case "general":
53
+ default:
54
+ return undefined;
55
+ }
56
+ }
@@ -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
+ }
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
5
5
  */
6
- import type { Message, Provider, StreamChunk, ThinkingLevel } from "./types.js";
6
+ import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
7
7
  type ReasoningContentEcho = "tool_calls" | "all";
8
- export declare function toChatCompletionsMessage(message: Message, options?: {
8
+ export declare function toChatCompletionsMessage(message: ProviderMessage, options?: {
9
9
  reasoningContentEcho?: ReasoningContentEcho;
10
10
  }): Record<string, unknown>;
11
11
  export interface ProviderInstanceOptions {
@@ -21,11 +21,10 @@ export declare function normalizeToolArgs(raw: string): string;
21
21
  /**
22
22
  * Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
23
23
  *
24
- * Multi-tool-call streams are buffered by `index` and emitted in index order at
25
- * `finish_reason === "tool_calls"`, so the agent layer always sees a clean
26
- * (isStart -> args -> isEnd) sequence per call. This matters for providers like
27
- * Kimi K2.5 that emit several parallel tool calls per assistant turn -- the
28
- * previous single-slot implementation silently dropped every call but the last.
24
+ * Multi-tool-call streams are tracked by `index`, but tool-call starts and
25
+ * argument deltas are emitted as soon as they arrive so the TUI can render
26
+ * partial write previews before the tool executes. End events are still flushed
27
+ * in index order to keep multi-call turns deterministic.
29
28
  */
30
29
  export declare function translateOpenAIStream(stream: AsyncIterable<any>): AsyncIterable<StreamChunk>;
31
30
  export {};
package/dist/provider.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import OpenAI from "openai";
7
7
  import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
8
+ import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
8
9
  import { resolveProviderRequestConfig } from "./provider-transform.js";
9
10
  export function toChatCompletionsMessage(message, options = {}) {
10
11
  const reasoningContentEcho = options.reasoningContentEcho ?? "tool_calls";
@@ -189,14 +190,14 @@ function extractBalancedJson(s, start) {
189
190
  /**
190
191
  * Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
191
192
  *
192
- * Multi-tool-call streams are buffered by `index` and emitted in index order at
193
- * `finish_reason === "tool_calls"`, so the agent layer always sees a clean
194
- * (isStart -> args -> isEnd) sequence per call. This matters for providers like
195
- * Kimi K2.5 that emit several parallel tool calls per assistant turn -- the
196
- * previous single-slot implementation silently dropped every call but the last.
193
+ * Multi-tool-call streams are tracked by `index`, but tool-call starts and
194
+ * argument deltas are emitted as soon as they arrive so the TUI can render
195
+ * partial write previews before the tool executes. End events are still flushed
196
+ * in index order to keep multi-call turns deterministic.
197
197
  */
198
198
  export async function* translateOpenAIStream(stream) {
199
199
  const toolCalls = new Map();
200
+ const textFilter = createProviderProtocolArtifactFilter();
200
201
  function* flushToolCalls() {
201
202
  if (toolCalls.size === 0)
202
203
  return;
@@ -204,13 +205,34 @@ export async function* translateOpenAIStream(stream) {
204
205
  for (const [, entry] of sorted) {
205
206
  if (!entry.id || !entry.name)
206
207
  continue;
207
- const args = normalizeToolArgs(entry.args);
208
- yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
209
- yield { type: "tool_call", id: entry.id, name: entry.name, arguments: args, isStart: false, isEnd: false };
210
- yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: false, isEnd: true };
208
+ if (!entry.started) {
209
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
210
+ entry.started = true;
211
+ if (entry.args) {
212
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: entry.args, isStart: false, isEnd: false };
213
+ }
214
+ }
215
+ yield {
216
+ type: "tool_call",
217
+ id: entry.id,
218
+ name: entry.name,
219
+ arguments: "",
220
+ argumentsFull: normalizeToolArgs(entry.args),
221
+ isStart: false,
222
+ isEnd: true,
223
+ };
211
224
  }
212
225
  toolCalls.clear();
213
226
  }
227
+ function* startToolCallIfReady(entry) {
228
+ if (entry.started || !entry.id || !entry.name)
229
+ return;
230
+ entry.started = true;
231
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
232
+ if (entry.args) {
233
+ yield { type: "tool_call", id: entry.id, name: entry.name, arguments: entry.args, isStart: false, isEnd: false };
234
+ }
235
+ }
214
236
  for await (const chunk of stream) {
215
237
  const delta = chunk.choices?.[0]?.delta;
216
238
  const usage = chunk.usage;
@@ -240,12 +262,16 @@ export async function* translateOpenAIStream(stream) {
240
262
  yield { type: "reasoning_delta", content: thinkMatch[1] };
241
263
  }
242
264
  const remaining = delta.content.replace(/<think>[\s\S]*?<\/think>/, "");
243
- if (remaining) {
244
- yield { type: "text", content: remaining };
265
+ const cleaned = textFilter.push(remaining);
266
+ if (cleaned) {
267
+ yield { type: "text", content: cleaned };
245
268
  }
246
269
  }
247
270
  else {
248
- yield { type: "text", content: delta.content };
271
+ const cleaned = textFilter.push(delta.content);
272
+ if (cleaned) {
273
+ yield { type: "text", content: cleaned };
274
+ }
249
275
  }
250
276
  }
251
277
  if (delta?.tool_calls) {
@@ -253,15 +279,28 @@ export async function* translateOpenAIStream(stream) {
253
279
  const idx = typeof tc.index === "number" ? tc.index : 0;
254
280
  let entry = toolCalls.get(idx);
255
281
  if (!entry) {
256
- entry = { id: "", name: "", args: "" };
282
+ entry = { id: "", name: "", args: "", started: false };
257
283
  toolCalls.set(idx, entry);
258
284
  }
259
285
  if (tc.id)
260
286
  entry.id = tc.id;
261
287
  if (tc.function?.name)
262
288
  entry.name = tc.function.name;
263
- if (typeof tc.function?.arguments === "string")
264
- entry.args += tc.function.arguments;
289
+ yield* startToolCallIfReady(entry);
290
+ if (typeof tc.function?.arguments === "string" && tc.function.arguments) {
291
+ const merged = mergeToolArgumentDelta(entry.args, tc.function.arguments);
292
+ entry.args = merged.args;
293
+ if (entry.started && merged.delta) {
294
+ yield {
295
+ type: "tool_call",
296
+ id: entry.id,
297
+ name: entry.name,
298
+ arguments: merged.delta,
299
+ isStart: false,
300
+ isEnd: false,
301
+ };
302
+ }
303
+ }
265
304
  }
266
305
  }
267
306
  const finishReason = chunk.choices?.[0]?.finish_reason;
@@ -269,5 +308,28 @@ export async function* translateOpenAIStream(stream) {
269
308
  yield* flushToolCalls();
270
309
  }
271
310
  }
311
+ const remainingText = textFilter.flush();
312
+ if (remainingText) {
313
+ yield { type: "text", content: remainingText };
314
+ }
272
315
  yield* flushToolCalls();
273
316
  }
317
+ function mergeToolArgumentDelta(current, incoming) {
318
+ if (!current)
319
+ return { args: incoming, delta: incoming };
320
+ if (!incoming)
321
+ return { args: current, delta: "" };
322
+ // Standard OpenAI-compatible streams send incremental argument deltas. Some
323
+ // providers send cumulative snapshots instead. If the incoming chunk already
324
+ // contains what we have, emit only the new suffix so downstream state remains
325
+ // append-only.
326
+ if (incoming.startsWith(current)) {
327
+ return { args: incoming, delta: incoming.slice(current.length) };
328
+ }
329
+ // Repeated identical snapshots should not duplicate the TUI preview or final
330
+ // JSON arguments.
331
+ if (incoming === current || current.endsWith(incoming)) {
332
+ return { args: current, delta: "" };
333
+ }
334
+ return { args: current + incoming, delta: incoming };
335
+ }
@@ -193,6 +193,8 @@ function normalizeMessageToEntries(message, id, timestamp) {
193
193
  }
194
194
  case "tool":
195
195
  return [{ id, type: "tool_result", message, timestamp }];
196
+ case "meta":
197
+ return [];
196
198
  case "system":
197
199
  return [{
198
200
  id,
@@ -241,7 +243,7 @@ function pruneIncompleteTail(messages) {
241
243
  let sawNonUserInCurrentTurn = false;
242
244
  for (let i = 0; i < messages.length; i++) {
243
245
  const message = messages[i];
244
- if (message.role === "system")
246
+ if (message.role === "system" || message.role === "meta")
245
247
  continue;
246
248
  if (message.role === "user") {
247
249
  currentTurnStart = i;
@@ -157,7 +157,7 @@ async function handleMemoryCommand(args, ctx) {
157
157
  return lines.join("\n");
158
158
  }
159
159
  if (sub === "add") {
160
- return "Manual memory writes are disabled. Bubble now follows the Codex-style automatic startup memory pipeline.";
160
+ return "Manual memory writes are disabled. Bubble now follows the automatic startup memory pipeline.";
161
161
  }
162
162
  if (sub === "search") {
163
163
  const query = rest.join(" ").trim();
@@ -265,7 +265,7 @@ const builtinSlashCommandEntries = [
265
265
  },
266
266
  {
267
267
  name: "memory",
268
- description: "Inspect and maintain Codex-style automatic persistent memory. Usage: /memory [status|search|compact|summarize|refresh|reset]",
268
+ description: "Inspect and maintain Bubble's automatic persistent memory. Usage: /memory [status|search|compact|summarize|refresh|reset]",
269
269
  async handler(args, ctx) {
270
270
  return handleMemoryCommand(args, ctx);
271
271
  },
@@ -737,7 +737,6 @@ const builtinSlashCommandEntries = [
737
737
  return "Session is already compact enough.";
738
738
  }
739
739
  const systemMessage = ctx.agent.messages.find((message) => message.role === "system");
740
- ctx.clearMessages();
741
740
  ctx.agent.messages = [
742
741
  ...(systemMessage ? [systemMessage] : []),
743
742
  ...ctx.sessionManager.getMessages(),
@@ -30,5 +30,7 @@ export interface SystemPromptOptions {
30
30
  skills?: SkillSummary[];
31
31
  /** Prompt-visible memory guidance and summaries */
32
32
  memoryPrompt?: string;
33
+ /** Durable child-agent profile prompt used for subagents. */
34
+ agentProfilePrompt?: string;
33
35
  }
34
36
  export declare function buildSystemPrompt(options?: SystemPromptOptions): string;
@@ -0,0 +1,6 @@
1
+ import type { ToolRegistryEntry } from "../types.js";
2
+ export declare function createSpawnAgentTool(): ToolRegistryEntry;
3
+ export declare function createWaitAgentTool(): ToolRegistryEntry;
4
+ export declare function createSendInputTool(): ToolRegistryEntry;
5
+ export declare function createCloseAgentTool(): ToolRegistryEntry;
6
+ export declare function createAgentLifecycleTools(): ToolRegistryEntry[];