@bubblebrain-ai/bubble 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.js +1 -1
- package/dist/agent/profiles.d.ts +59 -0
- package/dist/agent/profiles.js +460 -0
- package/dist/agent/subagent-control.d.ts +52 -0
- package/dist/agent/subagent-control.js +38 -0
- package/dist/agent/task-size.d.ts +9 -0
- package/dist/agent/task-size.js +33 -0
- package/dist/agent/tool-intent.d.ts +1 -0
- package/dist/agent/tool-intent.js +1 -1
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +648 -55
- package/dist/context/budget.js +1 -0
- package/dist/context/compact-llm.js +7 -6
- package/dist/context/compact.js +6 -6
- package/dist/context/projector.d.ts +3 -3
- package/dist/context/projector.js +32 -18
- package/dist/context/prune.d.ts +2 -2
- package/dist/context/prune.js +1 -4
- package/dist/main.js +12 -5
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +85 -35
- package/dist/orchestrator/hooks.d.ts +5 -3
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +11 -1
- package/dist/prompt/environment.js +23 -2
- package/dist/prompt/provider-prompts/deepseek.js +1 -2
- package/dist/prompt/provider-prompts/kimi.js +1 -2
- package/dist/prompt/reminders.d.ts +21 -2
- package/dist/prompt/reminders.js +53 -8
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +17 -23
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +16 -8
- package/dist/provider.js +149 -34
- package/dist/session-log.js +3 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +355 -0
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +3 -1
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +228 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +75 -56
- package/dist/tools/exit-plan-mode.js +3 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +32 -0
- package/dist/tools/file-state.d.ts +25 -0
- package/dist/tools/file-state.js +52 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +9 -7
- package/dist/tools/lsp.js +2 -0
- package/dist/tools/memory.js +2 -0
- package/dist/tools/question.js +2 -0
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.js +6 -1
- package/dist/tools/skill.js +1 -0
- package/dist/tools/task.js +1 -0
- package/dist/tools/todo.js +1 -0
- package/dist/tools/tool-search.js +2 -1
- package/dist/tools/web-fetch.js +1 -0
- package/dist/tools/web-search.js +1 -0
- package/dist/tools/write.d.ts +4 -3
- package/dist/tools/write.js +135 -54
- package/dist/tui/display-history.d.ts +10 -1
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.js +811 -274
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +114 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/types.d.ts +105 -10
- package/package.json +1 -1
package/dist/prompt/reminders.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System reminders - short, runtime-variable instructions injected into the
|
|
3
|
-
* message stream as
|
|
3
|
+
* message stream as hidden meta messages.
|
|
4
4
|
*
|
|
5
5
|
* Rationale: the static system prompt is stable and cacheable. Mode transitions
|
|
6
6
|
* and other ephemeral state are signaled via reminders so we do not invalidate
|
|
7
7
|
* the prompt cache every time something changes.
|
|
8
8
|
*/
|
|
9
9
|
export function wrapInSystemReminder(content) {
|
|
10
|
-
return
|
|
10
|
+
return content.trim();
|
|
11
11
|
}
|
|
12
12
|
export function isPermissionModeReminder(content) {
|
|
13
13
|
if (typeof content !== "string")
|
|
@@ -20,7 +20,7 @@ const PLAN_MODE_ENTER = `
|
|
|
20
20
|
Plan mode is now ACTIVE.
|
|
21
21
|
|
|
22
22
|
Rules while in plan mode:
|
|
23
|
-
- Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch,
|
|
23
|
+
- Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch, spawn_agent, wait_agent, send_input, close_agent, skill, todo_write, tool_search, question, and exit_plan_mode.
|
|
24
24
|
- Writes, edits, and shell commands WILL be rejected by the harness; do not try them.
|
|
25
25
|
- Do not edit files or claim implementation is complete while plan mode is active.
|
|
26
26
|
- Investigate the codebase, then use the question tool to clarify important ambiguities, tradeoffs, requirements, or preference choices that would materially change the plan.
|
|
@@ -55,7 +55,6 @@ export function reminderForMode(mode) {
|
|
|
55
55
|
return wrapInSystemReminder(DEFAULT_ENTER);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
// Backward-compat exports kept in case external code pinned the old names.
|
|
59
58
|
export const PLAN_MODE_ENTER_REMINDER = reminderForMode("plan");
|
|
60
59
|
export const PLAN_MODE_EXIT_REMINDER = reminderForMode("default");
|
|
61
60
|
/**
|
|
@@ -172,13 +171,59 @@ Treat the task output as a bounded subtask result:
|
|
|
172
171
|
- do not re-run the same exploratory search unless the subtask uncovered a concrete contradiction
|
|
173
172
|
`);
|
|
174
173
|
}
|
|
175
|
-
|
|
174
|
+
// Removed: buildVerificationReminder / buildVerificationFailureReminder.
|
|
175
|
+
// The verification reminder ladder pressured the model to run a "verification"
|
|
176
|
+
// after every file change. For models with hex-tokenization blind spots (e.g.
|
|
177
|
+
// DeepSeek v4-pro), this triggered death loops where the model wrote ad-hoc
|
|
178
|
+
// validation scripts that found the bug but could never fix it. CC trusts the
|
|
179
|
+
// model to decide when verification is meaningful; we follow that.
|
|
180
|
+
/**
|
|
181
|
+
* Fired when the same edit/write tool call (identical tool name + args) has
|
|
182
|
+
* just failed for the second time in a row. Models — especially thinking-heavy
|
|
183
|
+
* ones — can otherwise spiral on `No changes made: identical content` or
|
|
184
|
+
* `oldText not found` because their internal reasoning convinces them they
|
|
185
|
+
* are typing the change correctly even though the JSON args arrive identical.
|
|
186
|
+
* This nudge forces a strategy change.
|
|
187
|
+
*/
|
|
188
|
+
export function buildEditRetryEscalationReminder(reason) {
|
|
176
189
|
return wrapInSystemReminder(`
|
|
177
|
-
|
|
190
|
+
The same edit/write call has failed twice with identical arguments.
|
|
178
191
|
|
|
179
192
|
${reason}
|
|
180
193
|
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
Stop retrying the same call. Pick one of:
|
|
195
|
+
- Re-read the target file and compare the actual bytes to your intended oldText / newText. Trailing whitespace, unicode lookalikes, or off-by-one boundaries are common causes.
|
|
196
|
+
- If you intended to add a single character (e.g. fixing a 5-digit hex color to 6 digits), confirm that your newText string actually contains the added character before sending again.
|
|
197
|
+
- Use the write tool with overwrite=true and the full new content instead of edit — useful when the change spans many lines or the diff anchor is ambiguous. Existing files must be read or modified in this session before full-file replacement.
|
|
198
|
+
- If you cannot determine the cause, ask the user for clarification.
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Fired the FIRST time the model re-reads a file it already read in this turn.
|
|
203
|
+
* Soft — does not freeze the tool. Just prevents a 3rd / 4th re-read.
|
|
204
|
+
*/
|
|
205
|
+
export function buildRedundantReadReminder(path) {
|
|
206
|
+
return wrapInSystemReminder(`
|
|
207
|
+
You already read ${path} earlier in this turn. Use the content already in context rather than re-reading.
|
|
208
|
+
Only re-read this path if a subsequent tool call (edit/write/bash) modified it since.
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Injected once at task start when the user's input looks like a small,
|
|
213
|
+
* focused task (e.g. "write an HTML page about X"). Counterweight to the
|
|
214
|
+
* default protocol which biases toward thorough exploration.
|
|
215
|
+
*/
|
|
216
|
+
export function buildSmallTaskHint() {
|
|
217
|
+
return wrapInSystemReminder(`
|
|
218
|
+
This appears to be a small, focused task (short request, single deliverable, no integration ambiguity).
|
|
219
|
+
|
|
220
|
+
Prefer direct execution over exploration:
|
|
221
|
+
- If the target file path is given or obvious, use write/edit directly.
|
|
222
|
+
- Do not glob, read, or grep adjacent files unless the request explicitly references them.
|
|
223
|
+
- Do not pre-plan with todo_write for tasks that can be done in one or two tool calls.
|
|
224
|
+
- Skip the "investigate the codebase" step that applies to larger changes.
|
|
183
225
|
`);
|
|
184
226
|
}
|
|
227
|
+
// Removed: buildFinalizeOpportunityReminder. Was paired with the verification
|
|
228
|
+
// nag ladder. Without the ladder, "you can finalize now" advice is redundant —
|
|
229
|
+
// the model finalises whenever its own judgement says the task is done.
|
package/dist/prompt/runtime.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface RuntimePromptOptions {
|
|
|
3
3
|
thinkingLevel?: ThinkingLevel;
|
|
4
4
|
/**
|
|
5
5
|
* Kept for API compatibility. Agent mode is no longer baked into the static
|
|
6
|
-
* system prompt — mode changes are signalled via
|
|
6
|
+
* system prompt — mode changes are signalled via hidden runtime reminders
|
|
7
7
|
* (see src/prompt/reminders.ts) so the base prompt stays stable for caching.
|
|
8
8
|
*/
|
|
9
9
|
mode?: PermissionMode;
|
package/dist/prompt/runtime.js
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
|
+
// Compact, prose-shaped guidelines. Each line is one rule. The set is kept
|
|
2
|
+
// short on purpose: a thinking-heavy model burns reasoning tokens on every
|
|
3
|
+
// rule it has to weigh per turn, and most behaviors should be background
|
|
4
|
+
// disposition, not active checklist items. Add to this list only when an
|
|
5
|
+
// observed failure cannot be addressed by an existing rule.
|
|
1
6
|
const defaultGuidelines = [
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"Prefer structured search tools over bash for repository searches whenever possible",
|
|
9
|
-
"Do not repeat near-identical searches when they are not producing new evidence",
|
|
10
|
-
"When investigating configuration or security questions, stop once the relevant load path, storage path, and exposure path are identified",
|
|
11
|
-
"Use the task tool 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",
|
|
7
|
+
"Ground decisions in the codebase: inspect relevant files, command output, or runtime state before making claims about behavior. Separate confirmed facts from inference when evidence is incomplete.",
|
|
8
|
+
"Choose the smallest coherent change. Edit only the files required for the requested change; do not refactor or improve adjacent code unprompted.",
|
|
9
|
+
"For modifications to existing code, read the file first. For brand-new files whose target path is known and does not exist, write directly without exploratory reading. Use edit for small targeted changes; use write with overwrite=true for intentional full-file replacement of an existing file. Never delete and recreate a file just to overwrite it.",
|
|
10
|
+
"Prefer structured tools (glob, grep, lsp, read) over bash for search and inspection. Do not repeat a near-identical search or re-read the same file unless new evidence changes the question.",
|
|
11
|
+
"If a tool fails, diagnose the error before switching tactics. Do not retry the identical call with identical arguments. After two equivalent failures, switch approach — re-read the file, use a different tool, rewrite the whole file with write overwrite=true, or ask the user.",
|
|
12
|
+
"Before reporting a task complete, verify it works when verification is meaningful and cheap — run the existing test, execute the script, check the output. If no test exists, the change is purely declarative (static HTML/markdown/config), or running the code is not practical, state that explicitly rather than inventing a verification step. Do not write throwaway validation scripts to prove correctness; if there is no real check to run, report the change and stop.",
|
|
15
13
|
];
|
|
16
14
|
export function buildRuntimePrompt(options = {}) {
|
|
17
|
-
const thinkingLevel = options.thinkingLevel ?? "off";
|
|
18
15
|
const guidelines = dedupe(defaultGuidelines, options.guidelines ?? []);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
16
|
+
// The execution flow is stated as a single prose sentence rather than a
|
|
17
|
+
// numbered protocol. Numbered checklists prompt thinking models to walk
|
|
18
|
+
// each step explicitly in their reasoning every turn, even for trivial
|
|
19
|
+
// tasks — multiplying latency without improving quality. Prose lets the
|
|
20
|
+
// protocol act as background disposition.
|
|
21
|
+
return `Work by understanding the requested outcome, grounding decisions in the codebase, making the smallest coherent change, and verifying when possible. Scale your effort to the task: a one-file create-or-edit deserves direct execution, not extensive pre-exploration.
|
|
28
22
|
|
|
29
23
|
Guidelines:
|
|
30
24
|
${guidelines.map((item) => `- ${item}`).join("\n")}`;
|
|
@@ -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
|
+
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
6
|
+
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
|
|
7
7
|
type ReasoningContentEcho = "tool_calls" | "all";
|
|
8
|
-
export
|
|
8
|
+
export type ToolArgsMergeMode = "delta" | "snapshot";
|
|
9
|
+
export interface TranslateOpenAIStreamOptions {
|
|
10
|
+
toolArgsMergeMode?: ToolArgsMergeMode;
|
|
11
|
+
}
|
|
12
|
+
export declare function toChatCompletionsMessage(message: ProviderMessage, options?: {
|
|
9
13
|
reasoningContentEcho?: ReasoningContentEcho;
|
|
10
14
|
}): Record<string, unknown>;
|
|
11
15
|
export interface ProviderInstanceOptions {
|
|
@@ -17,15 +21,19 @@ export interface ProviderInstanceOptions {
|
|
|
17
21
|
}
|
|
18
22
|
export declare function createUnavailableProvider(message: string): Provider;
|
|
19
23
|
export declare function createProviderInstance(options: ProviderInstanceOptions): Provider;
|
|
24
|
+
export interface NormalizedToolArgs {
|
|
25
|
+
args: string;
|
|
26
|
+
corrupt: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function normalizeToolArgsDetailed(raw: string): NormalizedToolArgs;
|
|
20
29
|
export declare function normalizeToolArgs(raw: string): string;
|
|
21
30
|
/**
|
|
22
31
|
* Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
|
|
23
32
|
*
|
|
24
|
-
* Multi-tool-call streams are
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* previous single-slot implementation silently dropped every call but the last.
|
|
33
|
+
* Multi-tool-call streams are tracked by `index`, but tool-call starts and
|
|
34
|
+
* argument deltas are emitted as soon as they arrive so the TUI can render
|
|
35
|
+
* partial write previews before the tool executes. End events are still flushed
|
|
36
|
+
* in index order to keep multi-call turns deterministic.
|
|
29
37
|
*/
|
|
30
|
-
export declare function translateOpenAIStream(stream: AsyncIterable<any
|
|
38
|
+
export declare function translateOpenAIStream(stream: AsyncIterable<any>, options?: TranslateOpenAIStreamOptions): AsyncIterable<StreamChunk>;
|
|
31
39
|
export {};
|
package/dist/provider.js
CHANGED
|
@@ -4,8 +4,25 @@
|
|
|
4
4
|
* Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
|
|
5
5
|
*/
|
|
6
6
|
import OpenAI from "openai";
|
|
7
|
+
import { appendFileSync } from "node:fs";
|
|
7
8
|
import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
|
|
9
|
+
import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
|
|
8
10
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
11
|
+
// Diagnostic logger for tool-args byte-loss investigation. Activate with
|
|
12
|
+
// BUBBLE_DEBUG_TOOL_ARGS=/path/to/log.jsonl (any writable path)
|
|
13
|
+
// Each line is a JSON record describing a transition. When debugging is off,
|
|
14
|
+
// the function is a no-op and free.
|
|
15
|
+
const TOOL_ARGS_DEBUG_PATH = process.env.BUBBLE_DEBUG_TOOL_ARGS?.trim();
|
|
16
|
+
function debugToolArgs(event) {
|
|
17
|
+
if (!TOOL_ARGS_DEBUG_PATH)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
appendFileSync(TOOL_ARGS_DEBUG_PATH, JSON.stringify({ t: Date.now(), ...event }) + "\n", "utf-8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Diagnostic failures must not affect the model session.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
9
26
|
export function toChatCompletionsMessage(message, options = {}) {
|
|
10
27
|
const reasoningContentEcho = options.reasoningContentEcho ?? "tool_calls";
|
|
11
28
|
if (message.role === "assistant") {
|
|
@@ -90,7 +107,9 @@ export function createProviderInstance(options) {
|
|
|
90
107
|
const stream = (await client.chat.completions.create(body, {
|
|
91
108
|
signal: chatOptions.abortSignal,
|
|
92
109
|
}));
|
|
93
|
-
yield* translateOpenAIStream(stream
|
|
110
|
+
yield* translateOpenAIStream(stream, {
|
|
111
|
+
toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
|
|
112
|
+
});
|
|
94
113
|
yield { type: "done" };
|
|
95
114
|
}
|
|
96
115
|
async function complete(messages, chatOptions) {
|
|
@@ -117,19 +136,16 @@ export function createProviderInstance(options) {
|
|
|
117
136
|
}
|
|
118
137
|
return { streamChat, complete };
|
|
119
138
|
}
|
|
120
|
-
|
|
121
|
-
// as repeated full snapshots in each delta instead of incremental chunks, so
|
|
122
|
-
// a naive `+=` produces `{"x":1}{"x":1}` — not valid JSON. Parse the raw
|
|
123
|
-
// stream; if it doesn't parse but contains a balanced `{…}` prefix or suffix
|
|
124
|
-
// that does, use that. Empty or unsalvageable input becomes `"{}"` so the
|
|
125
|
-
// downstream echo to the model is always valid JSON.
|
|
126
|
-
export function normalizeToolArgs(raw) {
|
|
139
|
+
export function normalizeToolArgsDetailed(raw) {
|
|
127
140
|
const s = (raw ?? "").trim();
|
|
128
|
-
if (!s)
|
|
129
|
-
|
|
141
|
+
if (!s) {
|
|
142
|
+
debugToolArgs({ stage: "normalize", input: raw, output: "{}", reason: "empty" });
|
|
143
|
+
return { args: "{}", corrupt: false };
|
|
144
|
+
}
|
|
130
145
|
try {
|
|
131
146
|
JSON.parse(s);
|
|
132
|
-
|
|
147
|
+
debugToolArgs({ stage: "normalize", input: raw, output: s, reason: "passthrough" });
|
|
148
|
+
return { args: s, corrupt: false };
|
|
133
149
|
}
|
|
134
150
|
catch { }
|
|
135
151
|
const firstBrace = extractBalancedJson(s, 0);
|
|
@@ -138,21 +154,39 @@ export function normalizeToolArgs(raw) {
|
|
|
138
154
|
JSON.parse(firstBrace);
|
|
139
155
|
}
|
|
140
156
|
catch {
|
|
141
|
-
|
|
157
|
+
debugToolArgs({ stage: "normalize", input: raw, output: "{}", reason: "first-brace-unparseable", firstBrace });
|
|
158
|
+
return { args: "{}", corrupt: true };
|
|
142
159
|
}
|
|
143
160
|
// If the content after the first balanced object is another valid object
|
|
144
161
|
// with the same parse, we've got a snapshot duplication — keep one copy.
|
|
145
162
|
const rest = s.slice(firstBrace.length).trim();
|
|
146
|
-
if (!rest)
|
|
147
|
-
|
|
163
|
+
if (!rest) {
|
|
164
|
+
debugToolArgs({ stage: "normalize", input: raw, output: firstBrace, reason: "single-brace" });
|
|
165
|
+
return { args: firstBrace, corrupt: false };
|
|
166
|
+
}
|
|
148
167
|
try {
|
|
149
168
|
JSON.parse(rest);
|
|
150
|
-
|
|
169
|
+
debugToolArgs({ stage: "normalize", input: raw, output: firstBrace, reason: "snapshot-dedup", rest });
|
|
170
|
+
return { args: firstBrace, corrupt: false };
|
|
151
171
|
}
|
|
152
172
|
catch { }
|
|
153
|
-
|
|
173
|
+
debugToolArgs({ stage: "normalize", input: raw, output: firstBrace, reason: "trailing-junk-dropped", rest });
|
|
174
|
+
return { args: firstBrace, corrupt: false };
|
|
154
175
|
}
|
|
155
|
-
|
|
176
|
+
debugToolArgs({ stage: "normalize", input: raw, output: "{}", reason: "no-balanced-json" });
|
|
177
|
+
return { args: "{}", corrupt: true };
|
|
178
|
+
}
|
|
179
|
+
export function normalizeToolArgs(raw) {
|
|
180
|
+
return normalizeToolArgsDetailed(raw).args;
|
|
181
|
+
}
|
|
182
|
+
function resolveToolArgsMergeMode(providerId, baseURL) {
|
|
183
|
+
const id = providerId.toLowerCase();
|
|
184
|
+
const url = baseURL.toLowerCase();
|
|
185
|
+
// Fireworks-hosted Kimi has been observed to stream cumulative snapshots
|
|
186
|
+
// rather than OpenAI-style argument deltas.
|
|
187
|
+
if (id === "fireworks" || url.includes("fireworks.ai"))
|
|
188
|
+
return "snapshot";
|
|
189
|
+
return "delta";
|
|
156
190
|
}
|
|
157
191
|
function extractBalancedJson(s, start) {
|
|
158
192
|
if (s[start] !== "{")
|
|
@@ -189,14 +223,21 @@ function extractBalancedJson(s, start) {
|
|
|
189
223
|
/**
|
|
190
224
|
* Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
|
|
191
225
|
*
|
|
192
|
-
* Multi-tool-call streams are
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* previous single-slot implementation silently dropped every call but the last.
|
|
226
|
+
* Multi-tool-call streams are tracked by `index`, but tool-call starts and
|
|
227
|
+
* argument deltas are emitted as soon as they arrive so the TUI can render
|
|
228
|
+
* partial write previews before the tool executes. End events are still flushed
|
|
229
|
+
* in index order to keep multi-call turns deterministic.
|
|
197
230
|
*/
|
|
198
|
-
export async function* translateOpenAIStream(stream) {
|
|
231
|
+
export async function* translateOpenAIStream(stream, options = {}) {
|
|
199
232
|
const toolCalls = new Map();
|
|
233
|
+
const textFilter = createProviderProtocolArtifactFilter();
|
|
234
|
+
const toolArgsMergeMode = options.toolArgsMergeMode ?? "delta";
|
|
235
|
+
// DeepSeek (and some inference re-hosts) sometimes deliver reasoning twice:
|
|
236
|
+
// once via a dedicated `reasoning_content` / `thinking` field, and again
|
|
237
|
+
// embedded as `<think>...</think>` inside `delta.content`. Track whether we
|
|
238
|
+
// have seen the dedicated channel; if yes, strip <think> blocks from text
|
|
239
|
+
// silently instead of yielding a second reasoning_delta.
|
|
240
|
+
let hasDedicatedReasoningChannel = false;
|
|
200
241
|
function* flushToolCalls() {
|
|
201
242
|
if (toolCalls.size === 0)
|
|
202
243
|
return;
|
|
@@ -204,13 +245,37 @@ export async function* translateOpenAIStream(stream) {
|
|
|
204
245
|
for (const [, entry] of sorted) {
|
|
205
246
|
if (!entry.id || !entry.name)
|
|
206
247
|
continue;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
248
|
+
if (!entry.started) {
|
|
249
|
+
yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
|
|
250
|
+
entry.started = true;
|
|
251
|
+
if (entry.args) {
|
|
252
|
+
yield { type: "tool_call", id: entry.id, name: entry.name, arguments: entry.args, isStart: false, isEnd: false };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const normalized = normalizeToolArgsDetailed(entry.args);
|
|
256
|
+
debugToolArgs({ stage: "flush-end", id: entry.id, name: entry.name, entryArgs: entry.args, finalArgs: normalized.args, corrupt: normalized.corrupt });
|
|
257
|
+
yield {
|
|
258
|
+
type: "tool_call",
|
|
259
|
+
id: entry.id,
|
|
260
|
+
name: entry.name,
|
|
261
|
+
arguments: "",
|
|
262
|
+
argumentsFull: normalized.args,
|
|
263
|
+
argumentsCorrupt: normalized.corrupt || undefined,
|
|
264
|
+
isStart: false,
|
|
265
|
+
isEnd: true,
|
|
266
|
+
};
|
|
211
267
|
}
|
|
212
268
|
toolCalls.clear();
|
|
213
269
|
}
|
|
270
|
+
function* startToolCallIfReady(entry) {
|
|
271
|
+
if (entry.started || !entry.id || !entry.name)
|
|
272
|
+
return;
|
|
273
|
+
entry.started = true;
|
|
274
|
+
yield { type: "tool_call", id: entry.id, name: entry.name, arguments: "", isStart: true, isEnd: false };
|
|
275
|
+
if (entry.args) {
|
|
276
|
+
yield { type: "tool_call", id: entry.id, name: entry.name, arguments: entry.args, isStart: false, isEnd: false };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
214
279
|
for await (const chunk of stream) {
|
|
215
280
|
const delta = chunk.choices?.[0]?.delta;
|
|
216
281
|
const usage = chunk.usage;
|
|
@@ -231,21 +296,26 @@ export async function* translateOpenAIStream(stream) {
|
|
|
231
296
|
}
|
|
232
297
|
const reasoning = delta?.reasoning ?? delta?.thinking ?? delta?.reasoning_content;
|
|
233
298
|
if (reasoning) {
|
|
299
|
+
hasDedicatedReasoningChannel = true;
|
|
234
300
|
yield { type: "reasoning_delta", content: reasoning };
|
|
235
301
|
}
|
|
236
302
|
if (delta?.content) {
|
|
237
303
|
const thinkMatch = delta.content.match(/<think>([\s\S]*?)<\/think>/);
|
|
238
304
|
if (thinkMatch) {
|
|
239
|
-
if (thinkMatch[1]) {
|
|
305
|
+
if (thinkMatch[1] && !hasDedicatedReasoningChannel) {
|
|
240
306
|
yield { type: "reasoning_delta", content: thinkMatch[1] };
|
|
241
307
|
}
|
|
242
308
|
const remaining = delta.content.replace(/<think>[\s\S]*?<\/think>/, "");
|
|
243
|
-
|
|
244
|
-
|
|
309
|
+
const cleaned = textFilter.push(remaining);
|
|
310
|
+
if (cleaned) {
|
|
311
|
+
yield { type: "text", content: cleaned };
|
|
245
312
|
}
|
|
246
313
|
}
|
|
247
314
|
else {
|
|
248
|
-
|
|
315
|
+
const cleaned = textFilter.push(delta.content);
|
|
316
|
+
if (cleaned) {
|
|
317
|
+
yield { type: "text", content: cleaned };
|
|
318
|
+
}
|
|
249
319
|
}
|
|
250
320
|
}
|
|
251
321
|
if (delta?.tool_calls) {
|
|
@@ -253,15 +323,29 @@ export async function* translateOpenAIStream(stream) {
|
|
|
253
323
|
const idx = typeof tc.index === "number" ? tc.index : 0;
|
|
254
324
|
let entry = toolCalls.get(idx);
|
|
255
325
|
if (!entry) {
|
|
256
|
-
entry = { id: "", name: "", args: "" };
|
|
326
|
+
entry = { id: "", name: "", args: "", started: false };
|
|
257
327
|
toolCalls.set(idx, entry);
|
|
258
328
|
}
|
|
259
329
|
if (tc.id)
|
|
260
330
|
entry.id = tc.id;
|
|
261
331
|
if (tc.function?.name)
|
|
262
332
|
entry.name = tc.function.name;
|
|
263
|
-
|
|
264
|
-
|
|
333
|
+
yield* startToolCallIfReady(entry);
|
|
334
|
+
if (typeof tc.function?.arguments === "string" && tc.function.arguments) {
|
|
335
|
+
debugToolArgs({ stage: "raw-chunk", id: entry.id, name: entry.name, idx, raw: tc.function.arguments });
|
|
336
|
+
const merged = mergeToolArgumentDelta(entry.args, tc.function.arguments, toolArgsMergeMode);
|
|
337
|
+
entry.args = merged.args;
|
|
338
|
+
if (entry.started && merged.delta) {
|
|
339
|
+
yield {
|
|
340
|
+
type: "tool_call",
|
|
341
|
+
id: entry.id,
|
|
342
|
+
name: entry.name,
|
|
343
|
+
arguments: merged.delta,
|
|
344
|
+
isStart: false,
|
|
345
|
+
isEnd: false,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
265
349
|
}
|
|
266
350
|
}
|
|
267
351
|
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
@@ -269,5 +353,36 @@ export async function* translateOpenAIStream(stream) {
|
|
|
269
353
|
yield* flushToolCalls();
|
|
270
354
|
}
|
|
271
355
|
}
|
|
356
|
+
const remainingText = textFilter.flush();
|
|
357
|
+
if (remainingText) {
|
|
358
|
+
yield { type: "text", content: remainingText };
|
|
359
|
+
}
|
|
272
360
|
yield* flushToolCalls();
|
|
273
361
|
}
|
|
362
|
+
function mergeToolArgumentDelta(current, incoming, mode) {
|
|
363
|
+
if (!current) {
|
|
364
|
+
debugToolArgs({ stage: "merge", branch: "empty-current", current, incoming, args: incoming, delta: incoming });
|
|
365
|
+
return { args: incoming, delta: incoming };
|
|
366
|
+
}
|
|
367
|
+
if (!incoming) {
|
|
368
|
+
debugToolArgs({ stage: "merge", branch: "empty-incoming", current, incoming, args: current, delta: "" });
|
|
369
|
+
return { args: current, delta: "" };
|
|
370
|
+
}
|
|
371
|
+
if (mode === "snapshot") {
|
|
372
|
+
// Snapshot streams repeat the current full argument buffer. Only treat a
|
|
373
|
+
// chunk as duplicate when it is exactly equal, or as growth when it carries
|
|
374
|
+
// the current buffer as a prefix. A suffix match is not enough: the next
|
|
375
|
+
// legitimate delta can be a single trailing character like "0".
|
|
376
|
+
if (incoming === current) {
|
|
377
|
+
debugToolArgs({ stage: "merge", branch: "snapshot-dup", current, incoming, args: current, delta: "" });
|
|
378
|
+
return { args: current, delta: "" };
|
|
379
|
+
}
|
|
380
|
+
if (incoming.startsWith(current)) {
|
|
381
|
+
const delta = incoming.slice(current.length);
|
|
382
|
+
debugToolArgs({ stage: "merge", branch: "snapshot-grow", current, incoming, args: incoming, delta });
|
|
383
|
+
return { args: incoming, delta };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
debugToolArgs({ stage: "merge", branch: mode === "delta" ? "delta-append" : "snapshot-fallback-concat", current, incoming, args: current + incoming, delta: incoming });
|
|
387
|
+
return { args: current + incoming, delta: incoming };
|
|
388
|
+
}
|
package/dist/session-log.js
CHANGED
|
@@ -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;
|
package/dist/system-prompt.d.ts
CHANGED
|
@@ -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[];
|