@bubblebrain-ai/bubble 0.0.28 → 0.0.29

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 (59) hide show
  1. package/README.md +21 -0
  2. package/dist/agent/categories.d.ts +2 -0
  3. package/dist/agent/categories.js +4 -0
  4. package/dist/agent/child-runner.d.ts +5 -1
  5. package/dist/agent/child-runner.js +35 -2
  6. package/dist/agent/profiles.js +3 -0
  7. package/dist/agent/structured-output.d.ts +37 -0
  8. package/dist/agent/structured-output.js +193 -0
  9. package/dist/agent/subagent-control.d.ts +3 -0
  10. package/dist/agent/subagent-scheduler.d.ts +10 -0
  11. package/dist/agent/subagent-scheduler.js +31 -0
  12. package/dist/agent/workflow/control.d.ts +37 -0
  13. package/dist/agent/workflow/control.js +20 -0
  14. package/dist/agent/workflow/errors.d.ts +16 -0
  15. package/dist/agent/workflow/errors.js +24 -0
  16. package/dist/agent/workflow/runtime.d.ts +75 -0
  17. package/dist/agent/workflow/runtime.js +237 -0
  18. package/dist/agent.d.ts +105 -0
  19. package/dist/agent.js +425 -17
  20. package/dist/context/compact-llm.d.ts +10 -1
  21. package/dist/context/compact-llm.js +13 -5
  22. package/dist/context/compact.d.ts +30 -0
  23. package/dist/context/compact.js +34 -17
  24. package/dist/network/provider-transport.d.ts +9 -0
  25. package/dist/network/provider-transport.js +19 -1
  26. package/dist/provider.d.ts +14 -0
  27. package/dist/provider.js +24 -0
  28. package/dist/session.d.ts +16 -0
  29. package/dist/session.js +33 -1
  30. package/dist/slash-commands/commands.js +47 -1
  31. package/dist/slash-commands/types.d.ts +16 -1
  32. package/dist/tools/agent-lifecycle.d.ts +6 -0
  33. package/dist/tools/agent-lifecycle.js +285 -0
  34. package/dist/tools/child-tools.d.ts +10 -0
  35. package/dist/tools/child-tools.js +12 -0
  36. package/dist/tools/read.d.ts +1 -1
  37. package/dist/tools/read.js +9 -0
  38. package/dist/tui/image-display.d.ts +6 -0
  39. package/dist/tui/image-display.js +26 -1
  40. package/dist/tui-ink/app.js +84 -6
  41. package/dist/tui-ink/compaction-progress.d.ts +19 -0
  42. package/dist/tui-ink/compaction-progress.js +74 -0
  43. package/dist/tui-ink/input-box.d.ts +7 -1
  44. package/dist/tui-ink/input-box.js +48 -15
  45. package/dist/tui-ink/markdown.d.ts +18 -0
  46. package/dist/tui-ink/markdown.js +172 -16
  47. package/dist/tui-ink/message-list.js +38 -94
  48. package/dist/tui-ink/run.js +5 -0
  49. package/dist/tui-ink/subagent-inspector.d.ts +17 -0
  50. package/dist/tui-ink/subagent-inspector.js +189 -0
  51. package/dist/tui-ink/subagent-view.d.ts +47 -0
  52. package/dist/tui-ink/subagent-view.js +163 -0
  53. package/dist/tui-ink/terminal-env.d.ts +15 -0
  54. package/dist/tui-ink/terminal-env.js +22 -0
  55. package/dist/tui-ink/use-terminal-size.js +33 -6
  56. package/dist/tui-ink/width.d.ts +18 -0
  57. package/dist/tui-ink/width.js +130 -0
  58. package/dist/types.d.ts +35 -0
  59. package/package.json +2 -1
@@ -1,6 +1,5 @@
1
- export function compactSessionEntries(entries, options = {}) {
1
+ export function planSessionCompaction(entries, options = {}) {
2
2
  const keepRecentTurns = options.keepRecentTurns ?? 2;
3
- const maxSummaryItems = options.maxSummaryItems ?? 4;
4
3
  const metadataEntries = entries.filter((entry) => entry.type === "metadata");
5
4
  const nonMetadataEntries = entries.filter((entry) => entry.type !== "metadata");
6
5
  const latestSummaryIndex = findLatestSummaryIndex(nonMetadataEntries);
@@ -10,34 +9,52 @@ export function compactSessionEntries(entries, options = {}) {
10
9
  .map((entry, index) => (entry.type === "user_message" ? index : -1))
11
10
  .filter((index) => index >= 0);
12
11
  if (turnStartIndexes.length <= keepRecentTurns) {
13
- return { compacted: false };
12
+ return { compactable: false };
14
13
  }
15
14
  const keepStartIndex = turnStartIndexes[Math.max(0, turnStartIndexes.length - keepRecentTurns)];
16
15
  if (keepStartIndex <= 0) {
17
- return { compacted: false };
18
- }
19
- const oldEntries = activeEntries.slice(0, keepStartIndex);
20
- const keptEntries = activeEntries.slice(keepStartIndex);
21
- const summary = buildCompactionSummary(oldEntries, maxSummaryItems);
22
- if (!summary) {
23
- return { compacted: false };
16
+ return { compactable: false };
24
17
  }
18
+ return {
19
+ compactable: true,
20
+ metadataEntries,
21
+ oldEntries: activeEntries.slice(0, keepStartIndex),
22
+ keptEntries: activeEntries.slice(keepStartIndex),
23
+ };
24
+ }
25
+ /**
26
+ * Assemble the post-compaction entry list from a plan and a (possibly
27
+ * LLM-generated) summary string. The summary entry is keyed off the full
28
+ * original `entries` so its id never collides with a prior summary.
29
+ */
30
+ export function buildCompactedEntries(entries, plan, summary) {
25
31
  const summaryEntry = {
26
32
  id: nextSummaryId(entries),
27
33
  type: "summary",
28
34
  summary,
29
35
  timestamp: Date.now(),
30
36
  };
31
- const nextEntries = [
32
- ...metadataEntries,
33
- summaryEntry,
34
- ...keptEntries,
35
- ];
37
+ return [...plan.metadataEntries, summaryEntry, ...plan.keptEntries];
38
+ }
39
+ /** Flatten a plan's old entries into messages for an external summarizer. */
40
+ export function planOldMessages(plan) {
41
+ return entriesToMessages(plan.oldEntries);
42
+ }
43
+ export function compactSessionEntries(entries, options = {}) {
44
+ const maxSummaryItems = options.maxSummaryItems ?? 4;
45
+ const plan = planSessionCompaction(entries, options);
46
+ if (!plan.compactable) {
47
+ return { compacted: false };
48
+ }
49
+ const summary = buildCompactionSummary(plan.oldEntries, maxSummaryItems);
50
+ if (!summary) {
51
+ return { compacted: false };
52
+ }
36
53
  return {
37
54
  compacted: true,
38
55
  summary,
39
- entries: nextEntries,
40
- droppedEntries: oldEntries.length,
56
+ entries: buildCompactedEntries(entries, plan, summary),
57
+ droppedEntries: plan.oldEntries.length,
41
58
  };
42
59
  }
43
60
  export function compactMessages(messages, options = {}) {
@@ -28,5 +28,14 @@ export declare function normalizeProviderNetworkError(error: unknown, options: {
28
28
  env?: NodeJS.ProcessEnv;
29
29
  }): Error;
30
30
  export declare function isProviderTransportError(error: unknown): boolean;
31
+ /**
32
+ * Request/response timeouts surface as prose rather than errno tokens — e.g.
33
+ * Bun fetch throws a DOMException named "TimeoutError" with message
34
+ * "The operation timed out.", and openai-node raises APIConnectionTimeoutError.
35
+ * These are kept OUT of isProviderNetworkErrorText on purpose: that predicate
36
+ * drives normalizeProviderNetworkError's proxy/TLS/CA advice, and a plain
37
+ * timeout must not be rewrapped into a misleading "check your proxy" message.
38
+ */
39
+ export declare function isProviderTimeoutErrorText(text: string): boolean;
31
40
  export declare function shouldEnableFetchVerbose(env?: NodeJS.ProcessEnv, providerVerboseEnvVar?: string): boolean;
32
41
  export {};
@@ -99,7 +99,25 @@ export function normalizeProviderNetworkError(error, options) {
99
99
  return new Error(message, { cause: error });
100
100
  }
101
101
  export function isProviderTransportError(error) {
102
- return isProviderNetworkErrorText(errorMessageChain(error).join("\n"));
102
+ const text = errorMessageChain(error).join("\n");
103
+ return isProviderNetworkErrorText(text) || isProviderTimeoutErrorText(text);
104
+ }
105
+ /**
106
+ * Request/response timeouts surface as prose rather than errno tokens — e.g.
107
+ * Bun fetch throws a DOMException named "TimeoutError" with message
108
+ * "The operation timed out.", and openai-node raises APIConnectionTimeoutError.
109
+ * These are kept OUT of isProviderNetworkErrorText on purpose: that predicate
110
+ * drives normalizeProviderNetworkError's proxy/TLS/CA advice, and a plain
111
+ * timeout must not be rewrapped into a misleading "check your proxy" message.
112
+ */
113
+ export function isProviderTimeoutErrorText(text) {
114
+ return [
115
+ /operation timed out/i,
116
+ /request timed out/i,
117
+ /\bTimeoutError\b/i,
118
+ /\bAPIConnectionTimeoutError\b/i,
119
+ /\bESOCKETTIMEDOUT\b/i,
120
+ ].some((pattern) => pattern.test(text));
103
121
  }
104
122
  export function shouldEnableFetchVerbose(env = process.env, providerVerboseEnvVar) {
105
123
  const providerValue = providerVerboseEnvVar ? env[providerVerboseEnvVar] : undefined;
@@ -54,4 +54,18 @@ export declare function translateOpenAIFullResponse(response: any): AsyncIterabl
54
54
  * in index order to keep multi-call turns deterministic.
55
55
  */
56
56
  export declare function translateOpenAIStream(stream: AsyncIterable<any>, options?: TranslateOpenAIStreamOptions): AsyncIterable<StreamChunk>;
57
+ /** Largest value Node's 32-bit timers accept; ~24.8 days. */
58
+ export declare const MAX_TIMER_MS = 2147483647;
59
+ /**
60
+ * Resolve the provider request timeout (ms) from the operator override.
61
+ *
62
+ * Default is effectively NO TIMEOUT — safe for streaming APIs where the model
63
+ * sends chunks continuously. But Node's timers are 32-bit: a duration above
64
+ * 2**31-1 ms overflows, which makes Node print a TimeoutOverflowWarning to
65
+ * stderr (corrupting the Ink TUI) AND silently clamp the timeout to 1ms,
66
+ * aborting the request almost immediately. So we use the largest SAFE timer
67
+ * value as the "no timeout" sentinel — never Number.MAX_SAFE_INTEGER — and
68
+ * clamp any operator-supplied value into range too.
69
+ */
70
+ export declare function resolveRequestTimeoutMs(raw: string | undefined): number;
57
71
  export {};
package/dist/provider.js CHANGED
@@ -94,6 +94,7 @@ export function createProviderInstance(options) {
94
94
  const client = new OpenAI({
95
95
  apiKey: options.apiKey,
96
96
  baseURL: options.baseURL,
97
+ timeout: resolveRequestTimeoutMs(process.env.BUBBLE_PROVIDER_REQUEST_TIMEOUT_MS),
97
98
  });
98
99
  const fallbackModel = "gpt-4o";
99
100
  async function* streamChat(messages, chatOptions) {
@@ -704,6 +705,29 @@ function mergeToolArgumentDelta(current, incoming, mode) {
704
705
  debugToolArgs({ stage: "merge", branch: mode === "delta" ? "delta-append" : "snapshot-fallback-concat", current, incoming, args: current + incoming, delta: incoming });
705
706
  return { args: current + incoming, delta: incoming };
706
707
  }
708
+ function parsePositiveInt(raw) {
709
+ if (!raw?.trim())
710
+ return undefined;
711
+ const value = Number(raw);
712
+ return Number.isInteger(value) && value > 0 ? value : undefined;
713
+ }
714
+ /** Largest value Node's 32-bit timers accept; ~24.8 days. */
715
+ export const MAX_TIMER_MS = 2_147_483_647; // 2**31 - 1
716
+ /**
717
+ * Resolve the provider request timeout (ms) from the operator override.
718
+ *
719
+ * Default is effectively NO TIMEOUT — safe for streaming APIs where the model
720
+ * sends chunks continuously. But Node's timers are 32-bit: a duration above
721
+ * 2**31-1 ms overflows, which makes Node print a TimeoutOverflowWarning to
722
+ * stderr (corrupting the Ink TUI) AND silently clamp the timeout to 1ms,
723
+ * aborting the request almost immediately. So we use the largest SAFE timer
724
+ * value as the "no timeout" sentinel — never Number.MAX_SAFE_INTEGER — and
725
+ * clamp any operator-supplied value into range too.
726
+ */
727
+ export function resolveRequestTimeoutMs(raw) {
728
+ const requested = parsePositiveInt(raw);
729
+ return Math.min(requested ?? MAX_TIMER_MS, MAX_TIMER_MS);
730
+ }
707
731
  function mergeStreamingText(current, incoming, mode) {
708
732
  if (!current)
709
733
  return { args: incoming, delta: incoming };
package/dist/session.d.ts CHANGED
@@ -57,6 +57,22 @@ export declare class SessionManager {
57
57
  appendTodosSnapshot(todos: Todo[]): void;
58
58
  getTodos(): Todo[];
59
59
  compact(options?: CompactOptions): CompactResult;
60
+ /**
61
+ * Inspect whether the session is large enough to compact and, if so, return
62
+ * the older messages an external summarizer should condense. Returns null
63
+ * when there isn't enough history past the last summary to bother — the
64
+ * caller should then report "already compact enough" without calling a model.
65
+ */
66
+ getCompactionPlan(options?: CompactOptions): {
67
+ oldMessages: Message[];
68
+ } | null;
69
+ /**
70
+ * Apply a precomputed (typically LLM-generated) summary as the compaction
71
+ * checkpoint, rewriting the log to [metadata, summary, kept turns]. Mirrors
72
+ * `compact()` but skips the built-in heuristic summarizer. Returns
73
+ * `{ compacted: false }` if the session is no longer compactable.
74
+ */
75
+ applyLLMCompaction(summary: string, options?: CompactOptions): CompactResult;
60
76
  getMessages(): Message[];
61
77
  /**
62
78
  * Pre-edit file snapshot store for this session, used by /rewind.
package/dist/session.js CHANGED
@@ -6,7 +6,7 @@ import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, statS
6
6
  import { basename, dirname, join } from "node:path";
7
7
  import { getBubbleHome } from "./bubble-home.js";
8
8
  import { CheckpointStore } from "./checkpoints.js";
9
- import { compactSessionEntries } from "./context/compact.js";
9
+ import { buildCompactedEntries, compactSessionEntries, planOldMessages, planSessionCompaction, } from "./context/compact.js";
10
10
  import { SessionLog } from "./session-log.js";
11
11
  import { normalizeSingleLine, truncateVisual } from "./text-display.js";
12
12
  import { deterministicTitleFromUserContent } from "./session-title.js";
@@ -162,6 +162,38 @@ export class SessionManager {
162
162
  }
163
163
  return result;
164
164
  }
165
+ /**
166
+ * Inspect whether the session is large enough to compact and, if so, return
167
+ * the older messages an external summarizer should condense. Returns null
168
+ * when there isn't enough history past the last summary to bother — the
169
+ * caller should then report "already compact enough" without calling a model.
170
+ */
171
+ getCompactionPlan(options) {
172
+ const plan = planSessionCompaction(this.log.list(), options);
173
+ if (!plan.compactable)
174
+ return null;
175
+ return { oldMessages: planOldMessages(plan) };
176
+ }
177
+ /**
178
+ * Apply a precomputed (typically LLM-generated) summary as the compaction
179
+ * checkpoint, rewriting the log to [metadata, summary, kept turns]. Mirrors
180
+ * `compact()` but skips the built-in heuristic summarizer. Returns
181
+ * `{ compacted: false }` if the session is no longer compactable.
182
+ */
183
+ applyLLMCompaction(summary, options) {
184
+ const entries = this.log.list();
185
+ const plan = planSessionCompaction(entries, options);
186
+ if (!plan.compactable)
187
+ return { compacted: false };
188
+ const nextEntries = buildCompactedEntries(entries, plan, summary);
189
+ this.rewrite(nextEntries);
190
+ return {
191
+ compacted: true,
192
+ summary,
193
+ entries: nextEntries,
194
+ droppedEntries: plan.oldEntries.length,
195
+ };
196
+ }
165
197
  getMessages() {
166
198
  return this.log.toMessages();
167
199
  }
@@ -306,6 +306,13 @@ const builtinSlashCommandEntries = [
306
306
  ctx.openPicker("skill");
307
307
  },
308
308
  },
309
+ {
310
+ name: "agents",
311
+ description: "Inspect spawned subagents and their working traces (also Ctrl+G)",
312
+ async handler(_args, ctx) {
313
+ ctx.openPicker("agents");
314
+ },
315
+ },
309
316
  {
310
317
  name: "help",
311
318
  description: "Show available slash commands",
@@ -978,7 +985,46 @@ const builtinSlashCommandEntries = [
978
985
  if (preHook?.decision === "deny") {
979
986
  return preHook.reason ?? `Compaction blocked by hook ${preHook.sourceHookId ?? "<unknown>"}.`;
980
987
  }
981
- const result = ctx.sessionManager.compact();
988
+ // Plan first so we can report "already compact" without spending a model
989
+ // call, and so the LLM summarizer gets the exact set of evicted messages.
990
+ const plan = ctx.sessionManager.getCompactionPlan();
991
+ if (!plan) {
992
+ await ctx.hookController?.runEvent({
993
+ eventName: "PostCompact",
994
+ cwd: ctx.cwd,
995
+ sessionId: ctx.sessionManager.getSessionFile(),
996
+ agentRole: "driver",
997
+ target: "manual",
998
+ payload: { kind: "manual", compacted: false },
999
+ });
1000
+ return "Session is already compact enough.";
1001
+ }
1002
+ // Stream an LLM summary for high fidelity, reporting progress to the TUI.
1003
+ // On any failure (or empty output) fall back to the instant heuristic
1004
+ // compaction so /compact always makes progress.
1005
+ let result;
1006
+ try {
1007
+ ctx.compactionProgress?.({ phase: "collecting", streamedChars: 0 });
1008
+ let summary = "";
1009
+ try {
1010
+ summary = await ctx.agent.summarizeForCompaction(plan.oldMessages, (full) => {
1011
+ ctx.compactionProgress?.({ phase: "summarizing", streamedChars: full.length });
1012
+ });
1013
+ }
1014
+ catch {
1015
+ summary = "";
1016
+ }
1017
+ if (summary) {
1018
+ ctx.compactionProgress?.({ phase: "applying", streamedChars: summary.length });
1019
+ result = ctx.sessionManager.applyLLMCompaction(summary);
1020
+ }
1021
+ else {
1022
+ result = ctx.sessionManager.compact();
1023
+ }
1024
+ }
1025
+ finally {
1026
+ ctx.compactionProgress?.(null);
1027
+ }
982
1028
  if (!result.compacted) {
983
1029
  await ctx.hookController?.runEvent({
984
1030
  eventName: "PostCompact",
@@ -11,6 +11,16 @@ import type { MemoryScope } from "../memory/index.js";
11
11
  import type { ThemeMode } from "../config.js";
12
12
  import type { ExternalHookController } from "../hooks/controller.js";
13
13
  export type SidebarMode = "auto" | "expanded" | "collapsed";
14
+ /**
15
+ * Live progress for a manual `/compact` run, pushed to the TUI so it can render
16
+ * a progress bar. `phase` advances collecting → summarizing → applying;
17
+ * `streamedChars` is the running length of the streamed summary (drives the
18
+ * bar's fill within the summarizing phase). Hosts without a UI omit the sink.
19
+ */
20
+ export interface CompactionProgress {
21
+ phase: "collecting" | "summarizing" | "applying";
22
+ streamedChars: number;
23
+ }
14
24
  export interface SidebarCommandState {
15
25
  mode: SidebarMode;
16
26
  visible: boolean;
@@ -24,7 +34,7 @@ export interface SlashCommandContext {
24
34
  exit: () => void;
25
35
  sessionManager?: SessionManager;
26
36
  createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
27
- openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup", providerId?: string) => void;
37
+ openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup" | "agents", providerId?: string) => void;
28
38
  registry: ProviderRegistry;
29
39
  skillRegistry: SkillRegistry;
30
40
  bashAllowlist?: BashAllowlist;
@@ -56,6 +66,11 @@ export interface SlashCommandContext {
56
66
  fillComposer?: (text: string) => void;
57
67
  /** Open the interactive usage stats panel. */
58
68
  openStats?: () => void;
69
+ /**
70
+ * Push live compaction progress to the running TUI. Pass a progress object
71
+ * while compacting and `null` to clear the indicator. Absent in non-TUI hosts.
72
+ */
73
+ compactionProgress?: (progress: CompactionProgress | null) => void;
59
74
  }
60
75
  /**
61
76
  * Return types for a slash command handler:
@@ -28,5 +28,11 @@ export declare function createListAgentsTool(): ToolRegistryEntry;
28
28
  export declare const AGENT_TEAM_MIN_ITEMS = 2;
29
29
  export declare const AGENT_TEAM_MAX_ITEMS = 32;
30
30
  export declare function createAgentTeamTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
31
+ /** Specs bound for one agent_batch call (design v2 §1.3). */
32
+ export declare const AGENT_BATCH_MIN_SPECS = 2;
33
+ export declare const AGENT_BATCH_MAX_SPECS = 32;
34
+ export declare function createAgentBatchTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
35
+ export declare function createRunWorkflowTool(options?: AgentLifecycleToolOptions): ToolRegistryEntry;
36
+ export declare function createWaitWorkflowTool(): ToolRegistryEntry;
31
37
  export declare function createAgentLifecycleTools(options?: AgentLifecycleToolOptions): ToolRegistryEntry[];
32
38
  export {};