@bubblebrain-ai/bubble 0.0.49 → 0.0.50

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 (38) hide show
  1. package/dist/agent/abort-errors.d.ts +4 -0
  2. package/dist/agent/abort-errors.js +30 -0
  3. package/dist/agent/provider-fingerprint.d.ts +6 -0
  4. package/dist/agent/provider-fingerprint.js +89 -0
  5. package/dist/agent/resident-history.d.ts +7 -0
  6. package/dist/agent/resident-history.js +61 -0
  7. package/dist/agent/transcript-helpers.d.ts +20 -0
  8. package/dist/agent/transcript-helpers.js +62 -0
  9. package/dist/agent/update-queue.d.ts +13 -0
  10. package/dist/agent/update-queue.js +69 -0
  11. package/dist/agent.d.ts +32 -15
  12. package/dist/agent.js +79 -297
  13. package/dist/external-runtime/grok-runtime.js +1 -1
  14. package/dist/feishu/agent-host/run-driver.js +1 -1
  15. package/dist/lsp/service.js +2 -2
  16. package/dist/model-catalog.js +8 -6
  17. package/dist/permissions/settings.d.ts +0 -1
  18. package/dist/permissions/settings.js +0 -2
  19. package/dist/provider-registry.js +13 -1
  20. package/dist/provider-transform.js +7 -8
  21. package/dist/slash-commands/commands.js +13 -9
  22. package/dist/tools/grep.js +1 -1
  23. package/dist/tui-ink/app-helpers.d.ts +43 -0
  24. package/dist/tui-ink/app-helpers.js +150 -0
  25. package/dist/tui-ink/app.d.ts +4 -11
  26. package/dist/tui-ink/app.js +56 -645
  27. package/dist/tui-ink/command-palette.d.ts +32 -0
  28. package/dist/tui-ink/command-palette.js +158 -0
  29. package/dist/tui-ink/display-reconstruct.d.ts +3 -0
  30. package/dist/tui-ink/display-reconstruct.js +82 -0
  31. package/dist/tui-ink/rewind-picker.d.ts +8 -0
  32. package/dist/tui-ink/rewind-picker.js +102 -0
  33. package/dist/tui-ink/session-picker.js +1 -1
  34. package/dist/tui-ink/stats-panel.d.ts +11 -0
  35. package/dist/tui-ink/stats-panel.js +65 -0
  36. package/dist/tui-ink/waiting-indicator.d.ts +12 -0
  37. package/dist/tui-ink/waiting-indicator.js +129 -0
  38. package/package.json +1 -1
@@ -12,3 +12,7 @@ export declare class SubagentAbortError extends AgentAbortError {
12
12
  }
13
13
  /** Shown when the model produced no user-visible content despite recovery attempts. */
14
14
  export declare const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
15
+ export declare function throwIfAborted(signal?: AbortSignal): void;
16
+ export declare function isAbortLikeError(error: unknown, signal?: AbortSignal): boolean;
17
+ export declare function isAbortError(error: unknown, signal?: AbortSignal): boolean;
18
+ export declare function summarizeInterruptError(error: unknown): string;
@@ -19,3 +19,33 @@ export class SubagentAbortError extends AgentAbortError {
19
19
  }
20
20
  /** Shown when the model produced no user-visible content despite recovery attempts. */
21
21
  export const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
22
+ export function throwIfAborted(signal) {
23
+ if (!signal?.aborted)
24
+ return;
25
+ const reason = signal.reason;
26
+ if (reason instanceof Error)
27
+ throw reason;
28
+ throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
29
+ }
30
+ export function isAbortLikeError(error, signal) {
31
+ if (signal?.aborted)
32
+ return true;
33
+ if (error instanceof AgentAbortError)
34
+ return true;
35
+ if (error instanceof DOMException && error.name === "AbortError")
36
+ return true;
37
+ if (typeof error === "object" && error !== null && error.name === "AbortError")
38
+ return true;
39
+ return false;
40
+ }
41
+ export function isAbortError(error, signal) {
42
+ return isAbortLikeError(error, signal);
43
+ }
44
+ export function summarizeInterruptError(error) {
45
+ const message = error instanceof Error
46
+ ? error.message
47
+ : typeof error === "string"
48
+ ? error
49
+ : String(error);
50
+ return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error";
51
+ }
@@ -0,0 +1,6 @@
1
+ import type { Message, ProviderMessage, ProviderMetadataProvider, ProviderRawContentBlock, ToolDefinition } from "../types.js";
2
+ export declare function appendProviderContentBlock(message: Extract<Message, {
3
+ role: "assistant";
4
+ }>, provider: ProviderMetadataProvider, block: ProviderRawContentBlock): void;
5
+ export declare function buildProviderRequestFingerprint(messages: ProviderMessage[], tools: ToolDefinition[], providerId: string, toolChoice?: string): Record<string, unknown>;
6
+ export declare function cloneProviderRawContentBlock(block: ProviderRawContentBlock): ProviderRawContentBlock;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Provider-request fingerprints and raw content-block plumbing.
3
+ *
4
+ * The fingerprint is a structural digest of the projected request (roles,
5
+ * char volumes, JSON byte sizes) used to detect whether a retried request
6
+ * actually changed, and to trace what the model saw.
7
+ */
8
+ import { estimateContextTokens } from "../context/budget.js";
9
+ export function appendProviderContentBlock(message, provider, block) {
10
+ const current = message.providerMetadata?.[provider]?.contentBlocks ?? [];
11
+ message.providerMetadata = {
12
+ ...message.providerMetadata,
13
+ [provider]: {
14
+ ...message.providerMetadata?.[provider],
15
+ contentBlocks: [...current, cloneProviderRawContentBlock(block)],
16
+ },
17
+ };
18
+ }
19
+ export function buildProviderRequestFingerprint(messages, tools, providerId, toolChoice) {
20
+ const roleCounts = {};
21
+ let contentChars = 0;
22
+ let reasoningChars = 0;
23
+ let toolResultChars = 0;
24
+ let maxToolResultChars = 0;
25
+ let assistantToolCalls = 0;
26
+ let rawAnthropicBlocks = 0;
27
+ let rawAnthropicThinkingBlocks = 0;
28
+ let rawAnthropicSignatureChars = 0;
29
+ for (const message of messages) {
30
+ roleCounts[message.role] = (roleCounts[message.role] ?? 0) + 1;
31
+ if (message.role === "assistant") {
32
+ contentChars += message.content.length;
33
+ reasoningChars += message.reasoning?.length ?? 0;
34
+ assistantToolCalls += message.toolCalls?.length ?? 0;
35
+ const blocks = message.providerMetadata?.anthropic?.contentBlocks ?? [];
36
+ rawAnthropicBlocks += blocks.length;
37
+ for (const block of blocks) {
38
+ if (block.type === "thinking" || block.type === "redacted_thinking") {
39
+ rawAnthropicThinkingBlocks += 1;
40
+ }
41
+ if (typeof block.signature === "string") {
42
+ rawAnthropicSignatureChars += block.signature.length;
43
+ }
44
+ }
45
+ }
46
+ else if (message.role === "tool") {
47
+ toolResultChars += message.content.length;
48
+ maxToolResultChars = Math.max(maxToolResultChars, message.content.length);
49
+ }
50
+ else if (message.role === "user") {
51
+ contentChars += typeof message.content === "string"
52
+ ? message.content.length
53
+ : message.content.reduce((sum, part) => sum + (part.type === "text" ? part.text.length : part.image_url.url.length), 0);
54
+ }
55
+ else {
56
+ contentChars += message.content.length;
57
+ }
58
+ }
59
+ const systemMessages = messages.filter((message) => message.role === "system");
60
+ const bodyMessages = messages.filter((message) => message.role !== "system");
61
+ const systemJsonBytes = Buffer.byteLength(JSON.stringify(systemMessages), "utf8");
62
+ const bodyJsonBytes = Buffer.byteLength(JSON.stringify(bodyMessages), "utf8");
63
+ const toolSchemaJsonBytes = Buffer.byteLength(JSON.stringify(tools), "utf8");
64
+ return {
65
+ roleCounts,
66
+ estimatedTokens: estimateContextTokens(messages, providerId),
67
+ projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
68
+ systemJsonBytes,
69
+ bodyJsonBytes,
70
+ toolSchemaJsonBytes,
71
+ staticPrefixJsonBytes: Buffer.byteLength(JSON.stringify({
72
+ system: systemMessages,
73
+ tools,
74
+ tool_choice: toolChoice,
75
+ }), "utf8"),
76
+ toolChoice,
77
+ contentChars,
78
+ reasoningChars,
79
+ toolResultChars,
80
+ maxToolResultChars,
81
+ assistantToolCalls,
82
+ rawAnthropicBlocks,
83
+ rawAnthropicThinkingBlocks,
84
+ rawAnthropicSignatureChars,
85
+ };
86
+ }
87
+ export function cloneProviderRawContentBlock(block) {
88
+ return JSON.parse(JSON.stringify(block));
89
+ }
@@ -0,0 +1,7 @@
1
+ import type { Message } from "../types.js";
2
+ export declare const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
3
+ export declare const RESIDENT_HISTORY_HEAP_HARD_LIMIT: number;
4
+ export declare function estimateResidentChars(messages: Message[]): number;
5
+ export declare function estimateToolPayloadChars(messages: Message[]): number;
6
+ export declare function countUserTurns(messages: Message[]): number;
7
+ export declare function getCurrentHeapUsed(): number;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resident-history size estimation and the cliff-edge guards that decide when
3
+ * in-memory history is compacted (see docs/harness-thinning.md, wave 2).
4
+ */
5
+ import { isRealUserMessage } from "../context/compact.js";
6
+ export const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
7
+ export const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
8
+ export function estimateResidentChars(messages) {
9
+ let total = 0;
10
+ for (const message of messages) {
11
+ switch (message.role) {
12
+ case "system":
13
+ case "meta":
14
+ total += message.content.length;
15
+ break;
16
+ case "tool":
17
+ total += message.content.length + message.toolCallId.length;
18
+ break;
19
+ case "assistant":
20
+ total += message.content.length + (message.reasoning?.length ?? 0);
21
+ total += message.toolCalls?.reduce((sum, toolCall) => sum + toolCall.id.length + toolCall.name.length + toolCall.arguments.length, 0) ?? 0;
22
+ break;
23
+ case "user":
24
+ if (typeof message.content === "string") {
25
+ total += message.content.length;
26
+ }
27
+ else {
28
+ total += message.content.reduce((sum, part) => {
29
+ if (part.type === "text") {
30
+ return sum + part.text.length;
31
+ }
32
+ return sum + part.image_url.url.length;
33
+ }, 0);
34
+ }
35
+ break;
36
+ }
37
+ }
38
+ return total;
39
+ }
40
+ export function estimateToolPayloadChars(messages) {
41
+ return messages.reduce((sum, message) => {
42
+ if (message.role !== "tool") {
43
+ return sum;
44
+ }
45
+ return sum + message.content.length;
46
+ }, 0);
47
+ }
48
+ export function countUserTurns(messages) {
49
+ // Real user turns only: projected reminders and summary envelopes are
50
+ // user-ROLE but not user TURNS — counting them used to shrink the keep
51
+ // window and fire compaction almost every turn.
52
+ return messages.reduce((count, message) => count + (isRealUserMessage(message) ? 1 : 0), 0);
53
+ }
54
+ export function getCurrentHeapUsed() {
55
+ try {
56
+ return process.memoryUsage().heapUsed;
57
+ }
58
+ catch {
59
+ return 0;
60
+ }
61
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Transcript-level helpers: interrupted-run boundaries, the cancelled-tool
3
+ * sentinel, required-arg validation and the subagent-lifecycle tool-name
4
+ * predicate used when forking context for children.
5
+ */
6
+ import type { Message, ToolResult } from "../types.js";
7
+ export declare function findMissingRequiredArgs(schema: {
8
+ required?: string[];
9
+ } | undefined, args: Record<string, any> | undefined): string[];
10
+ export declare function shouldAppendModelInterruptedBoundary(messages: Message[]): boolean;
11
+ export declare function createModelInterruptedMessage(error: unknown, metadata: {
12
+ model: string;
13
+ providerId: string;
14
+ modelId: string;
15
+ }): Extract<Message, {
16
+ role: "assistant";
17
+ }>;
18
+ export declare function lastProviderMessage(messages: Message[]): Message | undefined;
19
+ export declare function cancelledToolResult(toolName: string): ToolResult;
20
+ export declare function isSubagentLifecycleTool(name: string): boolean;
@@ -0,0 +1,62 @@
1
+ import { summarizeInterruptError } from "./abort-errors.js";
2
+ export function findMissingRequiredArgs(schema, args) {
3
+ const required = schema?.required;
4
+ if (!required || required.length === 0)
5
+ return [];
6
+ const missing = [];
7
+ for (const name of required) {
8
+ const value = args ? args[name] : undefined;
9
+ // Empty strings/arrays are intentionally allowed — writing an empty file
10
+ // or passing an empty list can be legitimate. Only undefined/null counts
11
+ // as "missing", because the observed failure mode is `finalArgs: "{}"`
12
+ // where the field is entirely absent.
13
+ if (value === undefined || value === null) {
14
+ missing.push(name);
15
+ }
16
+ }
17
+ return missing;
18
+ }
19
+ export function shouldAppendModelInterruptedBoundary(messages) {
20
+ return messages.at(-1)?.role === "tool";
21
+ }
22
+ export function createModelInterruptedMessage(error, metadata) {
23
+ return {
24
+ role: "assistant",
25
+ content: `[model request interrupted before a final answer was produced: ${summarizeInterruptError(error)}]`,
26
+ model: metadata.model,
27
+ providerId: metadata.providerId,
28
+ modelId: metadata.modelId,
29
+ };
30
+ }
31
+ export function lastProviderMessage(messages) {
32
+ for (let i = messages.length - 1; i >= 0; i--) {
33
+ const message = messages[i];
34
+ if (message.role === "system" || message.role === "meta")
35
+ continue;
36
+ return message;
37
+ }
38
+ return undefined;
39
+ }
40
+ export function cancelledToolResult(toolName) {
41
+ return {
42
+ content: `Tool "${toolName}" was cancelled.`,
43
+ isError: true,
44
+ status: "cancelled",
45
+ metadata: { reason: "cancelled" },
46
+ };
47
+ }
48
+ export function isSubagentLifecycleTool(name) {
49
+ return name === "subagent"
50
+ || name === "spawn_agent"
51
+ || name === "wait_agent"
52
+ || name === "send_input"
53
+ || name === "close_agent"
54
+ || name === "list_agents"
55
+ || name === "run_workflow"
56
+ || name === "wait_workflow"
57
+ // Legacy names: still present in transcripts recorded before the tools
58
+ // were removed (2026-07-06); forked children must not inherit their
59
+ // dangling tool_calls either.
60
+ || name === "agent_team"
61
+ || name === "agent_batch";
62
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Wake-latched update queue shared by the agent run loop: tool updates,
3
+ * subagent deliveries and workflow completions all park here until the loop
4
+ * is ready to drain them.
5
+ */
6
+ /** Exported for tests: the wake latch below is easiest to pin directly. */
7
+ export declare function createUpdateQueue<T>(): {
8
+ push(item: T): void;
9
+ drain(): T[];
10
+ hasItems(): boolean;
11
+ wait(signal?: AbortSignal): Promise<"woken" | "aborted">;
12
+ wake(): void;
13
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Wake-latched update queue shared by the agent run loop: tool updates,
3
+ * subagent deliveries and workflow completions all park here until the loop
4
+ * is ready to drain them.
5
+ */
6
+ /** Exported for tests: the wake latch below is easiest to pin directly. */
7
+ export function createUpdateQueue() {
8
+ const items = [];
9
+ let waiter;
10
+ let abortCleanup;
11
+ // Latch for wakes that arrive with no parked waiter — a wake() fired while
12
+ // the consuming generator is suspended at a yield would otherwise be lost.
13
+ // Callers today each pair their wake with a synchronously-checked flag
14
+ // (items / settled / pendingSubagentUpdates), so no wake is currently
15
+ // dropped; the latch keeps that correctness inside the queue instead of
16
+ // depending on every future caller repeating the pairing.
17
+ let signaled = false;
18
+ return {
19
+ push(item) {
20
+ items.push(item);
21
+ this.wake();
22
+ },
23
+ drain() {
24
+ return items.splice(0, items.length);
25
+ },
26
+ hasItems() {
27
+ return items.length > 0;
28
+ },
29
+ wait(signal) {
30
+ if (items.length > 0)
31
+ return Promise.resolve("woken");
32
+ if (signaled) {
33
+ signaled = false;
34
+ return Promise.resolve("woken");
35
+ }
36
+ if (signal?.aborted)
37
+ return Promise.resolve("aborted");
38
+ return new Promise((resolve) => {
39
+ abortCleanup?.();
40
+ abortCleanup = undefined;
41
+ const finish = (status) => {
42
+ if (waiter !== resolve)
43
+ return;
44
+ waiter = undefined;
45
+ abortCleanup?.();
46
+ abortCleanup = undefined;
47
+ resolve(status);
48
+ };
49
+ if (signal) {
50
+ const onAbort = () => finish("aborted");
51
+ signal.addEventListener("abort", onAbort, { once: true });
52
+ abortCleanup = () => signal.removeEventListener("abort", onAbort);
53
+ }
54
+ waiter = resolve;
55
+ });
56
+ },
57
+ wake() {
58
+ const resolve = waiter;
59
+ if (!resolve) {
60
+ signaled = true;
61
+ return;
62
+ }
63
+ waiter = undefined;
64
+ abortCleanup?.();
65
+ abortCleanup = undefined;
66
+ resolve("woken");
67
+ },
68
+ };
69
+ }
package/dist/agent.d.ts CHANGED
@@ -6,13 +6,15 @@ import { type ContextUsageSnapshot } from "./context/usage.js";
6
6
  import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
7
7
  import { type TurnHooks } from "./orchestrator/hooks.js";
8
8
  import type { ExternalHookController } from "./hooks/controller.js";
9
- import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "./agent/categories.js";
9
+ import type { AgentCategoriesConfig, ResolvedSubagentRoute } from "./agent/categories.js";
10
10
  import { type AgentRoutingConfig, type RoutableModelEntry, type RoutableModelIndex, type RoutingSnapshotAccessor } from "./agent/routing-catalog.js";
11
11
  import { SubagentRuntime } from "./agent/subagent/runtime.js";
12
- import { type WorkflowRunSnapshot } from "./agent/workflow/control.js";
13
12
  import { BudgetLedger } from "./agent/budget-ledger.js";
14
- import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
15
- import { type SubagentThreadSnapshot } from "./agent/subagent-control.js";
13
+ import { type WorkflowRunSnapshot } from "./agent/workflow/control.js";
14
+ import type { AgentProfile, SubagentRunResult } from "./agent/profiles.js";
15
+ import type { SubagentThreadSnapshot } from "./agent/subagent-control.js";
16
+ import { SubagentStore } from "./agent/subagent-store.js";
17
+ export { createUpdateQueue } from "./agent/update-queue.js";
16
18
  import { type RateLimitPolicy } from "./network/errors.js";
17
19
  import type { SkillSummary } from "./skills/types.js";
18
20
  import type { FileStateTracker } from "./tools/file-state.js";
@@ -108,7 +110,18 @@ export interface AgentRunOptions {
108
110
  resumeWithoutInput?: boolean;
109
111
  }
110
112
  export declare class Agent {
111
- messages: Message[];
113
+ /**
114
+ * Backing store for the public `messages` accessor. All whole-array
115
+ * rewrites (clear/compact/rewind/session switch/subagent fork) go through
116
+ * the setter, which notifies `onContextChanged` subscribers — that push
117
+ * channel is what keeps derived UI (e.g. the footer usage readout) in sync
118
+ * without per-path refresh calls that are easy to forget.
119
+ */
120
+ private _messages;
121
+ private contextChangedListeners;
122
+ private notifyingContextChange;
123
+ get messages(): Message[];
124
+ set messages(value: Message[]);
112
125
  private provider;
113
126
  private sessionID?;
114
127
  /**
@@ -226,6 +239,15 @@ export declare class Agent {
226
239
  providerId: string;
227
240
  model: string;
228
241
  }): string | undefined;
242
+ /**
243
+ * Push subscription for derived-context consumers (TUI footer, hosts).
244
+ * Fires whenever resident context mutates in a way that changes a
245
+ * `getContextUsageSnapshot()` reading: whole-array message rewrites,
246
+ * appended messages, system-prompt replacement, or model/provider switches
247
+ * (window size / estimation basis). Returns an unsubscribe function.
248
+ */
249
+ onContextChanged(listener: () => void): () => void;
250
+ private notifyContextChanged;
229
251
  getContextUsageSnapshot(): ContextUsageSnapshot;
230
252
  resetContextUsageAnchor(): void;
231
253
  /** Whether a given tool is deferred and not yet unlocked. */
@@ -326,7 +348,7 @@ export declare class Agent {
326
348
  * `(agent as any).subagentStore`; keeping this forward means the extraction
327
349
  * changed no caller, test or otherwise.
328
350
  */
329
- private get subagentStore();
351
+ get subagentStore(): SubagentStore;
330
352
  /** run_workflow children are workflowInternal and invisible to listSubAgents. */
331
353
  hasRunningWorkflow(): boolean;
332
354
  /**
@@ -393,7 +415,10 @@ export declare class Agent {
393
415
  * haiku for these twenty scouts" per spawn/batch member at request time.
394
416
  */
395
417
  /** Kept as a thin delegator: routing decisions now live in SubagentRouter. */
396
- private resolveRouteForSubagent;
418
+ resolveRouteForSubagent(profile: AgentProfile, category: string | undefined, override?: {
419
+ model?: string;
420
+ effort?: ThinkingLevel;
421
+ }): ResolvedSubagentRoute;
397
422
  /** Routable catalog across runnable providers (design v3.6); undefined when unwired. */
398
423
  listRoutableModels(): RoutableModelEntry[] | undefined;
399
424
  /**
@@ -418,11 +443,3 @@ export declare class Agent {
418
443
  private clearTodosAfterInterruptedRun;
419
444
  private executeTool;
420
445
  }
421
- /** Exported for tests: the wake latch below is easiest to pin directly. */
422
- export declare function createUpdateQueue<T>(): {
423
- push(item: T): void;
424
- drain(): T[];
425
- hasItems(): boolean;
426
- wait(signal?: AbortSignal): Promise<"woken" | "aborted">;
427
- wake(): void;
428
- };