@bubblebrain-ai/bubble 0.0.56 → 0.0.58

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.
@@ -27,7 +27,7 @@ export declare class StdioTransport implements McpTransport {
27
27
  constructor(config: StdioServerConfig);
28
28
  start(): Promise<void>;
29
29
  private onStdout;
30
- send(message: JsonRpcRequest | JsonRpcNotification): Promise<void>;
30
+ send(message: JsonRpcRequest | JsonRpcNotification | JsonRpcResponse): Promise<void>;
31
31
  onMessage(handler: IncomingHandler): void;
32
32
  onError(handler: (err: Error) => void): void;
33
33
  onClose(handler: () => void): void;
@@ -44,7 +44,7 @@ export declare class HttpTransport implements McpTransport {
44
44
  private readonly baseHeaders;
45
45
  constructor(config: HttpServerConfig | SseServerConfig);
46
46
  start(): Promise<void>;
47
- send(message: JsonRpcRequest | JsonRpcNotification): Promise<void>;
47
+ send(message: JsonRpcRequest | JsonRpcNotification | JsonRpcResponse): Promise<void>;
48
48
  private parseSseBody;
49
49
  onMessage(handler: IncomingHandler): void;
50
50
  onError(handler: (err: Error) => void): void;
@@ -100,7 +100,7 @@ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcRespo
100
100
  /** Low-level transport interface. */
101
101
  export interface McpTransport {
102
102
  start(): Promise<void>;
103
- send(message: JsonRpcRequest | JsonRpcNotification): Promise<void>;
103
+ send(message: JsonRpcRequest | JsonRpcNotification | JsonRpcResponse): Promise<void>;
104
104
  /** Called once per incoming JSON-RPC message (response or server→client request/notification). */
105
105
  onMessage(handler: (msg: JsonRpcResponse | JsonRpcNotification | JsonRpcRequest) => void): void;
106
106
  onError(handler: (err: Error) => void): void;
@@ -160,6 +160,10 @@ function serializeSessionEntry(entry) {
160
160
  return `[tool_call:${entry.toolCall.name}] ${truncate(entry.toolCall.arguments, 1_500)}`;
161
161
  case "tool_result":
162
162
  return `[tool_result${entry.message.isError ? " error=true" : ""}] ${truncate(entry.message.content, 2_000)}`;
163
+ case "provider_error":
164
+ // Operational diagnostics stay out of the memory-extraction prompt. The
165
+ // structured session record remains available for local debugging.
166
+ return "";
163
167
  }
164
168
  }
165
169
  function countMeaningfulEntries(entries) {
@@ -25,6 +25,7 @@ export const BUILTIN_PROVIDERS = [
25
25
  { id: "minimax", name: "MiniMax Token Plan", baseURL: "https://api.minimaxi.com/anthropic", protocol: "anthropic-messages" },
26
26
  { id: "minimax-anthropic", name: "MiniMax API", baseURL: "https://api.minimaxi.com/anthropic", protocol: "anthropic-messages" },
27
27
  { id: "stepfun", name: "StepFun Step Plan", baseURL: "https://api.stepfun.com/step_plan/v1" },
28
+ { id: "stepfun-api", name: "StepFun API", baseURL: "https://api.stepfun.com/v1", protocol: "openai-chat" },
28
29
  { id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
29
30
  { id: "moonshot-intl", name: "Moonshot (海外 platform.moonshot.ai)", baseURL: "https://api.moonshot.ai/v1" },
30
31
  { id: "kimi-for-coding", name: "Kimi for Coding", baseURL: "https://api.kimi.com/coding/v1" },
@@ -127,9 +128,11 @@ export const BUILTIN_MODELS = [
127
128
  { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providerId: "anthropic", tier: "balanced", reasoningLevels: ANTHROPIC_SONNET_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
128
129
  { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providerId: "anthropic", tier: "fast", reasoningLevels: ANTHROPIC_CHAT_LEVELS, contextWindow: 200000 },
129
130
  { id: "deepseek-v4-flash", name: "deepseek-v4-flash", providerId: "deepseek", tier: "fast", reasoningLevels: DEEPSEEK_V4_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
130
- // Experimental vision model stays explicit-only: it is text-capable, but
131
- // automatic fast-tier routing must not silently switch ordinary subagents
132
- // from the stable Flash model to an experimental multimodal endpoint.
131
+ // V4.1 Flash (2026-09-10): high is the default, not the only effort.
132
+ // https://api-docs.deepseek.com/guides/thinking_mode
133
+ { id: "deepseek-flash", name: "deepseek-flash", providerId: "deepseek", reasoningLevels: DEEPSEEK_V4_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
134
+ // Legacy vision alias now routes to V4.1 Flash. Keep its explicit-only
135
+ // routing policy; existing sessions can continue using the old model ID.
133
136
  { id: "deepseek-v4-flash-vision-exp", name: "DeepSeek-V4-Flash-Vision-Exp", providerId: "deepseek", reasoningLevels: DEEPSEEK_V4_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
134
137
  { id: "deepseek-v4-pro", name: "deepseek-v4-pro", providerId: "deepseek", tier: "strong", reasoningLevels: DEEPSEEK_V4_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
135
138
  // Offline/no-key fallback only: with an API key the registry replaces this
@@ -190,6 +193,8 @@ export const BUILTIN_MODELS = [
190
193
  { id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
191
194
  { id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
192
195
  { id: "step-router-v1", name: "Step Router V1", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
196
+ // Use server-default reasoning until this model's effort controls are documented.
197
+ { id: "water18-0910", name: "water18-0910", providerId: "stepfun-api", reasoningLevels: [] },
193
198
  { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "moonshot-cn", tier: "strong", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
194
199
  { id: "kimi-k2.7-code-highspeed", name: "Kimi K2.7 Code Highspeed", providerId: "moonshot-cn", tier: "fast", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
195
200
  { id: "kimi-k3", name: "Kimi K3", providerId: "moonshot-cn", tier: "strong", reasoningLevels: KIMI_K3_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
@@ -19,23 +19,27 @@ export interface UtcPricingWindow {
19
19
  /** Exclusive minute since 00:00 UTC. */
20
20
  endMinute: number;
21
21
  }
22
- export interface ModelPricing extends ModelPricingRate {
23
- providerId: string;
24
- modelId: string;
25
- currency: PricingCurrency;
22
+ export interface ModelPricingTariff extends ModelPricingRate {
26
23
  /**
27
24
  * Optional recurring UTC time-of-use tariff. Top-level rates are the peak
28
25
  * tariff; outside these windows, `offPeak` applies. DeepSeek introduced this
29
26
  * pricing shape for the V4 family on 2026-08-16.
30
27
  */
31
28
  peakWindowsUtc?: UtcPricingWindow[];
29
+ /** UTC weekdays with peak windows (0 = Sunday). Omitted means every day. */
30
+ peakWeekdaysUtc?: number[];
32
31
  offPeak?: ModelPricingRate;
33
32
  /** When the recurring tariff became effective. */
34
33
  effectiveFrom?: string;
35
34
  /** Rate used before `effectiveFrom` (for historical session statistics). */
36
- prior?: ModelPricingRate;
35
+ prior?: ModelPricingTariff;
37
36
  effectiveUntil?: string;
38
- original?: ModelPricingRate;
37
+ original?: ModelPricingTariff;
38
+ }
39
+ export interface ModelPricing extends ModelPricingTariff {
40
+ providerId: string;
41
+ modelId: string;
42
+ currency: PricingCurrency;
39
43
  }
40
44
  /**
41
45
  * Hand-maintained corrections layered over the generated models.dev snapshot.
Binary file
@@ -0,0 +1,34 @@
1
+ import type { ThinkingLevel } from "./types.js";
2
+ export interface SanitizedProviderError {
3
+ providerId: string;
4
+ modelId: string;
5
+ model?: string;
6
+ thinkingLevel: ThinkingLevel;
7
+ name: string;
8
+ message: string;
9
+ httpStatus?: number;
10
+ code?: string;
11
+ parameter?: string;
12
+ messageCount: number;
13
+ toolCount: number;
14
+ bubbleVersion: string;
15
+ pid: number;
16
+ runtimeStartedAt: number;
17
+ }
18
+ export interface ProviderErrorContext {
19
+ providerId: string;
20
+ modelId: string;
21
+ model?: string;
22
+ thinkingLevel: ThinkingLevel;
23
+ messageCount: number;
24
+ toolCount: number;
25
+ }
26
+ /**
27
+ * Build the allowlisted diagnostic record written to a session log.
28
+ *
29
+ * Deliberately do not serialize the original error object: SDK errors can
30
+ * contain request headers, request bodies, response bodies and stacks. Only
31
+ * scalar identifiers plus a locally-authored error category are retained.
32
+ */
33
+ export declare function createSanitizedProviderError(error: unknown, context: ProviderErrorContext): SanitizedProviderError;
34
+ export declare function sanitizeProviderErrorText(value: string): string;
@@ -0,0 +1,185 @@
1
+ import { getCurrentVersion } from "./update/index.js";
2
+ const MAX_FIELD_CHARS = 160;
3
+ const RUNTIME_STARTED_AT = Date.now();
4
+ const SAFE_ERROR_CODES = new Map([
5
+ ["authentication_error", "authentication_error"],
6
+ ["bad_request", "bad_request"],
7
+ ["context_length_exceeded", "context_length_exceeded"],
8
+ ["failed_precondition", "failed_precondition"],
9
+ ["internal_error", "internal_error"],
10
+ ["invalid_argument", "invalid_argument"],
11
+ ["invalidparameter", "invalid_parameter"],
12
+ ["invalid_parameter", "invalid_parameter"],
13
+ ["location_not_supported", "location_not_supported"],
14
+ ["permission_denied", "permission_denied"],
15
+ ["rate_limit_exceeded", "rate_limit_exceeded"],
16
+ ["rate_limit_exhausted", "rate_limit_exhausted"],
17
+ ["resource_exhausted", "resource_exhausted"],
18
+ ["service_unavailable", "service_unavailable"],
19
+ ["timeout", "timeout"],
20
+ ["unauthenticated", "unauthenticated"],
21
+ ["unsupported_parameter", "unsupported_parameter"],
22
+ ["unsupported_value", "unsupported_value"],
23
+ ]);
24
+ const SAFE_PARAMETER_NAMES = new Set([
25
+ "contents",
26
+ "frequency_penalty",
27
+ "generationConfig",
28
+ "input",
29
+ "max_output_tokens",
30
+ "max_tokens",
31
+ "messages",
32
+ "model",
33
+ "n",
34
+ "presence_penalty",
35
+ "prompt",
36
+ "reasoning_effort",
37
+ "response_format",
38
+ "seed",
39
+ "stop",
40
+ "stream",
41
+ "temperature",
42
+ "thinkingConfig",
43
+ "thinkingLevel",
44
+ "thinking_level",
45
+ "tool_choice",
46
+ "tools",
47
+ "topP",
48
+ "top_p",
49
+ ]);
50
+ /**
51
+ * Build the allowlisted diagnostic record written to a session log.
52
+ *
53
+ * Deliberately do not serialize the original error object: SDK errors can
54
+ * contain request headers, request bodies, response bodies and stacks. Only
55
+ * scalar identifiers plus a locally-authored error category are retained.
56
+ */
57
+ export function createSanitizedProviderError(error, context) {
58
+ const sources = errorSources(error);
59
+ const rawMessage = firstString(sources, ["message"])
60
+ || (typeof error === "string" ? error : "Provider request failed.");
61
+ const httpStatus = firstHttpStatus(sources);
62
+ const code = sanitizeErrorCode(firstScalarString(sources, ["code", "errorCode", "type"]));
63
+ const parameter = sanitizeParameterName(firstScalarString(sources, ["param", "parameter"]));
64
+ return {
65
+ providerId: sanitizeControlledIdentifier(context.providerId),
66
+ modelId: sanitizeControlledIdentifier(context.modelId),
67
+ ...(context.model ? { model: sanitizeControlledIdentifier(context.model) } : {}),
68
+ thinkingLevel: context.thinkingLevel,
69
+ name: "ProviderError",
70
+ message: sanitizeProviderErrorText(rawMessage),
71
+ ...(httpStatus !== undefined ? { httpStatus } : {}),
72
+ ...(code ? { code } : {}),
73
+ ...(parameter ? { parameter } : {}),
74
+ messageCount: Math.max(0, Math.trunc(context.messageCount)),
75
+ toolCount: Math.max(0, Math.trunc(context.toolCount)),
76
+ bubbleVersion: getCurrentVersion(),
77
+ pid: process.pid,
78
+ runtimeStartedAt: RUNTIME_STARTED_AT,
79
+ };
80
+ }
81
+ export function sanitizeProviderErrorText(value) {
82
+ const text = value.replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
83
+ // Fail closed: provider/SDK messages can embed request headers, bodies,
84
+ // prompts, URLs or credentials in formats a denylist cannot anticipate.
85
+ // Persist only fixed, locally-authored summaries selected by broad error
86
+ // categories; never return any substring copied from the provider message.
87
+ if (/user location (?:is )?not supported|location[^.]{0,40}(?:unsupported|not supported)/i.test(text)) {
88
+ return "User location is not supported for API use.";
89
+ }
90
+ if (/context[_ -]?(?:length|window)|prompt (?:is )?too long|maximum context|token limit/i.test(text)) {
91
+ return "Provider context limit was exceeded.";
92
+ }
93
+ if (/rate limit|too many requests|quota|resource exhausted|\b429\b/i.test(text)) {
94
+ return "Provider rate limit was exceeded.";
95
+ }
96
+ if (/unauthori[sz]ed|authentication|invalid (?:api )?key|missing (?:api )?key|\b401\b/i.test(text)) {
97
+ return "Provider authentication failed.";
98
+ }
99
+ if (/forbidden|permission denied|access denied|\b403\b/i.test(text)) {
100
+ return "Provider denied the request.";
101
+ }
102
+ if (/invalid[^.]{0,60}(?:parameter|argument)|unsupported[^.]{0,60}(?:parameter|argument)|bad request|\b400\b/i.test(text)) {
103
+ return "Provider rejected a request parameter.";
104
+ }
105
+ if (/timed? ?out|timeout/i.test(text)) {
106
+ return "Provider request timed out.";
107
+ }
108
+ if (/network|connection|socket|fetch failed|stream (?:closed|interrupted)/i.test(text)) {
109
+ return "Provider connection failed.";
110
+ }
111
+ if (/abort|cancel/i.test(text)) {
112
+ return "Provider request was cancelled.";
113
+ }
114
+ return "Provider request failed.";
115
+ }
116
+ function errorSources(error) {
117
+ const sources = [];
118
+ const root = asRecord(error);
119
+ if (root)
120
+ sources.push(root);
121
+ const nested = asRecord(root?.error);
122
+ if (nested)
123
+ sources.push(nested);
124
+ const cause = asRecord(root?.cause);
125
+ if (cause)
126
+ sources.push(cause);
127
+ const causeError = asRecord(cause?.error);
128
+ if (causeError)
129
+ sources.push(causeError);
130
+ return sources;
131
+ }
132
+ function asRecord(value) {
133
+ return value !== null && (typeof value === "object" || typeof value === "function")
134
+ ? value
135
+ : undefined;
136
+ }
137
+ function firstString(sources, keys) {
138
+ for (const source of sources) {
139
+ for (const key of keys) {
140
+ const value = source[key];
141
+ if (typeof value === "string" && value.trim())
142
+ return value.trim();
143
+ }
144
+ }
145
+ return undefined;
146
+ }
147
+ function firstScalarString(sources, keys) {
148
+ for (const source of sources) {
149
+ for (const key of keys) {
150
+ const value = source[key];
151
+ if ((typeof value === "string" || typeof value === "number") && String(value).trim()) {
152
+ return String(value).trim();
153
+ }
154
+ }
155
+ }
156
+ return undefined;
157
+ }
158
+ function firstHttpStatus(sources) {
159
+ for (const source of sources) {
160
+ for (const key of ["status", "statusCode", "httpStatus"]) {
161
+ const value = source[key];
162
+ const numeric = typeof value === "number" ? value : Number(value);
163
+ if (Number.isInteger(numeric) && numeric >= 100 && numeric <= 599)
164
+ return numeric;
165
+ }
166
+ }
167
+ return undefined;
168
+ }
169
+ function sanitizeControlledIdentifier(value) {
170
+ const normalized = value.trim();
171
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/.test(normalized))
172
+ return "unknown";
173
+ return normalized.slice(0, MAX_FIELD_CHARS);
174
+ }
175
+ function sanitizeErrorCode(value) {
176
+ if (!value)
177
+ return undefined;
178
+ return SAFE_ERROR_CODES.get(value.trim().toLowerCase());
179
+ }
180
+ function sanitizeParameterName(value) {
181
+ if (!value)
182
+ return undefined;
183
+ const normalized = value.trim();
184
+ return SAFE_PARAMETER_NAMES.has(normalized) ? normalized : undefined;
185
+ }
@@ -17,6 +17,7 @@ const KIMI_TOGGLE_THINKING_FAMILY = new Set(["kimi-k2.5", "kimi-k2.6"]);
17
17
  const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
18
18
  const MINIMAX_M3_FAMILY = new Set(["MiniMax-M3"]);
19
19
  const DEEPSEEK_V4_FAMILY = new Set([
20
+ "deepseek-flash",
20
21
  "deepseek-v4-flash",
21
22
  "deepseek-v4-flash-vision-exp",
22
23
  "deepseek-v4-pro",
@@ -111,6 +112,9 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
111
112
  },
112
113
  };
113
114
  }
115
+ if (providerId === "stepfun-api") {
116
+ return { effectiveThinkingLevel, reasoningContentEcho: "none" };
117
+ }
114
118
  if (providerId === "stepfun") {
115
119
  return {
116
120
  effectiveThinkingLevel,
package/dist/provider.js CHANGED
@@ -314,6 +314,7 @@ function shouldRequestStreamUsage(options) {
314
314
  || providerId === "zai-coding-plan"
315
315
  // Bailian sends usage either way; ask explicitly so it stays guaranteed.
316
316
  || providerId === "bailian-token-plan"
317
+ || providerId === "stepfun-api"
317
318
  || isMiniMaxOpenAICompatible(options);
318
319
  }
319
320
  function shouldUseNonStreamingToolCalls(options, tools, toolChoice) {
@@ -503,6 +504,7 @@ function usageToStreamChunk(usage) {
503
504
  usage: {
504
505
  promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
505
506
  completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
507
+ outputTokensReported: typeof usage.completion_tokens === "number",
506
508
  promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number"
507
509
  ? usage.prompt_cache_hit_tokens
508
510
  : typeof usage.prompt_tokens_details?.cached_tokens === "number"
@@ -673,6 +675,7 @@ export async function* translateOpenAIStream(stream, options = {}) {
673
675
  usage: {
674
676
  promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
675
677
  completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
678
+ outputTokensReported: typeof usage.completion_tokens === "number",
676
679
  promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number"
677
680
  ? usage.prompt_cache_hit_tokens
678
681
  : typeof usage.prompt_tokens_details?.cached_tokens === "number"
package/dist/sdk/index.js CHANGED
@@ -417,6 +417,11 @@ export class BubbleSdk {
417
417
  if (message.role === "assistant")
418
418
  recordMemoryCitations(cwd, message.content);
419
419
  },
420
+ onProviderError: (error) => {
421
+ if (this.turnCoordinator.isDeleted(sessionId))
422
+ return;
423
+ session.appendProviderError(error);
424
+ },
420
425
  onCompactionApplied: (summary) => {
421
426
  if (this.turnCoordinator.isDeleted(sessionId))
422
427
  return;
@@ -1,5 +1,6 @@
1
1
  import type { Message } from "./types.js";
2
- import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionSummaryEntry } from "./session-types.js";
2
+ import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionProviderErrorEntry, SessionSummaryEntry } from "./session-types.js";
3
+ import type { SanitizedProviderError } from "./provider-error-record.js";
3
4
  export declare class SessionLog {
4
5
  private entries;
5
6
  load(lines: string[]): void;
@@ -10,5 +11,6 @@ export declare class SessionLog {
10
11
  appendMessage(message: Message): SessionLogEntry[];
11
12
  appendSummary(summary: string): SessionSummaryEntry;
12
13
  appendMarker(kind: SessionMarkerKind, value: string): SessionLogEntry;
14
+ appendProviderError(error: SanitizedProviderError): SessionProviderErrorEntry;
13
15
  toMessages(): Message[];
14
16
  }
@@ -71,6 +71,16 @@ export class SessionLog {
71
71
  this.entries.push(entry);
72
72
  return entry;
73
73
  }
74
+ appendProviderError(error) {
75
+ const entry = {
76
+ id: nextEntryId(this.entries),
77
+ type: "provider_error",
78
+ error,
79
+ timestamp: Date.now(),
80
+ };
81
+ this.entries.push(entry);
82
+ return entry;
83
+ }
74
84
  toMessages() {
75
85
  const messages = [];
76
86
  let latestSummaryIndex = -1;
@@ -217,6 +227,7 @@ function isSessionLogEntry(entry) {
217
227
  "assistant_message",
218
228
  "tool_call",
219
229
  "tool_result",
230
+ "provider_error",
220
231
  ].includes(entry.type);
221
232
  }
222
233
  function nextEntryId(entries) {
@@ -1,5 +1,6 @@
1
1
  import type { AssistantMessage, Message, ThinkingLevel, ToolCall, ToolMessage, UserMessage } from "./types.js";
2
2
  import type { GoalState } from "./goal/store.js";
3
+ import type { SanitizedProviderError } from "./provider-error-record.js";
3
4
  export interface ExternalRuntimeSessionMetadata {
4
5
  /** External agent runtime that owns the conversational state. */
5
6
  id: "grok";
@@ -65,7 +66,11 @@ export interface SessionToolResultEntry extends BaseSessionLogEntry {
65
66
  type: "tool_result";
66
67
  message: ToolMessage;
67
68
  }
68
- export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry;
69
+ export interface SessionProviderErrorEntry extends BaseSessionLogEntry {
70
+ type: "provider_error";
71
+ error: SanitizedProviderError;
72
+ }
73
+ export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry | SessionProviderErrorEntry;
69
74
  export interface LegacySessionEntry {
70
75
  id: string;
71
76
  type: "metadata" | "message" | "compaction";
package/dist/session.d.ts CHANGED
@@ -5,6 +5,7 @@ import { CheckpointStore } from "./checkpoints.js";
5
5
  import { type CompactOptions, type CompactResult } from "./context/compact.js";
6
6
  import type { Message } from "./types.js";
7
7
  import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
8
+ import type { SanitizedProviderError } from "./provider-error-record.js";
8
9
  export interface SessionSummary {
9
10
  file: string;
10
11
  name: string;
@@ -62,6 +63,7 @@ export declare class SessionManager {
62
63
  appendMessage(message: Message): void;
63
64
  appendCompaction(summary: string): void;
64
65
  appendMarker(kind: SessionMarkerKind, value: string): void;
66
+ appendProviderError(error: SanitizedProviderError): void;
65
67
  compact(options?: CompactOptions): CompactResult;
66
68
  /**
67
69
  * Inspect whether the session is large enough to compact and, if so, return
package/dist/session.js CHANGED
@@ -172,6 +172,10 @@ export class SessionManager {
172
172
  const entry = this.log.appendMarker(kind, value);
173
173
  this.persist(entry);
174
174
  }
175
+ appendProviderError(error) {
176
+ const entry = this.log.appendProviderError(error);
177
+ this.persist(entry);
178
+ }
175
179
  compact(options) {
176
180
  const result = compactSessionEntries(this.log.list(), options);
177
181
  if (result.compacted && result.entries) {
@@ -0,0 +1,13 @@
1
+ /** Persistent headless transport. One Agent + MCP lifecycle, sequential user turns. */
2
+ import type { Readable } from "node:stream";
3
+ import type { AgentEvent } from "./types.js";
4
+ interface StreamSessionOptions {
5
+ input: Readable;
6
+ write: (message: Record<string, unknown>) => void;
7
+ sessionId: string;
8
+ model: string;
9
+ run: (prompt: string) => AsyncIterable<AgentEvent>;
10
+ onEvent?: (event: AgentEvent) => void;
11
+ }
12
+ export declare function runStreamJsonSession(options: StreamSessionOptions): Promise<void>;
13
+ export {};
@@ -0,0 +1,74 @@
1
+ import { PrintRunCollector } from "./print-output.js";
2
+ const maxLineBytes = 1024 * 1024;
3
+ async function* lines(input) {
4
+ input.setEncoding("utf8");
5
+ let pending = "";
6
+ for await (const chunk of input) {
7
+ pending += chunk;
8
+ let index;
9
+ while ((index = pending.indexOf("\n")) >= 0) {
10
+ const line = pending.slice(0, index).replace(/\r$/, "");
11
+ pending = pending.slice(index + 1);
12
+ if (Buffer.byteLength(line) > maxLineBytes)
13
+ throw new Error("Input line exceeds 1 MiB");
14
+ if (line.trim())
15
+ yield line;
16
+ }
17
+ if (Buffer.byteLength(pending) > maxLineBytes)
18
+ throw new Error("Input line exceeds 1 MiB");
19
+ }
20
+ if (pending.trim())
21
+ yield pending;
22
+ }
23
+ function userText(value) {
24
+ if (typeof value === "string" && value.trim())
25
+ return value;
26
+ if (Array.isArray(value) && value.length && value.every(item => item && item.type === "text" && typeof item.text === "string")) {
27
+ const text = value.map(item => item.text).join("\n");
28
+ if (text.trim())
29
+ return text;
30
+ }
31
+ throw new Error("message.content must be non-empty text or text blocks");
32
+ }
33
+ export async function runStreamJsonSession(options) {
34
+ const { write, sessionId, model } = options;
35
+ write({ type: "system", subtype: "init", session_id: sessionId, model, protocol_version: 1 });
36
+ let sequence = 0;
37
+ // Read only as turns finish: the stream's own backpressure bounds queued input.
38
+ // EOF drains all accepted messages before main() shuts down MCP and the Agent.
39
+ for await (const line of lines(options.input)) {
40
+ let requestId;
41
+ let prompt;
42
+ try {
43
+ const message = JSON.parse(line);
44
+ if (typeof message?.request_id === "string")
45
+ requestId = message.request_id;
46
+ if (message?.type !== "user" || message.message?.role !== "user") {
47
+ throw new Error('Expected {type:"user",message:{role:"user",content:"..."}}');
48
+ }
49
+ if (message.session_id && message.session_id !== sessionId)
50
+ throw new Error("Session ID does not match this process");
51
+ prompt = userText(message.message.content);
52
+ }
53
+ catch (error) {
54
+ write({ type: "result", is_error: true, subtype: "invalid_input", session_id: sessionId,
55
+ request_id: requestId, result: error instanceof Error ? error.message : String(error) });
56
+ continue;
57
+ }
58
+ const collector = new PrintRunCollector();
59
+ const envelope = { session_id: sessionId, request_id: requestId, turn: ++sequence };
60
+ try {
61
+ for await (const event of options.run(prompt)) {
62
+ collector.onEvent(event);
63
+ options.onEvent?.(event);
64
+ write({ type: "stream_event", ...envelope, event });
65
+ }
66
+ const summary = collector.summary();
67
+ write({ type: "result", ...envelope, is_error: false, result: summary.text, stopReason: "end_turn", ...summary });
68
+ }
69
+ catch (error) {
70
+ write({ type: "result", ...envelope, is_error: true, stopReason: "error", ...collector.summary(),
71
+ result: error instanceof Error ? error.message : String(error) });
72
+ }
73
+ }
74
+ }
@@ -84,7 +84,8 @@ export function reduceAgentEvent(state, event, ctx) {
84
84
  if (tc) {
85
85
  tc.rawArguments = event.arguments;
86
86
  tc.args = parsePartialArgs(event.arguments, tc.args);
87
- effects.push({ kind: "tools-updated" });
87
+ // Parameter chunks are streaming data, like text/reasoning. Let the
88
+ // controller coalesce their paint instead of rendering for every byte.
88
89
  return { state: { ...state, dirty: { ...state.dirty, tools: true } }, effects };
89
90
  }
90
91
  return { state, effects };
@@ -682,12 +682,13 @@ export class BubbleTuiController {
682
682
  this.applyEffects(effects);
683
683
  }
684
684
  const hasDirty = Object.values(this.runState.dirty).some(Boolean);
685
- // Text/reasoning deltas have no immediate effect notification. Batch
686
- // them behind one coalesced 40ms paint. Tool events already notified
687
- // through their effects, so acknowledging their dirty bits here avoids
688
- // a redundant delayed repaint. Dirty means "paint pending", not
685
+ // Streamed text, reasoning, and tool arguments share one 40ms paint.
686
+ // Repainting each argument chunk can starve input/timers while a
687
+ // buffered response drains, especially with a long reasoning preview.
688
+ // Tool lifecycle events still notify immediately through their effects.
689
+ // Dirty means "paint pending", not
689
690
  // "this run has ever changed", and must never remain latched forever.
690
- if (hasDirty && (event.type === "text_delta" || event.type === "reasoning_delta")) {
691
+ if (hasDirty && (event.type === "text_delta" || event.type === "reasoning_delta" || event.type === "tool_call_delta")) {
691
692
  this.deps.ports.flush.scheduleFlush(STREAMING_FLUSH_INTERVAL_MS, () => {
692
693
  if (this.disposed || !this.runActive)
693
694
  return;
package/dist/types.d.ts CHANGED
@@ -322,6 +322,8 @@ export type StreamChunk = {
322
322
  type: "done";
323
323
  };
324
324
  export interface TokenUsage {
325
+ /** Whether the upstream response explicitly supplied completion_tokens. */
326
+ outputTokensReported?: boolean;
325
327
  promptTokens: number;
326
328
  completionTokens: number;
327
329
  promptCacheHitTokens?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {