@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.
Files changed (41) hide show
  1. package/README.md +35 -0
  2. package/dist/agent/child-runner.d.ts +7 -0
  3. package/dist/agent/child-runner.js +67 -3
  4. package/dist/agent/subagent/runtime.d.ts +3 -0
  5. package/dist/agent/subagent/runtime.js +38 -8
  6. package/dist/agent/transcript-helpers.d.ts +1 -1
  7. package/dist/agent/transcript-helpers.js +4 -3
  8. package/dist/agent.d.ts +6 -0
  9. package/dist/agent.js +36 -0
  10. package/dist/cli.d.ts +2 -1
  11. package/dist/cli.js +22 -3
  12. package/dist/context/budget.js +7 -2
  13. package/dist/context/compact.js +8 -2
  14. package/dist/feishu/agent-host/run-driver.js +3 -0
  15. package/dist/main.js +16 -1
  16. package/dist/mcp/client.js +15 -1
  17. package/dist/mcp/transports.d.ts +2 -2
  18. package/dist/mcp/types.d.ts +1 -1
  19. package/dist/memory/phase1.js +4 -0
  20. package/dist/model-catalog.d.ts +1 -1
  21. package/dist/model-catalog.js +7 -0
  22. package/dist/model-pricing.js +0 -0
  23. package/dist/provider-ark-responses.d.ts +12 -2
  24. package/dist/provider-ark-responses.js +68 -13
  25. package/dist/provider-error-record.d.ts +34 -0
  26. package/dist/provider-error-record.js +185 -0
  27. package/dist/provider-transform.js +1 -0
  28. package/dist/provider.js +7 -1
  29. package/dist/sdk/index.js +5 -0
  30. package/dist/session-log.d.ts +3 -1
  31. package/dist/session-log.js +11 -0
  32. package/dist/session-types.d.ts +6 -1
  33. package/dist/session.d.ts +2 -0
  34. package/dist/session.js +4 -0
  35. package/dist/stream-json.d.ts +13 -0
  36. package/dist/stream-json.js +74 -0
  37. package/dist/tui/controller/agent-event-reducer.js +2 -1
  38. package/dist/tui/controller/controller.js +6 -5
  39. package/dist/tui/model/image-paste.js +9 -10
  40. package/dist/types.d.ts +3 -1
  41. package/package.json +1 -1
@@ -154,7 +154,21 @@ export class MCPClient {
154
154
  }
155
155
  return;
156
156
  }
157
- // Server→client request or notification. v1 ignores these (no sampling, no roots).
157
+ // Ping is a core MCP request and requires no advertised capability. Long-lived
158
+ // servers (including Playwright) close healthy sessions when it is ignored.
159
+ if ("id" in msg && "method" in msg) {
160
+ const response = msg.method === "ping"
161
+ ? { jsonrpc: "2.0", id: msg.id, result: {} }
162
+ : { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Unsupported client method: ${msg.method}` } };
163
+ void this.transport.send(response).catch((error) => {
164
+ for (const pending of this.pending.values()) {
165
+ clearTimeout(pending.timer);
166
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
167
+ }
168
+ this.pending.clear();
169
+ });
170
+ }
171
+ // Notifications need no response; sampling and roots are not advertised.
158
172
  }
159
173
  async close() {
160
174
  this.closed = true;
@@ -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) {
@@ -1,5 +1,5 @@
1
1
  import type { ReasoningEffort } from "./types.js";
2
- export type ProviderProtocol = "openai-chat" | "anthropic-messages" | "ark-responses" | "ai-sdk";
2
+ export type ProviderProtocol = "openai-chat" | "openai-responses" | "anthropic-messages" | "ark-responses" | "ai-sdk";
3
3
  export interface BuiltinProviderDefinition {
4
4
  id: string;
5
5
  name: string;
@@ -3,6 +3,7 @@ export const BUILTIN_PROVIDERS = [
3
3
  { id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1" },
4
4
  { id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", supportsOAuth: true },
5
5
  { id: "openai-codex", name: "OpenAI Codex (ChatGPT)", baseURL: "https://chatgpt.com/backend-api" },
6
+ { id: "opencode-zen", name: "OpenCode Zen", baseURL: "https://opencode.ai/zen/v1", protocol: "openai-responses" },
6
7
  // Grok subscription models via the CLI chat proxy. OAuth-only: the proxy
7
8
  // accepts xAI session bearers (from /login grok), not api.x.ai API keys.
8
9
  { id: "grok", name: "Grok Subscription", baseURL: "https://cli-chat-proxy.grok.com/v1", supportsOAuth: true },
@@ -39,6 +40,7 @@ const GPT51_LEVELS = ["off", "low", "medium", "high"];
39
40
  const GPT51_CODEX_MAX_LEVELS = ["off", "low", "medium", "high", "xhigh"];
40
41
  const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
41
42
  const OPENAI_CHAT_LEVELS = ["off"];
43
+ const MUSE_SPARK_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
42
44
  // Ox Alpha declares mandatory reasoning with exactly low/high/max through
43
45
  // OpenRouter's live model metadata. The endpoint rejects disabled reasoning.
44
46
  const OPENROUTER_OX_REASONING_LEVELS = ["low", "high", "max"];
@@ -117,11 +119,16 @@ export const BUILTIN_MODELS = [
117
119
  { id: "o1-preview", name: "o1-preview", providerId: "openai", tier: "strong", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
118
120
  { id: "o1-mini", name: "o1-mini", providerId: "openai", tier: "fast", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
119
121
  { id: "gpt-4-turbo", name: "gpt-4-turbo", providerId: "openai", reasoningLevels: OPENAI_CHAT_LEVELS, contextWindow: 128000 },
122
+ { id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Free", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
123
+ { id: "muse-spark-1.2", name: "Muse Spark 1.2", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
124
+ { id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Free", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
120
125
  { id: "claude-fable-5", name: "Claude Fable 5", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_FABLE_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
121
126
  { id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_OPUS_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
122
127
  { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providerId: "anthropic", tier: "balanced", reasoningLevels: ANTHROPIC_SONNET_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
123
128
  { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providerId: "anthropic", tier: "fast", reasoningLevels: ANTHROPIC_CHAT_LEVELS, contextWindow: 200000 },
124
129
  { id: "deepseek-v4-flash", name: "deepseek-v4-flash", providerId: "deepseek", tier: "fast", reasoningLevels: DEEPSEEK_V4_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
130
+ // DeepSeek Flash: retain the high effort used by the previous preview.
131
+ { id: "deepseek-flash", name: "deepseek-flash", providerId: "deepseek", reasoningLevels: ["high"], defaultReasoningLevel: "high" },
125
132
  // Experimental vision model stays explicit-only: it is text-capable, but
126
133
  // automatic fast-tier routing must not silently switch ordinary subagents
127
134
  // from the stable Flash model to an experimental multimodal endpoint.
Binary file
@@ -1,5 +1,5 @@
1
1
  import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel, TokenUsage, ToolChoiceMode, ToolDefinition } from "./types.js";
2
- export interface ArkResponsesProviderOptions {
2
+ export interface ResponsesProviderOptions {
3
3
  providerId?: string;
4
4
  apiKey: string;
5
5
  baseURL: string;
@@ -7,7 +7,9 @@ export interface ArkResponsesProviderOptions {
7
7
  /** User-configured extra headers, merged last (client-identity gates). */
8
8
  headers?: Record<string, string>;
9
9
  }
10
+ export type ArkResponsesProviderOptions = ResponsesProviderOptions;
10
11
  export declare function createArkResponsesProvider(options: ArkResponsesProviderOptions): Provider;
12
+ export declare function createOpenAIResponsesProvider(options: ResponsesProviderOptions): Provider;
11
13
  export declare function buildArkResponsesBody(messages: ProviderMessage[], options: {
12
14
  model: string;
13
15
  tools?: ToolDefinition[];
@@ -15,5 +17,13 @@ export declare function buildArkResponsesBody(messages: ProviderMessage[], optio
15
17
  thinkingLevel?: ThinkingLevel;
16
18
  stream: boolean;
17
19
  }): Record<string, unknown>;
18
- export declare function translateArkResponsesStream(response: Response): AsyncIterable<StreamChunk>;
20
+ export declare function buildOpenAIResponsesBody(messages: ProviderMessage[], options: {
21
+ model: string;
22
+ tools?: ToolDefinition[];
23
+ toolChoice?: ToolChoiceMode;
24
+ temperature?: number;
25
+ thinkingLevel?: ThinkingLevel;
26
+ stream: boolean;
27
+ }): Record<string, unknown>;
28
+ export declare function translateArkResponsesStream(response: Response, captureOpenAIReasoning?: boolean): AsyncIterable<StreamChunk>;
19
29
  export declare function normalizeArkResponsesUsage(usage: any): TokenUsage;
@@ -1,33 +1,43 @@
1
1
  import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
2
2
  import { RateLimitError } from "./network/errors.js";
3
3
  import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, retryAfterMsFromResponse, sleepBeforeRetry, } from "./network/retry.js";
4
+ import { providerFetch } from "./network/provider-transport.js";
4
5
  const DEFAULT_ARK_MODEL = "doubao-seed-2-1-pro-260628";
6
+ const DEFAULT_MUSE_MODEL = "muse-spark-1.3-contributor-free";
5
7
  export function createArkResponsesProvider(options) {
8
+ return createResponsesProvider(options, "ark", DEFAULT_ARK_MODEL);
9
+ }
10
+ export function createOpenAIResponsesProvider(options) {
11
+ return createResponsesProvider(options, "openai", DEFAULT_MUSE_MODEL);
12
+ }
13
+ function createResponsesProvider(options, dialect, defaultModel) {
6
14
  async function* streamChat(messages, chatOptions) {
7
- const body = buildArkResponsesBody(messages, {
15
+ const body = buildResponsesBody(messages, {
8
16
  model: chatOptions.model,
9
17
  tools: chatOptions.tools,
10
18
  toolChoice: chatOptions.toolChoice,
19
+ temperature: chatOptions.temperature,
11
20
  thinkingLevel: chatOptions.thinkingLevel ?? options.thinkingLevel ?? "high",
12
21
  stream: true,
13
- });
22
+ }, dialect);
14
23
  for (let attempt = 0;; attempt += 1) {
15
24
  const response = await sendArkResponsesRequest(options, body, {
16
25
  signal: chatOptions.abortSignal,
17
26
  rateLimitPolicy: chatOptions.rateLimitPolicy,
18
27
  attempt,
19
28
  });
20
- yield* translateArkResponsesStream(response);
29
+ yield* translateArkResponsesStream(response, dialect === "openai");
21
30
  yield { type: "done" };
22
31
  return;
23
32
  }
24
33
  }
25
34
  async function complete(messages, chatOptions) {
26
- const body = buildArkResponsesBody(messages, {
27
- model: chatOptions?.model ?? DEFAULT_ARK_MODEL,
35
+ const body = buildResponsesBody(messages, {
36
+ model: chatOptions?.model ?? defaultModel,
37
+ temperature: chatOptions?.temperature,
28
38
  thinkingLevel: chatOptions?.thinkingLevel ?? options.thinkingLevel ?? "high",
29
39
  stream: false,
30
- });
40
+ }, dialect);
31
41
  const response = await sendArkResponsesRequest(options, body, {
32
42
  signal: chatOptions?.abortSignal,
33
43
  attempt: 0,
@@ -38,15 +48,34 @@ export function createArkResponsesProvider(options) {
38
48
  return { streamChat, complete };
39
49
  }
40
50
  export function buildArkResponsesBody(messages, options) {
51
+ return buildResponsesBody(messages, options, "ark");
52
+ }
53
+ export function buildOpenAIResponsesBody(messages, options) {
54
+ return buildResponsesBody(messages, options, "openai");
55
+ }
56
+ function buildResponsesBody(messages, options, dialect) {
41
57
  const body = {
42
58
  model: options.model,
43
- input: messages.flatMap((message) => convertMessageToArkResponsesInput(message)),
59
+ input: messages.flatMap((message) => convertMessageToResponsesInput(message, dialect)),
44
60
  store: false,
45
61
  stream: options.stream,
46
- thinking: {
47
- type: shouldDisableArkThinking(options.thinkingLevel) ? "disabled" : "enabled",
48
- },
49
62
  };
63
+ if (dialect === "ark") {
64
+ body.thinking = {
65
+ type: shouldDisableArkThinking(options.thinkingLevel) ? "disabled" : "enabled",
66
+ };
67
+ }
68
+ else {
69
+ body.include = ["reasoning.encrypted_content"];
70
+ if (options.thinkingLevel && options.thinkingLevel !== "off") {
71
+ body.reasoning = {
72
+ effort: options.thinkingLevel === "ultra" ? "xhigh" : options.thinkingLevel,
73
+ summary: "auto",
74
+ };
75
+ }
76
+ if (options.temperature !== undefined)
77
+ body.temperature = options.temperature;
78
+ }
50
79
  if (options.tools && options.tools.length > 0) {
51
80
  body.tools = options.tools.map((tool) => ({
52
81
  type: "function",
@@ -63,7 +92,7 @@ export function buildArkResponsesBody(messages, options) {
63
92
  function shouldDisableArkThinking(level) {
64
93
  return level === "off" || level === "minimal";
65
94
  }
66
- function convertMessageToArkResponsesInput(message) {
95
+ function convertMessageToResponsesInput(message, dialect) {
67
96
  if (message.role === "user") {
68
97
  if (typeof message.content === "string") {
69
98
  return [{
@@ -92,6 +121,17 @@ function convertMessageToArkResponsesInput(message) {
92
121
  }
93
122
  if (message.role === "assistant") {
94
123
  const items = [];
124
+ if (dialect === "openai") {
125
+ for (const block of message.providerMetadata?.openai?.contentBlocks ?? []) {
126
+ if (block.type !== "reasoning" || typeof block.encrypted_content !== "string")
127
+ continue;
128
+ items.push({
129
+ type: "reasoning",
130
+ summary: Array.isArray(block.summary) ? block.summary : [],
131
+ encrypted_content: block.encrypted_content,
132
+ });
133
+ }
134
+ }
95
135
  if (message.content) {
96
136
  items.push({
97
137
  type: "message",
@@ -118,7 +158,7 @@ function convertMessageToArkResponsesInput(message) {
118
158
  }];
119
159
  }
120
160
  async function sendArkResponsesRequest(options, body, requestOptions) {
121
- const response = await fetch(resolveArkResponsesUrl(options.baseURL), {
161
+ const response = await providerFetch(resolveArkResponsesUrl(options.baseURL), {
122
162
  method: "POST",
123
163
  headers: {
124
164
  "Authorization": `Bearer ${options.apiKey}`,
@@ -128,6 +168,8 @@ async function sendArkResponsesRequest(options, body, requestOptions) {
128
168
  },
129
169
  body: JSON.stringify(body),
130
170
  signal: requestOptions.signal,
171
+ }, {
172
+ providerName: options.providerId ?? "Responses provider",
131
173
  });
132
174
  if (response.ok)
133
175
  return response;
@@ -159,7 +201,7 @@ async function sendArkResponsesRequest(options, body, requestOptions) {
159
201
  function resolveArkResponsesUrl(baseURL) {
160
202
  return `${baseURL.trim().replace(/\/+$/, "")}/responses`;
161
203
  }
162
- export async function* translateArkResponsesStream(response) {
204
+ export async function* translateArkResponsesStream(response, captureOpenAIReasoning = false) {
163
205
  const toolCalls = new Map();
164
206
  const textFilter = createProviderProtocolArtifactFilter();
165
207
  for await (const event of parseArkResponsesSse(response)) {
@@ -253,6 +295,19 @@ export async function* translateArkResponsesStream(response) {
253
295
  }
254
296
  if (type === "response.output_item.done") {
255
297
  const item = event.item;
298
+ if (captureOpenAIReasoning
299
+ && item?.type === "reasoning"
300
+ && typeof item.encrypted_content === "string") {
301
+ yield {
302
+ type: "provider_content_block",
303
+ provider: "openai",
304
+ block: {
305
+ type: "reasoning",
306
+ summary: Array.isArray(item.summary) ? item.summary : [],
307
+ encrypted_content: item.encrypted_content,
308
+ },
309
+ };
310
+ }
256
311
  if (item?.type === "function_call") {
257
312
  const key = toolCallEventKey(event, item);
258
313
  const entry = toolCalls.get(key);
@@ -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",
package/dist/provider.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import OpenAI from "openai";
7
7
  import { appendFileSync } from "node:fs";
8
8
  import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
9
- import { createArkResponsesProvider } from "./provider-ark-responses.js";
9
+ import { createArkResponsesProvider, createOpenAIResponsesProvider } from "./provider-ark-responses.js";
10
10
  import { createAiSdkProvider } from "./provider-ai-sdk.js";
11
11
  import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
12
12
  import { buildGrokSubscriptionHeaders, createGrokSubscriptionFetch, isGrokSubscriptionBaseUrl, } from "./provider-grok.js";
@@ -89,6 +89,9 @@ export function createProviderInstance(options) {
89
89
  if (protocol === "ark-responses") {
90
90
  return createArkResponsesProvider(options);
91
91
  }
92
+ if (protocol === "openai-responses") {
93
+ return createOpenAIResponsesProvider(options);
94
+ }
92
95
  if (protocol === "ai-sdk") {
93
96
  return createAiSdkProvider(options);
94
97
  }
@@ -273,6 +276,9 @@ function resolveProviderProtocol(options) {
273
276
  return options.protocol;
274
277
  const providerId = (options.providerId || "").toLowerCase();
275
278
  const baseURL = (options.baseURL || "").toLowerCase();
279
+ if (providerId === "opencode-zen") {
280
+ return "openai-responses";
281
+ }
276
282
  if (providerId === "doubao"
277
283
  && baseURL.replace(/\/+$/, "") === "https://ark.cn-beijing.volces.com/api/v3") {
278
284
  return "ark-responses";
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