@bubblebrain-ai/bubble 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/agent/budget-ledger.d.ts +20 -0
  2. package/dist/agent/budget-ledger.js +51 -0
  3. package/dist/agent/execution-governor.js +1 -1
  4. package/dist/agent/profiles.d.ts +59 -0
  5. package/dist/agent/profiles.js +460 -0
  6. package/dist/agent/subagent-control.d.ts +52 -0
  7. package/dist/agent/subagent-control.js +38 -0
  8. package/dist/agent/task-size.d.ts +9 -0
  9. package/dist/agent/task-size.js +33 -0
  10. package/dist/agent/tool-intent.d.ts +1 -0
  11. package/dist/agent/tool-intent.js +1 -1
  12. package/dist/agent.d.ts +60 -1
  13. package/dist/agent.js +648 -55
  14. package/dist/context/budget.js +1 -0
  15. package/dist/context/compact-llm.js +7 -6
  16. package/dist/context/compact.js +6 -6
  17. package/dist/context/projector.d.ts +3 -3
  18. package/dist/context/projector.js +32 -18
  19. package/dist/context/prune.d.ts +2 -2
  20. package/dist/context/prune.js +1 -4
  21. package/dist/main.js +12 -5
  22. package/dist/mcp/manager.js +1 -0
  23. package/dist/orchestrator/default-hooks.js +85 -35
  24. package/dist/orchestrator/hooks.d.ts +5 -3
  25. package/dist/prompt/compose.d.ts +1 -0
  26. package/dist/prompt/compose.js +11 -1
  27. package/dist/prompt/environment.js +23 -2
  28. package/dist/prompt/provider-prompts/deepseek.js +1 -2
  29. package/dist/prompt/provider-prompts/kimi.js +1 -2
  30. package/dist/prompt/reminders.d.ts +21 -2
  31. package/dist/prompt/reminders.js +53 -8
  32. package/dist/prompt/runtime.d.ts +1 -1
  33. package/dist/prompt/runtime.js +17 -23
  34. package/dist/provider-artifacts.d.ts +7 -0
  35. package/dist/provider-artifacts.js +60 -0
  36. package/dist/provider.d.ts +16 -8
  37. package/dist/provider.js +149 -34
  38. package/dist/session-log.js +3 -1
  39. package/dist/system-prompt.d.ts +2 -0
  40. package/dist/tools/agent-lifecycle.d.ts +6 -0
  41. package/dist/tools/agent-lifecycle.js +355 -0
  42. package/dist/tools/bash.d.ts +2 -1
  43. package/dist/tools/bash.js +3 -1
  44. package/dist/tools/edit-apply.d.ts +25 -0
  45. package/dist/tools/edit-apply.js +228 -0
  46. package/dist/tools/edit.d.ts +2 -1
  47. package/dist/tools/edit.js +75 -56
  48. package/dist/tools/exit-plan-mode.js +3 -1
  49. package/dist/tools/file-mutation-queue.d.ts +1 -0
  50. package/dist/tools/file-mutation-queue.js +32 -0
  51. package/dist/tools/file-state.d.ts +25 -0
  52. package/dist/tools/file-state.js +52 -0
  53. package/dist/tools/glob.js +1 -0
  54. package/dist/tools/grep.js +1 -0
  55. package/dist/tools/index.d.ts +3 -1
  56. package/dist/tools/index.js +9 -7
  57. package/dist/tools/lsp.js +2 -0
  58. package/dist/tools/memory.js +2 -0
  59. package/dist/tools/question.js +2 -0
  60. package/dist/tools/read.d.ts +2 -1
  61. package/dist/tools/read.js +6 -1
  62. package/dist/tools/skill.js +1 -0
  63. package/dist/tools/task.js +1 -0
  64. package/dist/tools/todo.js +1 -0
  65. package/dist/tools/tool-search.js +2 -1
  66. package/dist/tools/web-fetch.js +1 -0
  67. package/dist/tools/web-search.js +1 -0
  68. package/dist/tools/write.d.ts +4 -3
  69. package/dist/tools/write.js +135 -54
  70. package/dist/tui/display-history.d.ts +10 -1
  71. package/dist/tui/markdown-inline.d.ts +22 -0
  72. package/dist/tui/markdown-inline.js +68 -0
  73. package/dist/tui/render-signature.d.ts +1 -0
  74. package/dist/tui/render-signature.js +7 -0
  75. package/dist/tui/run.js +811 -274
  76. package/dist/tui/streaming-tool-args.d.ts +15 -0
  77. package/dist/tui/streaming-tool-args.js +30 -0
  78. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  79. package/dist/tui/tool-renderers/fallback.js +75 -0
  80. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  81. package/dist/tui/tool-renderers/registry.js +11 -0
  82. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  83. package/dist/tui/tool-renderers/subagent.js +114 -0
  84. package/dist/tui/tool-renderers/types.d.ts +36 -0
  85. package/dist/tui/tool-renderers/types.js +1 -0
  86. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  87. package/dist/tui/tool-renderers/write-preview.js +30 -0
  88. package/dist/tui/tool-renderers/write.d.ts +6 -0
  89. package/dist/tui/tool-renderers/write.js +88 -0
  90. package/dist/types.d.ts +105 -10
  91. package/package.json +1 -1
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Extract user-visible signals from a partial tool-call JSON buffer.
3
+ *
4
+ * We deliberately do NOT attempt a full partial-JSON parse. The goal is just
5
+ * to surface the file path (so the tool header can render) and a coarse
6
+ * "how much has been streamed" hint, both available the moment the model has
7
+ * emitted enough text for them to be unambiguous.
8
+ */
9
+ export interface StreamingArgsHint {
10
+ /** First fully-closed string value found for a known path field. */
11
+ path?: string;
12
+ /** Count of escaped newline sequences (`\n`) seen so far — proxy for written line count. */
13
+ newlineCount: number;
14
+ }
15
+ export declare function extractStreamingArgsHint(raw: string): StreamingArgsHint;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Extract user-visible signals from a partial tool-call JSON buffer.
3
+ *
4
+ * We deliberately do NOT attempt a full partial-JSON parse. The goal is just
5
+ * to surface the file path (so the tool header can render) and a coarse
6
+ * "how much has been streamed" hint, both available the moment the model has
7
+ * emitted enough text for them to be unambiguous.
8
+ */
9
+ const PATH_FIELDS = ["path", "file_path", "filePath"];
10
+ export function extractStreamingArgsHint(raw) {
11
+ let path;
12
+ for (const field of PATH_FIELDS) {
13
+ const re = new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`);
14
+ const m = raw.match(re);
15
+ if (m) {
16
+ try {
17
+ path = JSON.parse(`"${m[1]}"`);
18
+ break;
19
+ }
20
+ catch {
21
+ // The matched substring ended mid-escape; ignore and wait for more.
22
+ }
23
+ }
24
+ }
25
+ const newlines = raw.match(/\\n/g);
26
+ return {
27
+ path,
28
+ newlineCount: newlines ? newlines.length : 0,
29
+ };
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolRenderer } from "./types.js";
2
+ export declare const fallbackToolRenderer: ToolRenderer;
@@ -0,0 +1,75 @@
1
+ import { fg, StyledText } from "@opentui/core";
2
+ export const fallbackToolRenderer = {
3
+ canRender: (_tool) => true,
4
+ render: renderFallbackTool,
5
+ };
6
+ function renderFallbackTool({ ctx, tool, syntaxStyle, width, helpers }) {
7
+ const { theme } = helpers;
8
+ const icon = helpers.toolStateIcon(tool);
9
+ const color = helpers.toolColor(tool);
10
+ const header = helpers.toolHeader(tool);
11
+ const diff = helpers.extractToolDiff(tool);
12
+ const isError = tool.isError === true || tool.status === "error";
13
+ if (diff && !isError && tool.name === "edit") {
14
+ return helpers.createBox(ctx, {
15
+ paddingLeft: 3,
16
+ marginTop: 1,
17
+ flexDirection: "column",
18
+ flexShrink: 0,
19
+ }, [
20
+ helpers.createText(ctx, new StyledText([
21
+ fg(color)(`${icon} ${helpers.displayToolName(tool.name)}`),
22
+ fg(theme.toolText)(header ? ` ${header}` : ""),
23
+ ])),
24
+ helpers.createBox(ctx, {
25
+ paddingLeft: 1,
26
+ marginTop: 1,
27
+ border: ["left"],
28
+ borderColor: theme.borderSubtle,
29
+ flexDirection: "column",
30
+ flexShrink: 0,
31
+ }, [helpers.createDiffRenderable(ctx, diff, helpers.toolPath(tool), syntaxStyle, width)]),
32
+ ]);
33
+ }
34
+ const chunks = [
35
+ fg(color)(`${icon} ${helpers.displayToolName(tool.name)}`),
36
+ ];
37
+ if (header)
38
+ chunks.push(fg(theme.toolText)(` ${header}`));
39
+ const showTail = !!tool.result || tool.status === "running" || tool.status === "pending" || tool.streamingArgs === true;
40
+ if (showTail) {
41
+ const summary = helpers.summarizeToolResult(tool);
42
+ if (summary) {
43
+ chunks.push(fg(theme.text)("\n"));
44
+ chunks.push(fg(theme.borderSubtle)(" "));
45
+ chunks.push(fg(isError ? theme.toolError : theme.textMuted)(summary));
46
+ }
47
+ const preview = helpers.toolPreview(tool);
48
+ if (preview) {
49
+ for (const line of preview.lines) {
50
+ chunks.push(fg(theme.text)("\n"));
51
+ chunks.push(fg(theme.borderSubtle)(" "));
52
+ chunks.push(fg(theme.toolText)(line));
53
+ }
54
+ if (preview.omitted > 0) {
55
+ chunks.push(fg(theme.text)("\n"));
56
+ chunks.push(fg(theme.borderSubtle)(" "));
57
+ chunks.push(fg(theme.textMuted)(`+ ${preview.omitted} more`));
58
+ }
59
+ }
60
+ }
61
+ const containerProps = {
62
+ paddingLeft: isError ? 1 : 3,
63
+ marginTop: 1,
64
+ flexDirection: "column",
65
+ flexShrink: 0,
66
+ };
67
+ if (isError) {
68
+ containerProps.border = ["left"];
69
+ containerProps.borderColor = theme.toolError;
70
+ containerProps.paddingLeft = 2;
71
+ }
72
+ return helpers.createBox(ctx, containerProps, [
73
+ helpers.createText(ctx, new StyledText(chunks), { wrapMode: "word" }),
74
+ ]);
75
+ }
@@ -0,0 +1,3 @@
1
+ import type { DisplayToolCall } from "../display-history.js";
2
+ import type { ToolRenderer } from "./types.js";
3
+ export declare function findToolRenderer(tool: DisplayToolCall): ToolRenderer | undefined;
@@ -0,0 +1,11 @@
1
+ import { fallbackToolRenderer } from "./fallback.js";
2
+ import { subagentToolRenderer } from "./subagent.js";
3
+ import { writeToolRenderer } from "./write.js";
4
+ const TOOL_RENDERERS = [
5
+ writeToolRenderer,
6
+ subagentToolRenderer,
7
+ fallbackToolRenderer,
8
+ ];
9
+ export function findToolRenderer(tool) {
10
+ return TOOL_RENDERERS.find((renderer) => renderer.canRender(tool));
11
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolRenderer } from "./types.js";
2
+ export declare const subagentToolRenderer: ToolRenderer;
@@ -0,0 +1,114 @@
1
+ import { fg, StyledText } from "@opentui/core";
2
+ export const subagentToolRenderer = {
3
+ canRender: (tool) => tool.name === "subagent" || tool.metadata?.kind === "subagent",
4
+ render: renderSubagentTool,
5
+ };
6
+ function renderSubagentTool({ ctx, tool, width, helpers }) {
7
+ const { theme } = helpers;
8
+ const metadata = tool.metadata ?? {};
9
+ const subagents = subagentsFrom(tool);
10
+ const mode = typeof metadata.mode === "string" ? metadata.mode : (subagents.length > 1 ? "parallel" : "single");
11
+ const completed = subagents.filter((item) => item.status === "completed").length;
12
+ const color = tool.isError ? theme.toolError : tool.status === "running" ? theme.toolPending : theme.toolSuccess;
13
+ const headerChunks = [
14
+ fg(color)(`> ${helpers.displayToolName(tool.name)}`),
15
+ fg(theme.toolText)(` ${mode}`),
16
+ ];
17
+ if (subagents.length > 0) {
18
+ headerChunks.push(fg(theme.textMuted)(` ${completed}/${subagents.length}`));
19
+ }
20
+ const children = [
21
+ helpers.createText(ctx, new StyledText(headerChunks), { wrapMode: "word" }),
22
+ ];
23
+ const rows = subagents.map((subagent) => renderSubagentRow(ctx, subagent, width, helpers));
24
+ if (rows.length > 0) {
25
+ children.push(helpers.createBox(ctx, {
26
+ paddingLeft: 1,
27
+ marginTop: 0,
28
+ border: ["left"],
29
+ borderColor: theme.borderSubtle,
30
+ flexDirection: "column",
31
+ flexShrink: 0,
32
+ }, rows));
33
+ }
34
+ else if (tool.result) {
35
+ children.push(helpers.createText(ctx, helpers.summarizeToolResult(tool), {
36
+ fg: tool.isError ? theme.toolError : theme.textMuted,
37
+ wrapMode: "word",
38
+ }));
39
+ }
40
+ return helpers.createBox(ctx, {
41
+ paddingLeft: 3,
42
+ marginTop: 1,
43
+ flexDirection: "column",
44
+ flexShrink: 0,
45
+ }, children);
46
+ }
47
+ function renderSubagentRow(ctx, subagent, width, helpers) {
48
+ const { theme } = helpers;
49
+ const status = subagent.status ?? "running";
50
+ const color = status === "completed"
51
+ ? theme.toolSuccess
52
+ : status === "running" || status === "queued"
53
+ ? theme.toolPending
54
+ : theme.toolError;
55
+ const source = subagent.profileSource ? ` [${subagent.profileSource}]` : "";
56
+ const usage = subagent.usage?.totalTokens ? ` ${subagent.usage.totalTokens} tokens` : "";
57
+ const summary = firstUsefulLine(subagent.error || subagent.summary || lastToolNote(subagent.toolNotes));
58
+ const task = firstUsefulLine(subagent.task);
59
+ const maxLine = Math.max(24, width - 12);
60
+ const lines = [
61
+ helpers.createText(ctx, new StyledText([
62
+ fg(color)(`${statusIcon(status)} ${subagentLabel(subagent)}`),
63
+ fg(theme.textMuted)(`${source} ${status}${usage}`),
64
+ ]), { wrapMode: "word" }),
65
+ ];
66
+ if (task) {
67
+ lines.push(helpers.createText(ctx, shorten(`task: ${task}`, maxLine), {
68
+ fg: theme.textMuted,
69
+ wrapMode: "word",
70
+ }));
71
+ }
72
+ if (summary) {
73
+ lines.push(helpers.createText(ctx, shorten(summary, maxLine), {
74
+ fg: subagent.error ? theme.toolError : theme.toolText,
75
+ wrapMode: "word",
76
+ }));
77
+ }
78
+ return helpers.createBox(ctx, {
79
+ flexDirection: "column",
80
+ flexShrink: 0,
81
+ }, lines);
82
+ }
83
+ function subagentLabel(subagent) {
84
+ if (subagent.nickname && subagent.agentName) {
85
+ return `${subagent.nickname} (${subagent.agentName})`;
86
+ }
87
+ return subagent.nickname ?? subagent.agentName ?? "subagent";
88
+ }
89
+ function subagentsFrom(tool) {
90
+ const raw = tool.metadata?.subagents;
91
+ if (!Array.isArray(raw))
92
+ return [];
93
+ return raw.filter((item) => typeof item === "object" && item !== null);
94
+ }
95
+ function statusIcon(status) {
96
+ if (status === "completed")
97
+ return "+";
98
+ if (status === "running")
99
+ return ">";
100
+ if (status === "queued")
101
+ return ".";
102
+ return "!";
103
+ }
104
+ function lastToolNote(notes) {
105
+ return notes?.filter(Boolean).at(-1);
106
+ }
107
+ function firstUsefulLine(value) {
108
+ return value?.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
109
+ }
110
+ function shorten(value, max) {
111
+ if (value.length <= max)
112
+ return value;
113
+ return `${value.slice(0, Math.max(0, max - 3))}...`;
114
+ }
@@ -0,0 +1,36 @@
1
+ import type { RenderContext, Renderable, StyledText, SyntaxStyle, TextRenderable } from "@opentui/core";
2
+ import type { DisplayToolCall } from "../display-history.js";
3
+ export interface ToolRenderer {
4
+ canRender(tool: DisplayToolCall): boolean;
5
+ expansionKey?(messageKey: string, tool: DisplayToolCall): string | undefined;
6
+ signature?(messageKey: string, tool: DisplayToolCall, expandedWrites: Set<string>): string;
7
+ render(context: ToolRenderContext): Renderable;
8
+ }
9
+ export interface ToolRenderContext {
10
+ ctx: RenderContext;
11
+ tool: DisplayToolCall;
12
+ syntaxStyle: SyntaxStyle;
13
+ width: number;
14
+ writeExpanded: boolean;
15
+ onToggleWrite?: () => void;
16
+ helpers: ToolRenderHelpers;
17
+ }
18
+ export interface ToolRenderHelpers {
19
+ theme: Record<string, string>;
20
+ createBox: (ctx: RenderContext, options: Record<string, unknown>, children?: Array<Renderable | null | undefined>) => Renderable;
21
+ createText: (ctx: RenderContext, content: string | StyledText, options?: Record<string, unknown>) => TextRenderable;
22
+ createCodeBlockRenderable: (ctx: RenderContext, content: string, filePath: string | undefined, syntaxStyle: SyntaxStyle) => Renderable;
23
+ createDiffRenderable: (ctx: RenderContext, diff: string, filePath: string | undefined, syntaxStyle: SyntaxStyle, width?: number) => Renderable;
24
+ toolColor: (tool: DisplayToolCall) => string;
25
+ displayToolName: (name: string) => string;
26
+ toolHeader: (tool: DisplayToolCall) => string;
27
+ toolPath: (tool: DisplayToolCall) => string | undefined;
28
+ extractToolDiff: (tool: DisplayToolCall) => string | undefined;
29
+ summarizeToolResult: (tool: DisplayToolCall) => string;
30
+ isToolFinished: (tool: DisplayToolCall) => boolean;
31
+ toolPreview: (tool: DisplayToolCall) => {
32
+ lines: string[];
33
+ omitted: number;
34
+ } | undefined;
35
+ toolStateIcon: (tool: DisplayToolCall) => string;
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { DisplayToolCall } from "../display-history.js";
2
+ export declare const WRITE_PREVIEW_CHAR_LIMIT = 5000;
3
+ export declare function isWritePreviewTool(tool: DisplayToolCall): tool is DisplayToolCall & {
4
+ args: {
5
+ content?: string;
6
+ };
7
+ };
8
+ export declare function formatWritePreview(content: string, expanded: boolean): {
9
+ content: string;
10
+ omittedLines: number;
11
+ omittedChars: number;
12
+ };
@@ -0,0 +1,30 @@
1
+ const WRITE_PREVIEW_LINE_LIMIT = 10;
2
+ export const WRITE_PREVIEW_CHAR_LIMIT = 5000;
3
+ export function isWritePreviewTool(tool) {
4
+ if (tool.isError)
5
+ return false;
6
+ if (tool.name !== "write")
7
+ return false;
8
+ if (typeof tool.args?.content === "string")
9
+ return true;
10
+ // While the model is still streaming the JSON args, content may not be
11
+ // populated yet — keep ownership so the header renders progressively.
12
+ return tool.streamingArgs === true;
13
+ }
14
+ export function formatWritePreview(content, expanded) {
15
+ const lines = content.split(/\r?\n/);
16
+ if (expanded) {
17
+ return { content, omittedLines: 0, omittedChars: 0 };
18
+ }
19
+ let previewContent = lines.slice(0, WRITE_PREVIEW_LINE_LIMIT).join("\n");
20
+ let omittedLines = Math.max(0, lines.length - WRITE_PREVIEW_LINE_LIMIT);
21
+ if (previewContent.length > WRITE_PREVIEW_CHAR_LIMIT) {
22
+ previewContent = previewContent.slice(0, WRITE_PREVIEW_CHAR_LIMIT);
23
+ omittedLines = Math.max(omittedLines, lines.length - previewContent.split(/\r?\n/).length);
24
+ }
25
+ return {
26
+ content: previewContent,
27
+ omittedLines,
28
+ omittedChars: Math.max(0, content.length - previewContent.length),
29
+ };
30
+ }
@@ -0,0 +1,6 @@
1
+ import type { DisplayMessage, DisplayToolCall } from "../display-history.js";
2
+ import type { ToolRenderer } from "./types.js";
3
+ export declare const writeToolRenderer: ToolRenderer;
4
+ export declare function writeToolKey(messageKey: string, tool: DisplayToolCall): string;
5
+ export declare function writeToolExpansionDigest(message: DisplayMessage, messageKey: string, expandedWrites: Set<string>): string;
6
+ export declare function writeToolExpansionSignature(messageKey: string, tool: DisplayToolCall, expandedWrites: Set<string>): string;
@@ -0,0 +1,88 @@
1
+ import { fg, StyledText } from "@opentui/core";
2
+ import { hashString } from "../render-signature.js";
3
+ import { formatWritePreview, isWritePreviewTool, WRITE_PREVIEW_CHAR_LIMIT } from "./write-preview.js";
4
+ export const writeToolRenderer = {
5
+ canRender: isWritePreviewTool,
6
+ expansionKey: writeToolKey,
7
+ signature: writeToolExpansionSignature,
8
+ render: renderWriteTool,
9
+ };
10
+ export function writeToolKey(messageKey, tool) {
11
+ return `${messageKey}:write:${tool.id}`;
12
+ }
13
+ export function writeToolExpansionDigest(message, messageKey, expandedWrites) {
14
+ return (message.toolCalls ?? [])
15
+ .filter((tool) => isWritePreviewTool(tool))
16
+ .map((tool) => writeToolExpansionSignature(messageKey, tool, expandedWrites))
17
+ .join("|");
18
+ }
19
+ export function writeToolExpansionSignature(messageKey, tool, expandedWrites) {
20
+ if (!isWritePreviewTool(tool))
21
+ return "";
22
+ const content = typeof tool.args.content === "string" ? tool.args.content : "";
23
+ return [
24
+ tool.id,
25
+ expandedWrites.has(writeToolKey(messageKey, tool)) ? "expanded" : "collapsed",
26
+ content.length,
27
+ content.split(/\r?\n/).length,
28
+ hashString(content.slice(0, WRITE_PREVIEW_CHAR_LIMIT)),
29
+ hashString(content.slice(-WRITE_PREVIEW_CHAR_LIMIT)),
30
+ ].join(":");
31
+ }
32
+ function renderWriteTool({ ctx, tool, syntaxStyle, writeExpanded, onToggleWrite, helpers }) {
33
+ const { theme } = helpers;
34
+ const color = helpers.toolColor(tool);
35
+ const icon = "●";
36
+ const header = helpers.toolHeader(tool);
37
+ const hasContent = typeof tool.args.content === "string";
38
+ const contentStr = hasContent ? String(tool.args.content) : "";
39
+ const preview = hasContent ? formatWritePreview(contentStr, writeExpanded) : null;
40
+ const writeLineCount = hasContent
41
+ ? contentStr.split(/\r?\n/).length
42
+ : (tool.streamingNewlineCount ?? 0) + 1;
43
+ const summary = tool.result
44
+ ? helpers.summarizeToolResult(tool)
45
+ : `${helpers.isToolFinished(tool) ? "Prepared" : "Writing"} ${writeLineCount} line${writeLineCount === 1 ? "" : "s"} to ${helpers.toolPath(tool) ?? "file"}`;
46
+ const hint = preview && preview.omittedLines > 0
47
+ ? `... +${preview.omittedLines} lines (${writeExpanded ? "ctrl+o to collapse" : "ctrl+o to expand"})`
48
+ : preview && preview.omittedChars > 0
49
+ ? `... +${preview.omittedChars} chars (${writeExpanded ? "ctrl+o to collapse" : "ctrl+o to expand"})`
50
+ : preview && writeExpanded
51
+ ? "(ctrl+o to collapse)"
52
+ : "";
53
+ return helpers.createBox(ctx, {
54
+ paddingLeft: 3,
55
+ marginTop: 1,
56
+ flexDirection: "column",
57
+ flexShrink: 0,
58
+ }, [
59
+ helpers.createText(ctx, new StyledText([
60
+ fg(color)(`${icon} ${helpers.displayToolName(tool.name)}`),
61
+ fg(theme.toolText)(header ? ` ${header}` : ""),
62
+ ]), {
63
+ onMouseUp: onToggleWrite,
64
+ }),
65
+ helpers.createBox(ctx, {
66
+ paddingLeft: 1,
67
+ marginTop: 0,
68
+ border: ["left"],
69
+ borderColor: theme.borderSubtle,
70
+ flexDirection: "column",
71
+ flexShrink: 0,
72
+ }, [
73
+ helpers.createText(ctx, `└ ${summary}`, {
74
+ fg: tool.isError ? theme.toolError : theme.textMuted,
75
+ onMouseUp: onToggleWrite,
76
+ }),
77
+ preview
78
+ ? helpers.createCodeBlockRenderable(ctx, preview.content, helpers.toolPath(tool), syntaxStyle)
79
+ : null,
80
+ hint
81
+ ? helpers.createText(ctx, hint, {
82
+ fg: theme.textMuted,
83
+ onMouseUp: onToggleWrite,
84
+ })
85
+ : null,
86
+ ]),
87
+ ]);
88
+ }
package/dist/types.d.ts CHANGED
@@ -17,11 +17,6 @@ export type ReasoningEffort = ThinkingLevel;
17
17
  export interface UserMessage {
18
18
  role: "user";
19
19
  content: string | ContentPart[];
20
- /**
21
- * Marks this message as harness-emitted metadata (e.g. a <system-reminder>),
22
- * not actual user input. Renderers may hide these; compaction should generally preserve them.
23
- */
24
- isMeta?: boolean;
25
20
  }
26
21
  export interface AssistantMessage {
27
22
  role: "assistant";
@@ -40,7 +35,19 @@ export interface SystemMessage {
40
35
  role: "system";
41
36
  content: string;
42
37
  }
43
- export type Message = UserMessage | AssistantMessage | ToolMessage | SystemMessage;
38
+ export type MetaMessageKind = "system-reminder" | "runtime-context";
39
+ export interface MetaMessage {
40
+ role: "meta";
41
+ kind: MetaMessageKind;
42
+ content: string;
43
+ /**
44
+ * Runtime metadata is hidden from transcript and session persistence. When
45
+ * true or omitted, the projector may convert it into model context.
46
+ */
47
+ includeInLlm?: boolean;
48
+ }
49
+ export type ProviderMessage = UserMessage | AssistantMessage | ToolMessage | SystemMessage;
50
+ export type Message = ProviderMessage | MetaMessage;
44
51
  export interface ToolParameter {
45
52
  type?: string;
46
53
  description?: string;
@@ -65,13 +72,27 @@ export interface ToolCall {
65
72
  id: string;
66
73
  name: string;
67
74
  arguments: string;
75
+ /**
76
+ * Provider-side flag set when the streamed arguments were unsalvageable
77
+ * (truncated mid-JSON, malformed snapshot). Persists into history so the
78
+ * model and the orchestrator can both see the call was rejected upstream
79
+ * rather than executed silently with empty args.
80
+ */
81
+ argsCorrupt?: boolean;
68
82
  }
69
83
  export interface ParsedToolCall extends ToolCall {
70
84
  parsedArgs: Record<string, any>;
85
+ /**
86
+ * Set when the raw `arguments` string failed to JSON.parse, indicating
87
+ * upstream streaming corruption (truncated chunks, malformed deltas, etc.).
88
+ * Consumers should refuse to execute the tool and surface a tool_use_error
89
+ * so the model can re-issue the call.
90
+ */
91
+ argsCorrupt?: boolean;
71
92
  }
72
93
  export type ToolResultStatus = "success" | "no_match" | "partial" | "timeout" | "blocked" | "command_error";
73
94
  export interface ToolResultMetadata {
74
- kind?: "search" | "read" | "write" | "edit" | "shell" | "web" | "security" | "lsp" | "question";
95
+ kind?: "search" | "read" | "write" | "edit" | "shell" | "web" | "security" | "lsp" | "question" | "subagent";
75
96
  path?: string;
76
97
  pattern?: string;
77
98
  matches?: number;
@@ -88,6 +109,22 @@ export interface ToolResult {
88
109
  status?: ToolResultStatus;
89
110
  metadata?: ToolResultMetadata;
90
111
  }
112
+ export type ToolEffect = "read" | "write_patch" | "write_direct" | "unknown";
113
+ export interface ToolUpdate {
114
+ type: "subagent_update";
115
+ parentToolCallId: string;
116
+ runId: string;
117
+ subAgentId: string;
118
+ agentName: string;
119
+ nickname?: string;
120
+ status: "queued" | "running" | "completed" | "failed" | "blocked" | "cancelled";
121
+ childEvent?: AgentEvent;
122
+ summaryDelta?: string;
123
+ toolName?: string;
124
+ toolCallId?: string;
125
+ message?: string;
126
+ metadata?: ToolResultMetadata;
127
+ }
91
128
  export type ToolExecutor = (args: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>;
92
129
  export interface ToolContext {
93
130
  cwd: string;
@@ -102,16 +139,52 @@ export interface ToolContext {
102
139
  subtaskType?: string;
103
140
  description?: string;
104
141
  }) => Promise<ToolResult>;
142
+ runSubAgent?: (input: string | ContentPart[], cwd: string, options: {
143
+ profile: import("./agent/profiles.js").AgentProfile;
144
+ runId: string;
145
+ subAgentId: string;
146
+ parentToolCallId: string;
147
+ approval?: "fail" | "disabled";
148
+ emitUpdate?: (update: ToolUpdate) => void;
149
+ description?: string;
150
+ abortSignal?: AbortSignal;
151
+ nickname?: string;
152
+ forkContext?: boolean;
153
+ }) => Promise<import("./agent/profiles.js").SubagentRunResult>;
154
+ spawnSubAgent?: (input: string | ContentPart[], cwd: string, options: {
155
+ profile: import("./agent/profiles.js").AgentProfile;
156
+ parentToolCallId: string;
157
+ approval?: "fail" | "disabled";
158
+ description?: string;
159
+ abortSignal?: AbortSignal;
160
+ forkContext?: boolean;
161
+ }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
162
+ waitSubAgents?: (options: {
163
+ agentIds?: string[];
164
+ timeoutMs?: number;
165
+ }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
166
+ sendSubAgentInput?: (agentId: string, input: string | ContentPart[], cwd: string, options?: {
167
+ interrupt?: boolean;
168
+ parentToolCallId?: string;
169
+ abortSignal?: AbortSignal;
170
+ }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
171
+ closeSubAgent?: (agentId: string) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
172
+ listSubAgents?: () => import("./agent/subagent-control.js").SubagentThreadSnapshot[];
105
173
  };
174
+ emitUpdate?: (update: ToolUpdate) => void;
106
175
  }
107
176
  export interface ToolRegistryEntry extends ToolDefinition {
108
177
  execute: ToolExecutor;
109
178
  /** Whether this tool is allowed in plan mode. Defaults to false (treated as write-capable). */
110
179
  readOnly?: boolean;
180
+ /** Capability classification used by subagent profiles. Defaults to "unknown". */
181
+ effect?: ToolEffect;
182
+ /** True when the tool may call ApprovalController.request(...) for an interactive decision. */
183
+ requiresApproval?: boolean;
111
184
  /**
112
185
  * If true, this tool is omitted from the tool list sent to the model on each
113
186
  * turn until unlocked via `tool_search`. Only the tool's name appears in a
114
- * startup &lt;system-reminder&gt;. Used for MCP tools to keep them out of the
187
+ * startup runtime reminder. Used for MCP tools to keep them out of the
115
188
  * per-turn context cost when not in use.
116
189
  */
117
190
  deferred?: boolean;
@@ -154,6 +227,8 @@ export type StreamChunk = {
154
227
  arguments: string;
155
228
  isStart: boolean;
156
229
  isEnd: boolean;
230
+ argumentsFull?: string;
231
+ argumentsCorrupt?: boolean;
157
232
  } | {
158
233
  type: "usage";
159
234
  usage: TokenUsage;
@@ -169,14 +244,14 @@ export interface TokenUsage {
169
244
  totalTokens?: number;
170
245
  }
171
246
  export interface Provider {
172
- streamChat(messages: Message[], options: {
247
+ streamChat(messages: ProviderMessage[], options: {
173
248
  model: string;
174
249
  tools?: ToolDefinition[];
175
250
  temperature?: number;
176
251
  thinkingLevel?: ThinkingLevel;
177
252
  abortSignal?: AbortSignal;
178
253
  }): AsyncIterable<StreamChunk>;
179
- complete(messages: Message[], options?: {
254
+ complete(messages: ProviderMessage[], options?: {
180
255
  model?: string;
181
256
  temperature?: number;
182
257
  thinkingLevel?: ThinkingLevel;
@@ -191,11 +266,31 @@ export type AgentEvent = {
191
266
  } | {
192
267
  type: "reasoning_delta";
193
268
  content: string;
269
+ } | {
270
+ type: "tool_call_start";
271
+ id: string;
272
+ name: string;
273
+ } | {
274
+ type: "tool_call_delta";
275
+ id: string;
276
+ name: string;
277
+ argumentsDelta: string;
278
+ arguments: string;
279
+ } | {
280
+ type: "tool_call_end";
281
+ id: string;
282
+ name: string;
283
+ arguments: string;
194
284
  } | {
195
285
  type: "tool_start";
196
286
  id: string;
197
287
  name: string;
198
288
  args: Record<string, any>;
289
+ } | {
290
+ type: "tool_update";
291
+ id: string;
292
+ name: string;
293
+ update: ToolUpdate;
199
294
  } | {
200
295
  type: "tool_end";
201
296
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {