@bubblebrain-ai/bubble 0.0.55 → 0.0.57
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/README.md +35 -0
- package/dist/agent/child-runner.d.ts +7 -0
- package/dist/agent/child-runner.js +67 -3
- package/dist/agent/subagent/runtime.d.ts +3 -0
- package/dist/agent/subagent/runtime.js +38 -8
- package/dist/agent/transcript-helpers.d.ts +1 -1
- package/dist/agent/transcript-helpers.js +4 -3
- package/dist/agent.d.ts +6 -0
- package/dist/agent.js +36 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +22 -3
- package/dist/context/budget.js +7 -2
- package/dist/context/compact.js +8 -2
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/main.js +16 -1
- package/dist/mcp/client.js +15 -1
- package/dist/mcp/transports.d.ts +2 -2
- package/dist/mcp/types.d.ts +1 -1
- package/dist/memory/phase1.js +4 -0
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +7 -0
- package/dist/model-pricing.js +0 -0
- package/dist/provider-ark-responses.d.ts +12 -2
- package/dist/provider-ark-responses.js +68 -13
- package/dist/provider-error-record.d.ts +34 -0
- package/dist/provider-error-record.js +185 -0
- package/dist/provider-transform.js +1 -0
- package/dist/provider.js +7 -1
- package/dist/sdk/index.js +5 -0
- package/dist/session-log.d.ts +3 -1
- package/dist/session-log.js +11 -0
- package/dist/session-types.d.ts +6 -1
- package/dist/session.d.ts +2 -0
- package/dist/session.js +4 -0
- package/dist/stream-json.d.ts +13 -0
- package/dist/stream-json.js +74 -0
- package/dist/tui/controller/agent-event-reducer.js +2 -1
- package/dist/tui/controller/controller.js +6 -5
- package/dist/tui/model/image-paste.js +9 -10
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,41 @@ Resume your last conversation:
|
|
|
67
67
|
bubble --resume
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## Persistent headless sessions
|
|
71
|
+
|
|
72
|
+
To keep one agent session and its MCP connections alive across multiple prompts:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bubble -p --input-format stream-json --output-format stream-json \
|
|
76
|
+
--model zhipuai-coding-plan:glm-5.3 --session my-session
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Write one JSON message per line to stdin (keep stdin open between turns):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{"type":"user","request_id":"turn-1","message":{"role":"user","content":"Inspect the page."}}
|
|
83
|
+
{"type":"user","request_id":"turn-2","message":{"role":"user","content":"Continue with the next action."}}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Stdout contains only newline-delimited JSON: one `system/init` event with the model
|
|
87
|
+
and `session_id`, `stream_event` messages wrapping native agent events (including
|
|
88
|
+
`text_delta`, `tool_start`, and `tool_end`), and one `result` per input. Results
|
|
89
|
+
include `is_error`, text in `result`, token usage, and the input `request_id` when
|
|
90
|
+
provided. This is Bubble's protocol; it does not claim full Claude Code protocol
|
|
91
|
+
compatibility. Input content may also be an array of text blocks.
|
|
92
|
+
|
|
93
|
+
Turns run sequentially in arrival order and retain the same agent history, MCP
|
|
94
|
+
connections, and tool discovery state. Malformed input and failed turns produce an
|
|
95
|
+
error result; the process remains available for the next input. An optional
|
|
96
|
+
`session_id` must match the initialized session. EOF drains submitted turns, then
|
|
97
|
+
releases resources. SIGINT/SIGTERM stop the process. Input lines are limited to
|
|
98
|
+
1 MiB. Permission settings remain unchanged unless explicitly overridden.
|
|
99
|
+
|
|
100
|
+
Both stream format flags require `-p`; positional prompts are not accepted in this
|
|
101
|
+
mode. To resume a saved session, pass both `--resume` and `--session <name>` so no
|
|
102
|
+
interactive picker is needed. Existing plain and single-result JSON print modes
|
|
103
|
+
retain their behavior.
|
|
104
|
+
|
|
70
105
|
## Embed with the SDK
|
|
71
106
|
|
|
72
107
|
The npm package exports `BubbleSdk` from both `@bubblebrain-ai/bubble` and
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
import type { SubagentRunOutcome } from "./subagent-scheduler.js";
|
|
12
12
|
import type { SubagentFinalReason, SubagentThreadRecord } from "./subagent-control.js";
|
|
13
13
|
import type { AgentEvent, Message, ToolRegistryEntry, ToolResult, ToolUpdate } from "../types.js";
|
|
14
|
+
/** Bubble-authored configuration failure whose message contains no provider data. */
|
|
15
|
+
export declare class SafeSubagentConfigurationError extends Error {
|
|
16
|
+
}
|
|
14
17
|
export interface ChildRunOptions {
|
|
15
18
|
approval: "fail" | "disabled";
|
|
16
19
|
abortSignal?: AbortSignal;
|
|
@@ -27,6 +30,10 @@ export interface ChildRunnerHost {
|
|
|
27
30
|
runLifecycleHook(record: SubagentThreadRecord, cwd: string, eventName: "SubagentStart" | "SubagentStop", status?: string, error?: string, abortSignal?: AbortSignal): Promise<void>;
|
|
28
31
|
finalizeBlocked(record: SubagentThreadRecord, error: string, options: ChildRunOptions): void;
|
|
29
32
|
createInstance(record: SubagentThreadRecord, tools: ToolRegistryEntry[], cwd: string, forkContext?: boolean): Promise<NonNullable<SubagentThreadRecord["agent"]>>;
|
|
33
|
+
recordProviderError(record: SubagentThreadRecord, error: unknown, request: {
|
|
34
|
+
messageCount: number;
|
|
35
|
+
toolCount: number;
|
|
36
|
+
}): void;
|
|
30
37
|
notifyWaiters(record: SubagentThreadRecord): void;
|
|
31
38
|
/** Called on every final state so background results can be ingested (§5). */
|
|
32
39
|
onFinal(record: SubagentThreadRecord, options: ChildRunOptions): void;
|
|
@@ -13,8 +13,12 @@ import { composeAbortSignals } from "./budget-ledger.js";
|
|
|
13
13
|
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "../provider-artifacts.js";
|
|
14
14
|
import { isRateLimitError } from "../network/errors.js";
|
|
15
15
|
import { isProviderTransportError } from "../network/provider-transport.js";
|
|
16
|
+
import { sanitizeProviderErrorText } from "../provider-error-record.js";
|
|
16
17
|
import { mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./profiles.js";
|
|
17
18
|
import { estimateHandoffTokens, HANDOFF_TOKEN_FLOOR, isIntermediateHandoff, stripInternalTagFragments, } from "./subagent-summary.js";
|
|
19
|
+
/** Bubble-authored configuration failure whose message contains no provider data. */
|
|
20
|
+
export class SafeSubagentConfigurationError extends Error {
|
|
21
|
+
}
|
|
18
22
|
export class ChildRunner {
|
|
19
23
|
host;
|
|
20
24
|
constructor(host) {
|
|
@@ -58,7 +62,13 @@ export class ChildRunner {
|
|
|
58
62
|
catch (error) {
|
|
59
63
|
// Instance creation failed before the run started: no SubagentStart
|
|
60
64
|
// fired, so no SubagentStop follows (§9 — hooks pair per started run).
|
|
61
|
-
|
|
65
|
+
const isSafeConfigurationError = error instanceof SafeSubagentConfigurationError;
|
|
66
|
+
if (!isSafeConfigurationError) {
|
|
67
|
+
this.host.recordProviderError(record, error, { messageCount: 0, toolCount: tools.length });
|
|
68
|
+
}
|
|
69
|
+
this.host.finalizeBlocked(record, isSafeConfigurationError
|
|
70
|
+
? error.message
|
|
71
|
+
: sanitizeProviderErrorText(error?.message || String(error)), options);
|
|
62
72
|
record.finalReason = "failed_fatal";
|
|
63
73
|
this.host.onFinal(record, options);
|
|
64
74
|
return { kind: "final" };
|
|
@@ -168,7 +178,15 @@ export class ChildRunner {
|
|
|
168
178
|
? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal)
|
|
169
179
|
: "failed_transient";
|
|
170
180
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
171
|
-
|
|
181
|
+
if (!cancelled) {
|
|
182
|
+
this.host.recordProviderError(record, error, {
|
|
183
|
+
messageCount: messagesBeforeInterruptedBoundary(subAgent.messages),
|
|
184
|
+
toolCount: tools.length,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
record.error = cancelled
|
|
188
|
+
? "Subagent was cancelled."
|
|
189
|
+
: sanitizeProviderErrorText(error?.message || String(error));
|
|
172
190
|
record.updatedAt = Date.now();
|
|
173
191
|
await this.host.runLifecycleHook(record, cwd, "SubagentStop", record.status, record.error, options.abortSignal);
|
|
174
192
|
emit(record.status, undefined, record.error);
|
|
@@ -178,7 +196,49 @@ export class ChildRunner {
|
|
|
178
196
|
}
|
|
179
197
|
record.summary = sanitizeSubagentSummary(record.summary);
|
|
180
198
|
if (needsExplicitFinalSummary(record, executedAnyTool)) {
|
|
181
|
-
|
|
199
|
+
try {
|
|
200
|
+
await this.runFinalSummaryTurn(record, subAgent, runCwd, options.abortSignal, emit);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const abortedNow = record.abortController.signal.aborted
|
|
204
|
+
|| options.abortSignal?.aborted
|
|
205
|
+
|| error instanceof AgentAbortError
|
|
206
|
+
|| error?.name === "AbortError";
|
|
207
|
+
if (isRateLimitError(error) && !abortedNow) {
|
|
208
|
+
record.status = "queued";
|
|
209
|
+
record.updatedAt = Date.now();
|
|
210
|
+
stripTrailingModelInterruptedBoundary(subAgent.messages);
|
|
211
|
+
emit("queued", undefined, `Rate limited; ${record.nickname} will retry with its context intact.`);
|
|
212
|
+
return { kind: "rate_limited", retryAfterMs: error.retryAfterMs };
|
|
213
|
+
}
|
|
214
|
+
if (isProviderTransportError(error) && !abortedNow) {
|
|
215
|
+
record.status = "queued";
|
|
216
|
+
record.updatedAt = Date.now();
|
|
217
|
+
stripTrailingModelInterruptedBoundary(subAgent.messages);
|
|
218
|
+
emit("queued", undefined, `Connection error; ${record.nickname} will retry with its context intact.`);
|
|
219
|
+
return { kind: "transport_retry" };
|
|
220
|
+
}
|
|
221
|
+
const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
|
|
222
|
+
record.status = cancelled ? "cancelled" : "failed";
|
|
223
|
+
record.finalReason = cancelled
|
|
224
|
+
? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal)
|
|
225
|
+
: "failed_transient";
|
|
226
|
+
if (!cancelled) {
|
|
227
|
+
this.host.recordProviderError(record, error, {
|
|
228
|
+
messageCount: messagesBeforeInterruptedBoundary(subAgent.messages),
|
|
229
|
+
toolCount: tools.length,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
record.error = cancelled
|
|
233
|
+
? "Subagent was cancelled."
|
|
234
|
+
: sanitizeProviderErrorText(error?.message || String(error));
|
|
235
|
+
record.updatedAt = Date.now();
|
|
236
|
+
await this.host.runLifecycleHook(record, cwd, "SubagentStop", record.status, record.error, options.abortSignal);
|
|
237
|
+
emit(record.status, undefined, record.error);
|
|
238
|
+
this.host.notifyWaiters(record);
|
|
239
|
+
this.host.onFinal(record, options);
|
|
240
|
+
return { kind: "final" };
|
|
241
|
+
}
|
|
182
242
|
}
|
|
183
243
|
record.status = "completed";
|
|
184
244
|
record.finalReason = "completed";
|
|
@@ -289,6 +349,10 @@ export function stripTrailingModelInterruptedBoundary(messages) {
|
|
|
289
349
|
break;
|
|
290
350
|
}
|
|
291
351
|
}
|
|
352
|
+
function messagesBeforeInterruptedBoundary(messages) {
|
|
353
|
+
const last = messages.at(-1);
|
|
354
|
+
return Math.max(0, messages.length - (last?.role === "assistant" && last.content.startsWith("[model request interrupted") ? 1 : 0));
|
|
355
|
+
}
|
|
292
356
|
export function summarizeSubagentToolEnd(event) {
|
|
293
357
|
const metadata = (event.result.metadata ?? {});
|
|
294
358
|
const reason = readString(metadata.reason);
|
|
@@ -4,6 +4,7 @@ import { SubagentStore } from "../subagent-store.js";
|
|
|
4
4
|
import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "../categories.js";
|
|
5
5
|
import type { SubagentRouter } from "./router.js";
|
|
6
6
|
import type { AgentEvent, ContentPart, Message, PermissionMode, Provider, ThinkingLevel, ToolRegistryEntry, ToolUpdate } from "../../types.js";
|
|
7
|
+
import type { ProviderErrorContext } from "../../provider-error-record.js";
|
|
7
8
|
import type { AgentSubagentRuntimeConfig } from "../../agent.js";
|
|
8
9
|
import type { HookCombinedResult } from "../../hooks/index.js";
|
|
9
10
|
/** The child agent surface the runtime drives; structurally satisfied by Agent. */
|
|
@@ -44,6 +45,7 @@ export interface SubagentRuntimeParent {
|
|
|
44
45
|
*/
|
|
45
46
|
allTools(): ToolRegistryEntry[];
|
|
46
47
|
createChild(spec: ChildAgentSpec): ChildAgentLike;
|
|
48
|
+
recordProviderError(error: unknown, context: ProviderErrorContext): void;
|
|
47
49
|
runExternalHook(input: {
|
|
48
50
|
eventName: string;
|
|
49
51
|
cwd: string;
|
|
@@ -185,6 +187,7 @@ export declare class SubagentRuntime {
|
|
|
185
187
|
private maybeEnqueueIngestion;
|
|
186
188
|
private finalizeSubagentBlocked;
|
|
187
189
|
private dispatchSubagentRun;
|
|
190
|
+
private recordSubagentProviderError;
|
|
188
191
|
private emitSubagentLifecycle;
|
|
189
192
|
/** Lets a blocked tool-execution loop drain freshly queued subagent updates. */
|
|
190
193
|
private wakeSubagentUpdateWaiters;
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* `SubagentRuntimeParent` as a LIVE accessor, never a construction-time
|
|
12
12
|
* snapshot: `/model` reassigns the parent's provider/model/thinking
|
|
13
13
|
* mid-session and children must inherit the current values.
|
|
14
|
-
* -
|
|
15
|
-
* notices are PULLED (`drainToolUpdates` /
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - Conversation state is never pushed back into the parent. Tool updates
|
|
15
|
+
* and ingestion notices are PULLED (`drainToolUpdates` /
|
|
16
|
+
* `drainIngestionNotices`). The only direct sink is a terminal,
|
|
17
|
+
* fail-closed provider diagnostic that contains no child transcript.
|
|
18
18
|
*
|
|
19
19
|
* `new Agent(...)` stays on Agent, reached through `parent.createChild`. That
|
|
20
20
|
* is the only genuine call back into the parent, and keeping it there is what
|
|
@@ -31,7 +31,7 @@ import { assignAgentNickname, discoverAgentProfiles, findAgentProfile, validateA
|
|
|
31
31
|
import { snapshotSubagentThread, subagentResultFromThread } from "../subagent-control.js";
|
|
32
32
|
import { SubagentStore } from "../subagent-store.js";
|
|
33
33
|
import { SubagentScheduler } from "../subagent-scheduler.js";
|
|
34
|
-
import { ChildRunner, classifySubagentAbortReason } from "../child-runner.js";
|
|
34
|
+
import { ChildRunner, classifySubagentAbortReason, SafeSubagentConfigurationError, } from "../child-runner.js";
|
|
35
35
|
import { ResultIntegrator } from "../result-integrator.js";
|
|
36
36
|
import { SubagentAbortError } from "../abort-errors.js";
|
|
37
37
|
import { createSubagentWorktree, finalizeSubagentWorktree } from "../worktree.js";
|
|
@@ -75,6 +75,7 @@ export class SubagentRuntime {
|
|
|
75
75
|
runLifecycleHook: (record, cwd, eventName, status, error, abortSignal) => this.runSubagentLifecycleHookFor(record, cwd, eventName, status, error, abortSignal),
|
|
76
76
|
finalizeBlocked: (record, error, options) => this.finalizeSubagentBlocked(record, error, options),
|
|
77
77
|
createInstance: (record, tools, cwd, forkContext) => this.createSubAgentInstance(record, tools, cwd, forkContext),
|
|
78
|
+
recordProviderError: (record, error, request) => this.recordSubagentProviderError(record, error, request),
|
|
78
79
|
notifyWaiters: (record) => this.store.notifyWaiters(record),
|
|
79
80
|
onFinal: (record, options) => {
|
|
80
81
|
this.reclaimWorktree(record);
|
|
@@ -506,7 +507,7 @@ export class SubagentRuntime {
|
|
|
506
507
|
onCancelledWhileQueued: (reason) => {
|
|
507
508
|
record.status = "cancelled";
|
|
508
509
|
record.finalReason = classifySubagentAbortReason(reason, options.abortSignal);
|
|
509
|
-
record.error =
|
|
510
|
+
record.error = "Subagent was cancelled.";
|
|
510
511
|
record.updatedAt = Date.now();
|
|
511
512
|
// This path is NOT only "run never started" — a 429/transport
|
|
512
513
|
// failure re-queues the entry with the abort listener re-armed, so
|
|
@@ -527,6 +528,7 @@ export class SubagentRuntime {
|
|
|
527
528
|
record.status = "failed";
|
|
528
529
|
record.finalReason = "rate_limited_exhausted";
|
|
529
530
|
record.error = `Provider rate limit persisted after ${attempts} attempts.`;
|
|
531
|
+
this.recordSubagentProviderError(record, Object.assign(new Error(record.error), { status: 429, code: "rate_limit_exhausted" }), { messageCount: record.agent?.messages.length ?? 0, toolCount: 0 });
|
|
530
532
|
record.updatedAt = Date.now();
|
|
531
533
|
// Reclaim before persist so the handoff note lands in the
|
|
532
534
|
// persisted toolNotes.
|
|
@@ -543,6 +545,10 @@ export class SubagentRuntime {
|
|
|
543
545
|
// to recover the child with its context intact.
|
|
544
546
|
record.finalReason = "failed_transient";
|
|
545
547
|
record.error = `Provider transport error persisted after ${attempts} attempts.`;
|
|
548
|
+
this.recordSubagentProviderError(record, new Error(record.error), {
|
|
549
|
+
messageCount: record.agent?.messages.length ?? 0,
|
|
550
|
+
toolCount: 0,
|
|
551
|
+
});
|
|
546
552
|
record.updatedAt = Date.now();
|
|
547
553
|
this.reclaimWorktree(record);
|
|
548
554
|
void this.runSubagentLifecycleHookFor(record, cwd, "SubagentStop", record.status, record.error);
|
|
@@ -553,6 +559,22 @@ export class SubagentRuntime {
|
|
|
553
559
|
},
|
|
554
560
|
});
|
|
555
561
|
}
|
|
562
|
+
recordSubagentProviderError(record, error, request) {
|
|
563
|
+
const route = record.route ?? {
|
|
564
|
+
providerId: this.parent.providerId,
|
|
565
|
+
model: this.parent.apiModel,
|
|
566
|
+
thinkingLevel: this.parent.thinkingLevel,
|
|
567
|
+
inherited: true,
|
|
568
|
+
};
|
|
569
|
+
this.parent.recordProviderError(error, {
|
|
570
|
+
providerId: route.providerId,
|
|
571
|
+
modelId: route.model,
|
|
572
|
+
model: route.providerId ? `${route.providerId}:${route.model}` : route.model,
|
|
573
|
+
thinkingLevel: route.thinkingLevel,
|
|
574
|
+
messageCount: request.messageCount,
|
|
575
|
+
toolCount: request.toolCount,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
556
578
|
emitSubagentLifecycle(record, options, status, event, message) {
|
|
557
579
|
const update = this.buildSubagentUpdate(record, status, event, message);
|
|
558
580
|
options.directEmit?.(update);
|
|
@@ -633,7 +655,15 @@ export class SubagentRuntime {
|
|
|
633
655
|
// NOTE: record.worktree must be assigned before anything below can
|
|
634
656
|
// throw — ChildRunner's failure path reclaims it off the record.
|
|
635
657
|
if (!record.worktree) {
|
|
636
|
-
|
|
658
|
+
try {
|
|
659
|
+
record.worktree = createSubagentWorktree(cwd, record.agentId);
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
const message = error instanceof Error && error.message.includes("need a git repository")
|
|
663
|
+
? "write_worktree subagents need a git repository: the working directory is not inside one."
|
|
664
|
+
: "Failed to create subagent worktree.";
|
|
665
|
+
throw new SafeSubagentConfigurationError(message);
|
|
666
|
+
}
|
|
637
667
|
}
|
|
638
668
|
childCwd = record.worktree.path;
|
|
639
669
|
childMode = "default";
|
|
@@ -705,7 +735,7 @@ export class SubagentRuntime {
|
|
|
705
735
|
return this.parent.provider;
|
|
706
736
|
}
|
|
707
737
|
if (!this.parent.providerFactory) {
|
|
708
|
-
throw new
|
|
738
|
+
throw new SafeSubagentConfigurationError([
|
|
709
739
|
`Subagent route requires provider "${route.providerId}" for model "${route.model}",`,
|
|
710
740
|
`but the parent agent only has provider "${this.parent.providerId || "none"}" and no provider factory is configured.`,
|
|
711
741
|
].join(" "));
|
|
@@ -8,7 +8,7 @@ export declare function findMissingRequiredArgs(schema: {
|
|
|
8
8
|
required?: string[];
|
|
9
9
|
} | undefined, args: Record<string, any> | undefined): string[];
|
|
10
10
|
export declare function shouldAppendModelInterruptedBoundary(messages: Message[]): boolean;
|
|
11
|
-
export declare function createModelInterruptedMessage(
|
|
11
|
+
export declare function createModelInterruptedMessage(_error: unknown, metadata: {
|
|
12
12
|
model: string;
|
|
13
13
|
providerId: string;
|
|
14
14
|
modelId: string;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { summarizeInterruptError } from "./abort-errors.js";
|
|
2
1
|
export function findMissingRequiredArgs(schema, args) {
|
|
3
2
|
const required = schema?.required;
|
|
4
3
|
if (!required || required.length === 0)
|
|
@@ -19,10 +18,12 @@ export function findMissingRequiredArgs(schema, args) {
|
|
|
19
18
|
export function shouldAppendModelInterruptedBoundary(messages) {
|
|
20
19
|
return messages.at(-1)?.role === "tool";
|
|
21
20
|
}
|
|
22
|
-
export function createModelInterruptedMessage(
|
|
21
|
+
export function createModelInterruptedMessage(_error, metadata) {
|
|
23
22
|
return {
|
|
24
23
|
role: "assistant",
|
|
25
|
-
|
|
24
|
+
// This boundary is replayed to future providers and memory extraction.
|
|
25
|
+
// Never copy provider error text into conversational history.
|
|
26
|
+
content: "[model request interrupted before a final answer was produced]",
|
|
26
27
|
model: metadata.model,
|
|
27
28
|
providerId: metadata.providerId,
|
|
28
29
|
modelId: metadata.modelId,
|
package/dist/agent.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { createUpdateQueue } from "./agent/update-queue.js";
|
|
|
18
18
|
import { type RateLimitPolicy } from "./network/errors.js";
|
|
19
19
|
import type { SkillSummary } from "./skills/types.js";
|
|
20
20
|
import type { FileStateTracker } from "./tools/file-state.js";
|
|
21
|
+
import { type SanitizedProviderError } from "./provider-error-record.js";
|
|
21
22
|
export { AgentAbortError, SubagentAbortError } from "./agent/abort-errors.js";
|
|
22
23
|
export declare const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
|
|
23
24
|
/** Runtime tuning for the subagent scheduler. */
|
|
@@ -51,6 +52,8 @@ export interface AgentOptions {
|
|
|
51
52
|
};
|
|
52
53
|
systemPrompt?: string;
|
|
53
54
|
onMessageAppend?: (message: Message) => void;
|
|
55
|
+
/** Persistable, allowlisted diagnostics for terminal provider failures. */
|
|
56
|
+
onProviderError?: (error: SanitizedProviderError) => void;
|
|
54
57
|
onToolResult?: (toolName: string, result: ToolResult) => void;
|
|
55
58
|
onModeUpdate?: (mode: PermissionMode) => void;
|
|
56
59
|
/**
|
|
@@ -157,6 +160,7 @@ export declare class Agent {
|
|
|
157
160
|
private onModeUpdate?;
|
|
158
161
|
private onCompactionApplied?;
|
|
159
162
|
private onMessageAppend?;
|
|
163
|
+
private onProviderError?;
|
|
160
164
|
private onToolResult?;
|
|
161
165
|
private hookDefinitions;
|
|
162
166
|
private externalHooks?;
|
|
@@ -440,6 +444,8 @@ export declare class Agent {
|
|
|
440
444
|
*/
|
|
441
445
|
private persistCompactionSummary;
|
|
442
446
|
private appendMessage;
|
|
447
|
+
private persistProviderError;
|
|
448
|
+
private recordProviderError;
|
|
443
449
|
private appendInterruptedAssistantBoundary;
|
|
444
450
|
private executeTool;
|
|
445
451
|
}
|
package/dist/agent.js
CHANGED
|
@@ -32,6 +32,7 @@ import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMeta
|
|
|
32
32
|
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
33
33
|
import { buildToolPromptOptions } from "./tools/prompt-metadata.js";
|
|
34
34
|
import { stopAutoServersForSession } from "./tools/server-manager.js";
|
|
35
|
+
import { createSanitizedProviderError, } from "./provider-error-record.js";
|
|
35
36
|
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceMessage, summarizeTraceToolResult, summarizeTraceValue, traceEvent, } from "./debug-trace.js";
|
|
36
37
|
const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 2;
|
|
37
38
|
const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
|
|
@@ -127,6 +128,7 @@ export class Agent {
|
|
|
127
128
|
onModeUpdate;
|
|
128
129
|
onCompactionApplied;
|
|
129
130
|
onMessageAppend;
|
|
131
|
+
onProviderError;
|
|
130
132
|
onToolResult;
|
|
131
133
|
hookDefinitions;
|
|
132
134
|
externalHooks;
|
|
@@ -174,6 +176,7 @@ export class Agent {
|
|
|
174
176
|
this.thinkingLevel = options.thinkingLevel ?? "off";
|
|
175
177
|
this._mode = options.mode ?? "default";
|
|
176
178
|
this.onMessageAppend = options.onMessageAppend;
|
|
179
|
+
this.onProviderError = options.onProviderError;
|
|
177
180
|
this.onToolResult = options.onToolResult;
|
|
178
181
|
this.onModeUpdate = options.onModeUpdate;
|
|
179
182
|
this.onCompactionApplied = options.onCompactionApplied;
|
|
@@ -228,6 +231,7 @@ export class Agent {
|
|
|
228
231
|
// profile is the author pre-unlocking them).
|
|
229
232
|
allTools: () => [...this.tools.values()],
|
|
230
233
|
createChild: (spec) => this.createChildAgent(spec),
|
|
234
|
+
recordProviderError: (error, context) => this.recordProviderError(error, context),
|
|
231
235
|
runExternalHook: (input, abortSignal) => this.runExternalHook(input, abortSignal),
|
|
232
236
|
},
|
|
233
237
|
router: this.router,
|
|
@@ -826,6 +830,11 @@ export class Agent {
|
|
|
826
830
|
parameters: t.parameters,
|
|
827
831
|
}));
|
|
828
832
|
const toolDefinitionTokens = estimateToolDefinitionsTokens(toolDefinitions, this.providerId);
|
|
833
|
+
let currentRequestMessageCount = this.messages.length;
|
|
834
|
+
const persistTerminalProviderError = (error) => this.persistProviderError(error, {
|
|
835
|
+
messageCount: currentRequestMessageCount,
|
|
836
|
+
toolCount: toolDefinitions.length,
|
|
837
|
+
});
|
|
829
838
|
// LLM-driven compaction runs ahead of projector's algorithmic passes. If
|
|
830
839
|
// it succeeds, this.messages is replaced with [preserved system+meta] +
|
|
831
840
|
// [LLM summary] + [last user msg], and the projector becomes a no-op for
|
|
@@ -845,6 +854,7 @@ export class Agent {
|
|
|
845
854
|
// A fresh estimate also lets us account for current tool schemas.
|
|
846
855
|
additionalInputTokens: toolDefinitionTokens,
|
|
847
856
|
});
|
|
857
|
+
currentRequestMessageCount = projectedMessages.length;
|
|
848
858
|
const requestBudget = getContextBudget(this.providerId, this.apiModel, projectedMessages, { additionalInputTokens: toolDefinitionTokens });
|
|
849
859
|
currentRequestEstimate = requestBudget.estimatedTokens;
|
|
850
860
|
const maxInputTokens = getMaxInputTokens(requestBudget.contextWindow);
|
|
@@ -1043,6 +1053,8 @@ export class Agent {
|
|
|
1043
1053
|
error: summarizeTraceError(error),
|
|
1044
1054
|
}, traceContext);
|
|
1045
1055
|
if (assistantAppended) {
|
|
1056
|
+
if (!isAbortLikeError(error, abortSignal))
|
|
1057
|
+
persistTerminalProviderError(error);
|
|
1046
1058
|
throw error;
|
|
1047
1059
|
}
|
|
1048
1060
|
const streamInterruption = isProviderStreamInterruption(error) ? error : undefined;
|
|
@@ -1077,9 +1089,12 @@ export class Agent {
|
|
|
1077
1089
|
}));
|
|
1078
1090
|
assistantAppended = true;
|
|
1079
1091
|
}
|
|
1092
|
+
if (!isAbortLikeError(error, abortSignal))
|
|
1093
|
+
persistTerminalProviderError(error);
|
|
1080
1094
|
throw error;
|
|
1081
1095
|
}
|
|
1082
1096
|
if (consecutiveOverflowRecoveries >= MAX_CONSECUTIVE_OVERFLOW_RECOVERIES) {
|
|
1097
|
+
persistTerminalProviderError(error);
|
|
1083
1098
|
throw error;
|
|
1084
1099
|
}
|
|
1085
1100
|
const messagesBeforeRecovery = this.messages;
|
|
@@ -1108,6 +1123,7 @@ export class Agent {
|
|
|
1108
1123
|
this.messages = messagesBeforeRecovery;
|
|
1109
1124
|
this.lastInputTokens = inputTokensBeforeRecovery;
|
|
1110
1125
|
this.lastAnchorMessageCount = anchorBeforeRecovery;
|
|
1126
|
+
persistTerminalProviderError(error);
|
|
1111
1127
|
throw error;
|
|
1112
1128
|
}
|
|
1113
1129
|
consecutiveOverflowRecoveries += 1;
|
|
@@ -1983,6 +1999,26 @@ export class Agent {
|
|
|
1983
1999
|
this.onMessageAppend?.(message);
|
|
1984
2000
|
this.notifyContextChanged();
|
|
1985
2001
|
}
|
|
2002
|
+
persistProviderError(error, request) {
|
|
2003
|
+
this.recordProviderError(error, {
|
|
2004
|
+
providerId: this.providerId,
|
|
2005
|
+
modelId: this.apiModel,
|
|
2006
|
+
model: this._model,
|
|
2007
|
+
thinkingLevel: this.thinkingLevel,
|
|
2008
|
+
messageCount: request.messageCount,
|
|
2009
|
+
toolCount: request.toolCount,
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
recordProviderError(error, context) {
|
|
2013
|
+
if (!this.onProviderError)
|
|
2014
|
+
return;
|
|
2015
|
+
try {
|
|
2016
|
+
this.onProviderError(createSanitizedProviderError(error, context));
|
|
2017
|
+
}
|
|
2018
|
+
catch {
|
|
2019
|
+
// Diagnostics are best-effort and must never replace the provider error.
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
1986
2022
|
appendInterruptedAssistantBoundary(currentAssistant, currentAssistantAppended) {
|
|
1987
2023
|
const last = lastProviderMessage(this.messages);
|
|
1988
2024
|
if (last?.role === "assistant" && last.error?.aborted) {
|
package/dist/cli.d.ts
CHANGED
|
@@ -12,7 +12,8 @@ export interface CliArgs {
|
|
|
12
12
|
sessionName?: string;
|
|
13
13
|
print?: boolean;
|
|
14
14
|
/** Print-mode output: plain (default) or a single JSON object on stdout. */
|
|
15
|
-
outputFormat?: "plain" | "json";
|
|
15
|
+
outputFormat?: "plain" | "json" | "stream-json";
|
|
16
|
+
inputFormat?: "text" | "stream-json";
|
|
16
17
|
prompt?: string;
|
|
17
18
|
thinkingLevel?: ThinkingLevel;
|
|
18
19
|
mode?: PermissionMode;
|
package/dist/cli.js
CHANGED
|
@@ -57,13 +57,21 @@ export function parseArgs(argv) {
|
|
|
57
57
|
break;
|
|
58
58
|
case "--output-format": {
|
|
59
59
|
const value = argv[++i];
|
|
60
|
-
if (value !== "plain" && value !== "json") {
|
|
61
|
-
console.error(`Invalid --output-format: ${value ?? "(missing)"}. Expected plain or json.`);
|
|
60
|
+
if (value !== "plain" && value !== "json" && value !== "stream-json") {
|
|
61
|
+
console.error(`Invalid --output-format: ${value ?? "(missing)"}. Expected plain, json, or stream-json.`);
|
|
62
62
|
process.exit(1);
|
|
63
63
|
}
|
|
64
64
|
args.outputFormat = value;
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
67
|
+
case "--input-format": {
|
|
68
|
+
const value = argv[++i];
|
|
69
|
+
if (value !== "text" && value !== "stream-json") {
|
|
70
|
+
throw new Error("--input-format expects text or stream-json");
|
|
71
|
+
}
|
|
72
|
+
args.inputFormat = value;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
67
75
|
case "--plan":
|
|
68
76
|
args.mode = "plan";
|
|
69
77
|
break;
|
|
@@ -96,6 +104,16 @@ export function parseArgs(argv) {
|
|
|
96
104
|
break;
|
|
97
105
|
}
|
|
98
106
|
}
|
|
107
|
+
const streamingInput = args.inputFormat === "stream-json";
|
|
108
|
+
const streamingOutput = args.outputFormat === "stream-json";
|
|
109
|
+
if (streamingInput || streamingOutput) {
|
|
110
|
+
if (!streamingInput || !streamingOutput || !args.print || args.prompt || args.command !== "default") {
|
|
111
|
+
throw new Error("Persistent mode requires -p --input-format stream-json --output-format stream-json and prompts on stdin");
|
|
112
|
+
}
|
|
113
|
+
if (args.resume && !args.sessionName) {
|
|
114
|
+
throw new Error("Persistent mode requires --session when using --resume (no interactive picker)");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
99
117
|
return args;
|
|
100
118
|
}
|
|
101
119
|
export function printHelp() {
|
|
@@ -117,8 +135,9 @@ Options (default):
|
|
|
117
135
|
--dangerously-skip-permissions
|
|
118
136
|
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
|
119
137
|
-p, --print Non-interactive mode (single prompt)
|
|
120
|
-
--output-format <fmt> Print-mode output: plain (default) or json
|
|
138
|
+
--output-format <fmt> Print-mode output: plain (default), json, or stream-json
|
|
121
139
|
(one JSON object on stdout: text, usage, num_turns)
|
|
140
|
+
--input-format <fmt> text (default) or stream-json; use both stream-json flags with -p\n for a persistent session (one user JSON message per line)
|
|
122
141
|
-v, --version Print the installed version and exit
|
|
123
142
|
-h, --help Show this help
|
|
124
143
|
|
package/dist/context/budget.js
CHANGED
|
@@ -36,8 +36,11 @@ export function estimateMessageTokens(message, providerId) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function estimateProviderMetadataOverhead(metadata, providerId) {
|
|
39
|
-
const blocks =
|
|
40
|
-
|
|
39
|
+
const blocks = [
|
|
40
|
+
...(metadata?.anthropic?.contentBlocks ?? []),
|
|
41
|
+
...(metadata?.openai?.contentBlocks ?? []),
|
|
42
|
+
];
|
|
43
|
+
if (blocks.length === 0)
|
|
41
44
|
return 0;
|
|
42
45
|
const estimate = (text) => estimateTextTokens(text, providerId);
|
|
43
46
|
return blocks.reduce((sum, block) => {
|
|
@@ -46,6 +49,8 @@ function estimateProviderMetadataOverhead(metadata, providerId) {
|
|
|
46
49
|
overhead += estimate(block.signature);
|
|
47
50
|
if (block.type === "redacted_thinking" && typeof block.data === "string")
|
|
48
51
|
overhead += estimate(block.data);
|
|
52
|
+
if (block.type === "reasoning" && typeof block.encrypted_content === "string")
|
|
53
|
+
overhead += estimate(block.encrypted_content);
|
|
49
54
|
return sum + overhead;
|
|
50
55
|
}, 0);
|
|
51
56
|
}
|
package/dist/context/compact.js
CHANGED
|
@@ -118,8 +118,14 @@ export function buildCompactionSummaryMessage(summary) {
|
|
|
118
118
|
}
|
|
119
119
|
export function planSessionCompaction(entries, options = {}) {
|
|
120
120
|
const keepRecentTurns = options.keepRecentTurns ?? 2;
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// Provider errors are diagnostics, not model context. Keep the latest audit
|
|
122
|
+
// records across compaction without allowing repeated failures to grow the
|
|
123
|
+
// compacted session forever.
|
|
124
|
+
const metadataEntries = [
|
|
125
|
+
...entries.filter((entry) => entry.type === "metadata"),
|
|
126
|
+
...entries.filter((entry) => entry.type === "provider_error").slice(-20),
|
|
127
|
+
];
|
|
128
|
+
const nonMetadataEntries = entries.filter((entry) => entry.type !== "metadata" && entry.type !== "provider_error");
|
|
123
129
|
const latestSummaryIndex = findLatestSummaryIndex(nonMetadataEntries);
|
|
124
130
|
const baseIndex = latestSummaryIndex >= 0 ? latestSummaryIndex + 1 : 0;
|
|
125
131
|
const activeEntries = nonMetadataEntries.slice(baseIndex);
|
|
@@ -130,6 +130,9 @@ export class RunDriver {
|
|
|
130
130
|
recordMemoryCitations(session.cwd, message.content);
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
|
+
onProviderError: (error) => {
|
|
134
|
+
session.manager.appendProviderError(error);
|
|
135
|
+
},
|
|
133
136
|
onCompactionApplied: (summary) => {
|
|
134
137
|
session.manager.applyLLMCompaction(summary);
|
|
135
138
|
},
|
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { SkillRegistry } from "./skills/registry.js";
|
|
|
20
20
|
import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
|
|
21
21
|
import { getProcessManager } from "./tasks/manager.js";
|
|
22
22
|
import { PromotionChannel } from "./tasks/promotion.js";
|
|
23
|
+
import { runStreamJsonSession } from "./stream-json.js";
|
|
23
24
|
import { PrintRunCollector, formatPrintJson, formatPrintJsonError } from "./print-output.js";
|
|
24
25
|
import { FileStateTracker } from "./tools/file-state.js";
|
|
25
26
|
import { GoalStore } from "./goal/store.js";
|
|
@@ -418,6 +419,9 @@ async function main() {
|
|
|
418
419
|
recordMemoryCitations(args.cwd, message.content);
|
|
419
420
|
}
|
|
420
421
|
},
|
|
422
|
+
onProviderError: (error) => {
|
|
423
|
+
sessionManager?.appendProviderError(error);
|
|
424
|
+
},
|
|
421
425
|
// Auto-compaction summaries are meta messages (dropped above); persist the
|
|
422
426
|
// compacted state as a session summary entry so it survives resume.
|
|
423
427
|
onCompactionApplied: (summary) => {
|
|
@@ -556,7 +560,7 @@ async function main() {
|
|
|
556
560
|
}
|
|
557
561
|
agent.injectDeferredToolsReminder();
|
|
558
562
|
// JSON print mode reserves stdout for the single result object.
|
|
559
|
-
(args.outputFormat === "json" ? console.error : console.log)(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
|
|
563
|
+
(args.outputFormat === "json" || args.outputFormat === "stream-json" ? console.error : console.log)(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
|
|
560
564
|
}
|
|
561
565
|
}
|
|
562
566
|
try {
|
|
@@ -567,6 +571,17 @@ async function main() {
|
|
|
567
571
|
process.exitCode = 1;
|
|
568
572
|
return;
|
|
569
573
|
}
|
|
574
|
+
if (args.inputFormat === "stream-json") {
|
|
575
|
+
await runStreamJsonSession({
|
|
576
|
+
input: process.stdin,
|
|
577
|
+
write: (message) => { process.stdout.write(JSON.stringify(message) + "\n"); },
|
|
578
|
+
sessionId: basename(sessionManager.getSessionFile()),
|
|
579
|
+
model: agent.model,
|
|
580
|
+
run: (prompt) => agent.run(prompt, args.cwd),
|
|
581
|
+
onEvent: (event) => traceEvent("print_agent_event", summarizeAgentEventForTrace(event)),
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
570
585
|
const prompt = args.prompt || (await readPipedStdin()) || "";
|
|
571
586
|
if (!prompt) {
|
|
572
587
|
console.error(chalk.red("Error: No prompt provided."));
|