@bubblebrain-ai/bubble 0.0.34 → 0.0.36

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 (168) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/abort-errors.d.ts +2 -2
  3. package/dist/agent/budget-ledger.d.ts +4 -45
  4. package/dist/agent/budget-ledger.js +5 -75
  5. package/dist/agent/categories.d.ts +66 -2
  6. package/dist/agent/categories.js +120 -19
  7. package/dist/agent/child-runner.d.ts +1 -3
  8. package/dist/agent/child-runner.js +6 -37
  9. package/dist/agent/discovery-barrier.js +1 -1
  10. package/dist/agent/profiles.d.ts +1 -2
  11. package/dist/agent/profiles.js +25 -14
  12. package/dist/agent/routing-catalog.d.ts +113 -0
  13. package/dist/agent/routing-catalog.js +240 -0
  14. package/dist/agent/subagent-control.d.ts +5 -12
  15. package/dist/agent/subagent-control.js +0 -2
  16. package/dist/agent/subagent-store.d.ts +1 -1
  17. package/dist/agent/subagent-store.js +1 -3
  18. package/dist/agent/workflow/control.d.ts +21 -0
  19. package/dist/agent/workflow/control.js +50 -8
  20. package/dist/agent/workflow/runtime.d.ts +15 -2
  21. package/dist/agent/workflow/runtime.js +112 -30
  22. package/dist/agent.d.ts +82 -48
  23. package/dist/agent.js +360 -232
  24. package/dist/approval/controller.js +28 -0
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +21 -1
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +2 -1
  29. package/dist/config.d.ts +5 -3
  30. package/dist/config.js +10 -5
  31. package/dist/context/compact-llm.d.ts +2 -2
  32. package/dist/external-runtime/grok-acp.d.ts +56 -0
  33. package/dist/external-runtime/grok-acp.js +386 -0
  34. package/dist/external-runtime/grok-binary.d.ts +25 -0
  35. package/dist/external-runtime/grok-binary.js +108 -0
  36. package/dist/external-runtime/grok-errors.d.ts +7 -0
  37. package/dist/external-runtime/grok-errors.js +15 -0
  38. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  39. package/dist/external-runtime/grok-input-policy.js +52 -0
  40. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  41. package/dist/external-runtime/grok-ndjson.js +193 -0
  42. package/dist/external-runtime/grok-network.d.ts +15 -0
  43. package/dist/external-runtime/grok-network.js +81 -0
  44. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  45. package/dist/external-runtime/grok-preflight.js +194 -0
  46. package/dist/external-runtime/grok-process.d.ts +26 -0
  47. package/dist/external-runtime/grok-process.js +184 -0
  48. package/dist/external-runtime/grok-profile.d.ts +32 -0
  49. package/dist/external-runtime/grok-profile.js +419 -0
  50. package/dist/external-runtime/grok-provider.d.ts +31 -0
  51. package/dist/external-runtime/grok-provider.js +41 -0
  52. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  53. package/dist/external-runtime/grok-runtime.js +882 -0
  54. package/dist/external-runtime/index.d.ts +13 -0
  55. package/dist/external-runtime/index.js +13 -0
  56. package/dist/external-runtime/session-policy.d.ts +11 -0
  57. package/dist/external-runtime/session-policy.js +30 -0
  58. package/dist/external-runtime/types.d.ts +65 -0
  59. package/dist/external-runtime/types.js +1 -0
  60. package/dist/feishu/agent-host/approval-card.js +21 -0
  61. package/dist/feishu/agent-host/run-driver.js +19 -3
  62. package/dist/feishu/serve.js +1 -0
  63. package/dist/goal/tools.d.ts +6 -3
  64. package/dist/goal/tools.js +7 -19
  65. package/dist/main.js +114 -28
  66. package/dist/memory/db.d.ts +2 -0
  67. package/dist/memory/db.js +10 -0
  68. package/dist/memory/index.d.ts +1 -0
  69. package/dist/memory/index.js +1 -0
  70. package/dist/memory/phase1.js +14 -2
  71. package/dist/memory/phase2.js +21 -2
  72. package/dist/memory/prompts.js +1 -1
  73. package/dist/memory/session-policy.d.ts +25 -0
  74. package/dist/memory/session-policy.js +55 -0
  75. package/dist/memory/storage.d.ts +1 -0
  76. package/dist/memory/storage.js +13 -0
  77. package/dist/model-catalog.d.ts +18 -0
  78. package/dist/model-catalog.js +92 -52
  79. package/dist/model-config.d.ts +3 -1
  80. package/dist/model-config.js +11 -0
  81. package/dist/oauth/grok.d.ts +25 -0
  82. package/dist/oauth/grok.js +218 -0
  83. package/dist/oauth/index.d.ts +1 -0
  84. package/dist/oauth/index.js +1 -0
  85. package/dist/oauth/storage.d.ts +9 -0
  86. package/dist/oauth/storage.js +25 -0
  87. package/dist/orchestrator/default-hooks.js +23 -7
  88. package/dist/prompt/compose.d.ts +3 -0
  89. package/dist/prompt/compose.js +3 -0
  90. package/dist/prompt/delegation.d.ts +1 -1
  91. package/dist/prompt/delegation.js +41 -8
  92. package/dist/prompt/environment.js +0 -4
  93. package/dist/prompt/reminders.d.ts +0 -1
  94. package/dist/prompt/reminders.js +1 -11
  95. package/dist/prompt/routing.d.ts +14 -0
  96. package/dist/prompt/routing.js +71 -0
  97. package/dist/prompt/runtime.js +1 -1
  98. package/dist/prompt/task-reminders.d.ts +20 -0
  99. package/dist/prompt/task-reminders.js +73 -1
  100. package/dist/provider-ai-sdk.js +2 -2
  101. package/dist/provider-grok.d.ts +26 -0
  102. package/dist/provider-grok.js +59 -0
  103. package/dist/provider-openai-codex.d.ts +17 -1
  104. package/dist/provider-openai-codex.js +101 -29
  105. package/dist/provider-registry.d.ts +72 -0
  106. package/dist/provider-registry.js +336 -59
  107. package/dist/provider-transform.d.ts +1 -1
  108. package/dist/provider-transform.js +35 -17
  109. package/dist/provider.d.ts +3 -0
  110. package/dist/provider.js +14 -0
  111. package/dist/session-types.d.ts +19 -1
  112. package/dist/session.d.ts +1 -0
  113. package/dist/session.js +4 -0
  114. package/dist/slash-commands/commands.js +224 -14
  115. package/dist/slash-commands/types.d.ts +14 -1
  116. package/dist/system-prompt.d.ts +2 -0
  117. package/dist/tools/agent-lifecycle.d.ts +1 -9
  118. package/dist/tools/agent-lifecycle.js +42 -272
  119. package/dist/tools/bash.js +1 -1
  120. package/dist/tools/child-tools.js +2 -0
  121. package/dist/tools/index.d.ts +2 -2
  122. package/dist/tools/index.js +7 -5
  123. package/dist/tools/lsp.js +1 -0
  124. package/dist/tools/memory.d.ts +7 -2
  125. package/dist/tools/memory.js +28 -43
  126. package/dist/tools/server.js +4 -0
  127. package/dist/tools/tool-search.d.ts +1 -1
  128. package/dist/tools/tool-search.js +23 -3
  129. package/dist/tui/model-switch.d.ts +7 -0
  130. package/dist/tui/model-switch.js +15 -5
  131. package/dist/tui-ink/app.d.ts +3 -1
  132. package/dist/tui-ink/app.js +672 -243
  133. package/dist/tui-ink/approval/approval-dialog.js +37 -1
  134. package/dist/tui-ink/approval/diff-view.js +4 -4
  135. package/dist/tui-ink/approval/select.js +2 -2
  136. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  137. package/dist/tui-ink/composer-buffer.js +317 -0
  138. package/dist/tui-ink/feedback-dialog.js +2 -2
  139. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  140. package/dist/tui-ink/footer.d.ts +1 -0
  141. package/dist/tui-ink/footer.js +7 -2
  142. package/dist/tui-ink/input-box.d.ts +36 -7
  143. package/dist/tui-ink/input-box.js +229 -157
  144. package/dist/tui-ink/markdown.js +11 -9
  145. package/dist/tui-ink/message-list.d.ts +5 -1
  146. package/dist/tui-ink/message-list.js +15 -2
  147. package/dist/tui-ink/model-picker.d.ts +61 -4
  148. package/dist/tui-ink/model-picker.js +464 -128
  149. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  150. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  151. package/dist/tui-ink/plan-confirm.js +1 -1
  152. package/dist/tui-ink/run.d.ts +3 -0
  153. package/dist/tui-ink/run.js +1 -1
  154. package/dist/tui-ink/subagent-view.d.ts +34 -1
  155. package/dist/tui-ink/subagent-view.js +82 -1
  156. package/dist/tui-ink/theme.d.ts +19 -2
  157. package/dist/tui-ink/theme.js +25 -8
  158. package/dist/tui-ink/welcome.d.ts +3 -2
  159. package/dist/tui-ink/welcome.js +10 -6
  160. package/dist/types.d.ts +8 -31
  161. package/dist/types.js +1 -1
  162. package/dist/variant/thinking-level.d.ts +2 -2
  163. package/dist/variant/thinking-level.js +2 -1
  164. package/dist/variant/variant-resolver.d.ts +22 -0
  165. package/dist/variant/variant-resolver.js +43 -0
  166. package/package.json +6 -2
  167. package/dist/tools/task.d.ts +0 -2
  168. package/dist/tools/task.js +0 -58
@@ -46,7 +46,7 @@ export class DiscoveryBarrier {
46
46
  return undefined;
47
47
  return {
48
48
  content: `Blocked speculative ${target.kind}. The path "${target.path}" has not been discovered in this repository context ` +
49
- "and was not explicitly requested by the user. Run glob/grep/lsp discovery first, then read only returned paths. " +
49
+ "and was not explicitly requested by the user. Run glob/grep discovery first, then read only returned paths. " +
50
50
  "Do not infer from this blocked call whether the path exists.",
51
51
  isError: true,
52
52
  status: "blocked",
@@ -20,8 +20,6 @@ export interface AgentProfile {
20
20
  category?: string;
21
21
  tools: AgentProfileTools;
22
22
  maxTurns?: number;
23
- /** Optional per-child token cap declared by the profile (may only lower the runtime default). */
24
- maxTokens?: number;
25
23
  approval: AgentProfileApproval;
26
24
  nicknameCandidates?: string[];
27
25
  prompt: string;
@@ -69,3 +67,4 @@ export declare function allowedToolEffectsForMode(mode: AgentProfileMode): Set<s
69
67
  export declare function selectToolsForAgentProfile(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): ToolRegistryEntry[];
70
68
  export declare function validateAgentProfileTools(tools: ToolRegistryEntry[], profile: AgentProfile, approval?: AgentProfileApproval): AgentProfileDiagnostic[];
71
69
  export declare function mergeUsage(current: SubagentRunResult["usage"], usage: TokenUsage): SubagentRunResult["usage"];
70
+ export declare function canonicalToolName(name: string): string;
@@ -10,21 +10,17 @@ const READONLY_PRESET = [
10
10
  "lsp",
11
11
  "web_search",
12
12
  "web_fetch",
13
- "memory_search",
14
- "memory_read_summary",
13
+ "memory",
15
14
  "skill",
16
15
  "todo_write",
17
16
  ];
18
17
  const SUBAGENT_DENY_TOOLS = new Set([
19
18
  "subagent",
20
- "task",
21
19
  "spawn_agent",
22
20
  "wait_agent",
23
21
  "send_input",
24
22
  "close_agent",
25
23
  "list_agents",
26
- "agent_team",
27
- "agent_batch",
28
24
  "run_workflow",
29
25
  "wait_workflow",
30
26
  ]);
@@ -128,12 +124,12 @@ export function builtinAgentProfiles() {
128
124
  "You are an explorer subagent for codebase reconnaissance.",
129
125
  "Answer the specific question by inspecting the repository directly. Prefer precise file paths and line-level evidence.",
130
126
  "Keep the answer compact and avoid broad refactors or implementation plans unless asked.",
131
- ].join("\n"), ["read", "glob", "grep", "lsp", "memory_search", "memory_read_summary", "skill", "todo_write"]),
127
+ ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill", "todo_write"]),
132
128
  roleProfile("worker", "Implementation-planning worker subagent", [
133
129
  "You are a worker subagent. In this Phase 1 runtime you are read-only, so you must not modify files.",
134
130
  "Analyze the assigned implementation slice, identify exact files to change, and return a concrete patch plan or findings.",
135
131
  "If write-capable worker mode is needed, say so explicitly.",
136
- ].join("\n"), ["read", "glob", "grep", "lsp", "memory_search", "memory_read_summary", "skill", "todo_write"]),
132
+ ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill", "todo_write"]),
137
133
  toProfile("search"),
138
134
  toProfile("security_investigation"),
139
135
  toProfile("evidence_correlation"),
@@ -169,7 +165,7 @@ export function allowedToolEffectsForMode(mode) {
169
165
  : new Set(["read"]);
170
166
  }
171
167
  export function selectToolsForAgentProfile(tools, profile, approval = profile.approval) {
172
- const explicitInclude = new Set(profile.tools.include ?? []);
168
+ const explicitInclude = new Set((profile.tools.include ?? []).map(canonicalToolName));
173
169
  const selected = requestedToolNames(profile);
174
170
  for (const tool of SUBAGENT_DENY_TOOLS)
175
171
  selected.delete(tool);
@@ -185,13 +181,17 @@ export function selectToolsForAgentProfile(tools, profile, approval = profile.ap
185
181
  continue;
186
182
  if (approval === "disabled" && tool.requiresApproval)
187
183
  continue;
188
- out.push(wrapApprovalFailTool(tool, approval));
184
+ // Explicit include is the profile author pre-unlocking the tool: the
185
+ // child is a fresh Agent with its own (empty) unlock set, so a deferred
186
+ // flag surviving here would re-lock the tool inside the child.
187
+ const entry = tool.deferred ? { ...tool, deferred: false } : tool;
188
+ out.push(wrapApprovalFailTool(entry, approval));
189
189
  }
190
190
  return out;
191
191
  }
192
192
  export function validateAgentProfileTools(tools, profile, approval = profile.approval) {
193
193
  const available = new Map(tools.map((tool) => [tool.name, tool]));
194
- const explicitInclude = new Set(profile.tools.include ?? []);
194
+ const explicitInclude = new Set((profile.tools.include ?? []).map(canonicalToolName));
195
195
  const allowedEffects = allowedToolEffectsForMode(profile.mode);
196
196
  const diagnostics = [];
197
197
  for (const name of requestedToolNames(profile)) {
@@ -258,6 +258,18 @@ function wrapApprovalFailTool(tool, approval) {
258
258
  }),
259
259
  };
260
260
  }
261
+ /**
262
+ * Renamed/merged tools keep working for user-defined profiles written
263
+ * against the old names (memory_search/memory_read_summary merged into
264
+ * "memory", 2026-07-13).
265
+ */
266
+ const LEGACY_TOOL_ALIASES = {
267
+ memory_search: "memory",
268
+ memory_read_summary: "memory",
269
+ };
270
+ export function canonicalToolName(name) {
271
+ return LEGACY_TOOL_ALIASES[name] ?? name;
272
+ }
261
273
  function requestedToolNames(profile) {
262
274
  const selected = new Set();
263
275
  if (profile.tools.preset === "readonly") {
@@ -266,12 +278,12 @@ function requestedToolNames(profile) {
266
278
  }
267
279
  else if (profile.tools.preset === "explicit") {
268
280
  for (const tool of profile.tools.include ?? [])
269
- selected.add(tool);
281
+ selected.add(canonicalToolName(tool));
270
282
  }
271
283
  for (const tool of profile.tools.include ?? [])
272
- selected.add(tool);
284
+ selected.add(canonicalToolName(tool));
273
285
  for (const tool of profile.tools.exclude ?? [])
274
- selected.delete(tool);
286
+ selected.delete(canonicalToolName(tool));
275
287
  return selected;
276
288
  }
277
289
  function loadProfilesFromDir(dir, source, diagnostics) {
@@ -323,7 +335,6 @@ function parseAgentProfileFile(raw, source, filePath) {
323
335
  category: stringValue(frontmatter.category),
324
336
  tools: toolsValue(frontmatter.tools),
325
337
  maxTurns: numberValue(frontmatter.maxTurns),
326
- maxTokens: numberValue(frontmatter.maxTokens),
327
338
  approval: approvalValue(frontmatter.approval),
328
339
  nicknameCandidates: stringArray(frontmatter.nicknameCandidates) ?? stringArray(frontmatter.nicknames),
329
340
  prompt: parsed.body.trim(),
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Routing catalog: the single source of catalog truth for subagent model
3
+ * routing (docs/model-routing-design.md §1).
4
+ *
5
+ * No routing consumer reads `listBuiltinModels` / `getConfigured` directly —
6
+ * neither reflects what is actually runnable. Consumers read a
7
+ * `RoutingSnapshot` through the live accessor created here; the accessor
8
+ * caches by the registry's routing revision and rebuilds lazily, so tier
9
+ * resolution, validation, the detector, and the menu are never stale (§1.5).
10
+ */
11
+ import { type ModelTier } from "../model-catalog.js";
12
+ import type { ProviderRegistry } from "../provider-registry.js";
13
+ import { selectTierCandidates, type AgentCategoriesConfig, type TierRoutingContext } from "./categories.js";
14
+ import type { ThinkingLevel } from "../types.js";
15
+ export interface AgentRoutingConfig {
16
+ /** Automatic builtin-tier downgrade routing (§3.2). */
17
+ autoTier: boolean;
18
+ /** Call-site cross-provider routing; default open, set false to lock (§7.2). */
19
+ allowCrossProvider: boolean;
20
+ }
21
+ export declare const DEFAULT_AGENT_ROUTING: AgentRoutingConfig;
22
+ export declare function sanitizeAgentRouting(value: unknown): AgentRoutingConfig;
23
+ export interface RoutingModelEntry {
24
+ id: string;
25
+ name: string;
26
+ tier?: ModelTier;
27
+ routingPriority?: number;
28
+ /** Index in the builtin catalog order, when the id is builtin (§3.2 tiebreak). */
29
+ builtinIndex?: number;
30
+ source: "builtin" | "dynamic" | "custom";
31
+ }
32
+ export type RoutingMembershipSource = "custom-allowlist" | "complete-discovery" | "fallback-union";
33
+ export interface RoutingSnapshot {
34
+ parent: {
35
+ providerId: string;
36
+ model: string;
37
+ tier?: ModelTier;
38
+ };
39
+ /** Catalog-effective provider id (openai OAuth -> "openai-codex" alias). */
40
+ effectiveProviderId: string;
41
+ membershipSource: RoutingMembershipSource;
42
+ /** Model list for the PARENT provider only (§1.2 scope note). */
43
+ models: RoutingModelEntry[];
44
+ /** True iff membershipSource === "custom-allowlist" — the only catalog
45
+ * that hard-rejects unknown ids (§1.4). */
46
+ authoritative: boolean;
47
+ registryRevision: number;
48
+ /**
49
+ * Earliest expiry of the discovery data this snapshot consumed. TTL expiry
50
+ * is passive — it bumps no revision — so the accessor must rebuild past
51
+ * this instant or it serves a catalog the registry itself no longer would.
52
+ */
53
+ discoveryExpiresAt?: number;
54
+ /** Provider ids with active credentials (getEnabled, §1.1). */
55
+ runnableProviderIds: string[];
56
+ /** Post-merge, post-tier-resolution category bindings (§3). */
57
+ resolvedCategories: Array<{
58
+ name: string;
59
+ model: string | "inherit";
60
+ thinkingLevel?: ThinkingLevel;
61
+ tierSource?: "builtin" | "user";
62
+ }>;
63
+ }
64
+ export type RoutingSnapshotAccessor = (parent: {
65
+ providerId: string;
66
+ model: string;
67
+ }) => RoutingSnapshot;
68
+ /**
69
+ * Membership versus metadata (§1.3): an authoritative source alone decides
70
+ * which ids exist; lower-priority sources only enrich per-field metadata
71
+ * (tier: dynamic ?? custom ?? builtin).
72
+ */
73
+ export declare function buildRoutingSnapshot(registry: ProviderRegistry, parent: {
74
+ providerId: string;
75
+ model: string;
76
+ }, agentCategories: AgentCategoriesConfig, agentRouting: AgentRoutingConfig): RoutingSnapshot;
77
+ /** Tier-resolution context derived from a snapshot (consumed by categories §3.2). */
78
+ export declare function tierContextFromSnapshot(snapshot: RoutingSnapshot, agentRouting: AgentRoutingConfig): TierRoutingContext;
79
+ /**
80
+ * Live accessor (§1.5): caches by (routing revision, parent route) and
81
+ * rebuilds lazily, so a discovery completion or provider mutation is
82
+ * reflected at the very next read. Building is synchronous over cached
83
+ * registry state — microseconds, never I/O.
84
+ */
85
+ export declare function createRoutingSnapshotAccessor(registry: ProviderRegistry, getAgentCategories: () => AgentCategoriesConfig, getAgentRouting: () => AgentRoutingConfig): RoutingSnapshotAccessor;
86
+ /** Tier of a model within a snapshot, when known. */
87
+ export declare function snapshotModelTier(snapshot: RoutingSnapshot, modelId: string): ModelTier | undefined;
88
+ export interface RoutableModelEntry {
89
+ /** Routable provider id — what goes before the colon in provider:model. */
90
+ providerId: string;
91
+ id: string;
92
+ name: string;
93
+ }
94
+ export type RoutableModelIndex = () => RoutableModelEntry[];
95
+ /** Revision-cached index over all runnable providers' catalogs. */
96
+ export declare function createRoutableModelIndex(registry: ProviderRegistry): RoutableModelIndex;
97
+ /** Loose normalization for model-name matching: case/punctuation-insensitive. */
98
+ export declare function normalizeModelToken(value: string): string;
99
+ /**
100
+ * Near-match candidates for a (probably mistyped) model id within one
101
+ * provider's catalog. "suggest" matches normalized prefixes in either
102
+ * direction (soft "did you mean" notes). "truncation" matches only inputs
103
+ * the catalog EXTENDS ("gpt-5.6" -> gpt-5.6-sol/-terra/-luna) — the one
104
+ * direction that is typo evidence. An input extending a catalog id
105
+ * (gpt-5.6-sol-20260701 vs cataloged gpt-5.6-sol) is more likely a newly
106
+ * released variant the local catalog lags on, and must not be treated as a
107
+ * typo by hard-reject paths. Capped, deterministic order.
108
+ */
109
+ export declare function nearModelMatches(input: string, candidates: RoutableModelEntry[], options?: {
110
+ limit?: number;
111
+ mode?: "suggest" | "truncation";
112
+ }): string[];
113
+ export { selectTierCandidates };
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Routing catalog: the single source of catalog truth for subagent model
3
+ * routing (docs/model-routing-design.md §1).
4
+ *
5
+ * No routing consumer reads `listBuiltinModels` / `getConfigured` directly —
6
+ * neither reflects what is actually runnable. Consumers read a
7
+ * `RoutingSnapshot` through the live accessor created here; the accessor
8
+ * caches by the registry's routing revision and rebuilds lazily, so tier
9
+ * resolution, validation, the detector, and the menu are never stale (§1.5).
10
+ */
11
+ import { getBuiltinModel, listBuiltinModels, listDynamicModelMetadata, } from "../model-catalog.js";
12
+ import { mergeAgentCategoriesWithProvenance, resolveSubagentRoute, selectTierCandidates, } from "./categories.js";
13
+ export const DEFAULT_AGENT_ROUTING = {
14
+ autoTier: true,
15
+ allowCrossProvider: true,
16
+ };
17
+ export function sanitizeAgentRouting(value) {
18
+ const config = { ...DEFAULT_AGENT_ROUTING };
19
+ if (value && typeof value === "object" && !Array.isArray(value)) {
20
+ const record = value;
21
+ if (typeof record.autoTier === "boolean")
22
+ config.autoTier = record.autoTier;
23
+ if (typeof record.allowCrossProvider === "boolean") {
24
+ config.allowCrossProvider = record.allowCrossProvider;
25
+ }
26
+ }
27
+ return config;
28
+ }
29
+ /**
30
+ * Membership versus metadata (§1.3): an authoritative source alone decides
31
+ * which ids exist; lower-priority sources only enrich per-field metadata
32
+ * (tier: dynamic ?? custom ?? builtin).
33
+ */
34
+ export function buildRoutingSnapshot(registry, parent, agentCategories, agentRouting) {
35
+ const configured = registry.getConfigured().find((item) => item.id === parent.providerId);
36
+ const effectiveProviderId = parent.providerId === "openai" && configured?.authType === "oauth"
37
+ ? "openai-codex"
38
+ : parent.providerId;
39
+ const builtins = listBuiltinModels(effectiveProviderId);
40
+ const builtinIndex = new Map(builtins.map((model, index) => [model.id, index]));
41
+ const dynamic = listDynamicModelMetadata(effectiveProviderId);
42
+ const custom = registry.getModelConfig().getCustomModels(parent.providerId);
43
+ const discovery = registry.getCachedDiscoverySnapshot(parent.providerId);
44
+ const metadataFor = (id) => {
45
+ const dynamicEntry = dynamic.find((model) => model.id === id);
46
+ const discoveryEntry = discovery?.models.find((model) => model.id === id);
47
+ const customEntry = custom.find((model) => model.id === id);
48
+ const builtinEntry = builtins.find((model) => model.id === id);
49
+ return {
50
+ id,
51
+ name: dynamicEntry?.name ?? discoveryEntry?.name ?? customEntry?.name ?? builtinEntry?.name ?? id,
52
+ // Field-level merge (§1.3): discovery decides existence, but tier falls
53
+ // back through custom to builtin when the fresher source omits it.
54
+ tier: dynamicEntry?.tier ?? discoveryEntry?.tier ?? customEntry?.tier ?? builtinEntry?.tier,
55
+ routingPriority: customEntry?.routingPriority,
56
+ builtinIndex: builtinIndex.get(id),
57
+ };
58
+ };
59
+ let membershipSource;
60
+ let memberIds;
61
+ const memberSource = new Map();
62
+ if (custom.length > 0) {
63
+ membershipSource = "custom-allowlist";
64
+ memberIds = custom.map((model) => model.id);
65
+ for (const id of memberIds)
66
+ memberSource.set(id, "custom");
67
+ }
68
+ else if (discovery && discovery.complete) {
69
+ membershipSource = "complete-discovery";
70
+ memberIds = discovery.models.map((model) => model.id);
71
+ for (const id of memberIds)
72
+ memberSource.set(id, "dynamic");
73
+ }
74
+ else {
75
+ membershipSource = "fallback-union";
76
+ memberIds = [];
77
+ for (const model of dynamic) {
78
+ if (!memberSource.has(model.id)) {
79
+ memberSource.set(model.id, "dynamic");
80
+ memberIds.push(model.id);
81
+ }
82
+ }
83
+ for (const model of custom) {
84
+ if (!memberSource.has(model.id)) {
85
+ memberSource.set(model.id, "custom");
86
+ memberIds.push(model.id);
87
+ }
88
+ }
89
+ for (const model of builtins) {
90
+ if (!memberSource.has(model.id)) {
91
+ memberSource.set(model.id, "builtin");
92
+ memberIds.push(model.id);
93
+ }
94
+ }
95
+ }
96
+ const models = memberIds.map((id) => ({
97
+ ...metadataFor(id),
98
+ source: memberSource.get(id) ?? "builtin",
99
+ }));
100
+ const parentTier = models.find((model) => model.id === parent.model)?.tier
101
+ ?? getBuiltinModel(effectiveProviderId, parent.model)?.tier;
102
+ const runnableProviderIds = registry.getEnabled().map((provider) => provider.id);
103
+ return {
104
+ parent: { ...parent, tier: parentTier },
105
+ effectiveProviderId,
106
+ membershipSource,
107
+ models,
108
+ authoritative: membershipSource === "custom-allowlist",
109
+ registryRevision: registry.getRoutingRevision(),
110
+ discoveryExpiresAt: discovery?.expiresAt,
111
+ runnableProviderIds,
112
+ resolvedCategories: resolveCategoriesForMenu(parent, parentTier, models, agentCategories, agentRouting),
113
+ };
114
+ }
115
+ /** Tier-resolution context derived from a snapshot (consumed by categories §3.2). */
116
+ export function tierContextFromSnapshot(snapshot, agentRouting) {
117
+ return {
118
+ parentTier: snapshot.parent.tier,
119
+ models: snapshot.models.map((model) => ({
120
+ id: model.id,
121
+ tier: model.tier,
122
+ routingPriority: model.routingPriority,
123
+ builtinIndex: model.builtinIndex,
124
+ })),
125
+ autoTier: agentRouting.autoTier,
126
+ };
127
+ }
128
+ /**
129
+ * Live accessor (§1.5): caches by (routing revision, parent route) and
130
+ * rebuilds lazily, so a discovery completion or provider mutation is
131
+ * reflected at the very next read. Building is synchronous over cached
132
+ * registry state — microseconds, never I/O.
133
+ */
134
+ export function createRoutingSnapshotAccessor(registry, getAgentCategories, getAgentRouting) {
135
+ let cached;
136
+ return (parent) => {
137
+ if (cached
138
+ && cached.registryRevision === registry.getRoutingRevision()
139
+ && cached.parent.providerId === parent.providerId
140
+ && cached.parent.model === parent.model
141
+ && (cached.discoveryExpiresAt === undefined || Date.now() < cached.discoveryExpiresAt)) {
142
+ return cached;
143
+ }
144
+ cached = buildRoutingSnapshot(registry, parent, getAgentCategories(), getAgentRouting());
145
+ return cached;
146
+ };
147
+ }
148
+ function resolveCategoriesForMenu(parent, parentTier, models, agentCategories, agentRouting) {
149
+ const merged = mergeAgentCategoriesWithProvenance(agentCategories);
150
+ const tierContext = {
151
+ parentTier,
152
+ models: models.map((model) => ({
153
+ id: model.id,
154
+ tier: model.tier,
155
+ routingPriority: model.routingPriority,
156
+ builtinIndex: model.builtinIndex,
157
+ })),
158
+ autoTier: agentRouting.autoTier,
159
+ };
160
+ return Object.entries(merged).map(([name, entry]) => {
161
+ const resolution = resolveSubagentRoute(name, { providerId: parent.providerId, model: parent.model, thinkingLevel: "medium" }, agentCategories, tierContext);
162
+ const route = "route" in resolution ? resolution.route : undefined;
163
+ return {
164
+ name,
165
+ model: route && route.model !== parent.model ? route.model : "inherit",
166
+ thinkingLevel: entry.config.thinkingLevel,
167
+ tierSource: entry.provenance.tierSource,
168
+ };
169
+ });
170
+ }
171
+ /** Tier of a model within a snapshot, when known. */
172
+ export function snapshotModelTier(snapshot, modelId) {
173
+ return snapshot.models.find((model) => model.id === modelId)?.tier;
174
+ }
175
+ /** Revision-cached index over all runnable providers' catalogs. */
176
+ export function createRoutableModelIndex(registry) {
177
+ let cachedRevision = -1;
178
+ let cached = [];
179
+ return () => {
180
+ const revision = registry.getRoutingRevision();
181
+ if (revision === cachedRevision)
182
+ return cached;
183
+ const entries = [];
184
+ for (const provider of registry.getEnabled()) {
185
+ const effectiveProviderId = provider.id === "openai" && provider.authType === "oauth"
186
+ ? "openai-codex"
187
+ : provider.id;
188
+ const seen = new Set();
189
+ const push = (id, name) => {
190
+ if (seen.has(id))
191
+ return;
192
+ seen.add(id);
193
+ entries.push({ providerId: provider.id, id, name });
194
+ };
195
+ for (const model of registry.getModelConfig().getCustomModels(provider.id))
196
+ push(model.id, model.name);
197
+ for (const model of listDynamicModelMetadata(effectiveProviderId))
198
+ push(model.id, model.name);
199
+ for (const model of listBuiltinModels(effectiveProviderId))
200
+ push(model.id, model.name);
201
+ }
202
+ cachedRevision = revision;
203
+ cached = entries;
204
+ return cached;
205
+ };
206
+ }
207
+ /** Loose normalization for model-name matching: case/punctuation-insensitive. */
208
+ export function normalizeModelToken(value) {
209
+ return value.toLowerCase().replace(/[-._\s]/g, "");
210
+ }
211
+ /**
212
+ * Near-match candidates for a (probably mistyped) model id within one
213
+ * provider's catalog. "suggest" matches normalized prefixes in either
214
+ * direction (soft "did you mean" notes). "truncation" matches only inputs
215
+ * the catalog EXTENDS ("gpt-5.6" -> gpt-5.6-sol/-terra/-luna) — the one
216
+ * direction that is typo evidence. An input extending a catalog id
217
+ * (gpt-5.6-sol-20260701 vs cataloged gpt-5.6-sol) is more likely a newly
218
+ * released variant the local catalog lags on, and must not be treated as a
219
+ * typo by hard-reject paths. Capped, deterministic order.
220
+ */
221
+ export function nearModelMatches(input, candidates, options = {}) {
222
+ const limit = options.limit ?? 5;
223
+ const mode = options.mode ?? "suggest";
224
+ const normalized = normalizeModelToken(input);
225
+ if (normalized.length < 3)
226
+ return [];
227
+ return candidates
228
+ .filter((candidate) => {
229
+ const other = normalizeModelToken(candidate.id);
230
+ if (other === normalized)
231
+ return false;
232
+ if (other.startsWith(normalized))
233
+ return true;
234
+ return mode === "suggest" && normalized.startsWith(other);
235
+ })
236
+ .map((candidate) => candidate.id)
237
+ .sort()
238
+ .slice(0, limit);
239
+ }
240
+ export { selectTierCandidates };
@@ -8,17 +8,8 @@ export type SubagentThreadStatus = "queued" | "running" | "completed" | "failed"
8
8
  * rendered in lifecycle tool replies (design doc §3.1) — a resume hint is
9
9
  * emitted iff the runtime judged the run resumable, never as a blanket string.
10
10
  */
11
- export type SubagentFinalReason = "completed" | "failed_transient" | "failed_fatal" | "rate_limited_exhausted" | "blocked" | "cancelled_interrupt" | "cancelled_user" | "cancelled_budget" | "cancelled_parent_abort";
11
+ export type SubagentFinalReason = "completed" | "failed_transient" | "failed_fatal" | "rate_limited_exhausted" | "blocked" | "cancelled_interrupt" | "cancelled_user" | "cancelled_parent_abort";
12
12
  export declare function isResumableReason(reason: SubagentFinalReason): boolean;
13
- /** Per-child token budget, fixed at dispatch time (design doc §6). */
14
- export interface SubagentTokenCap {
15
- /** Soft cap: crossing it injects a wrap-up reminder into the child. */
16
- soft: number;
17
- /** Hard cap: crossing it aborts this child only. Updated at turn checks. */
18
- hard: number;
19
- /** Ledger tokens already attributed to this child when the run started. */
20
- baseline: number;
21
- }
22
13
  export interface SubagentThreadSnapshot {
23
14
  agentId: string;
24
15
  runId: string;
@@ -40,7 +31,6 @@ export interface SubagentThreadSnapshot {
40
31
  deliveredAt?: number;
41
32
  /** 1-based position in the scheduler queue while status is "queued". */
42
33
  queuePosition?: number;
43
- tokenCap?: SubagentTokenCap;
44
34
  /** Present for write_worktree children: where the isolated checkout lives. */
45
35
  worktree?: SubagentWorktree;
46
36
  }
@@ -63,8 +53,11 @@ export interface SubagentThreadRecord {
63
53
  createdAt: number;
64
54
  updatedAt: number;
65
55
  deliveredAt?: number;
66
- tokenCap?: SubagentTokenCap;
67
56
  worktree?: SubagentWorktree;
57
+ /** True when the child was asked for schema-validated structured output:
58
+ * a compact JSON handoff is complete by construction, so the token-floor
59
+ * restate turn must not fire on it. */
60
+ expectsStructuredOutput?: boolean;
68
61
  /** True for agents spawned inside a run_workflow: kept out of list_agents /
69
62
  * wait_agent and not persisted (design v2 appendix; option C). */
70
63
  workflowInternal?: boolean;
@@ -9,7 +9,6 @@ export function isResumableReason(reason) {
9
9
  case "completed":
10
10
  case "failed_fatal":
11
11
  case "blocked":
12
- case "cancelled_budget":
13
12
  return false;
14
13
  }
15
14
  }
@@ -33,7 +32,6 @@ export function snapshotSubagentThread(record) {
33
32
  createdAt: record.createdAt,
34
33
  updatedAt: record.updatedAt,
35
34
  deliveredAt: record.deliveredAt,
36
- tokenCap: record.tokenCap ? { ...record.tokenCap } : undefined,
37
35
  worktree: record.worktree ? { ...record.worktree } : undefined,
38
36
  };
39
37
  }
@@ -7,7 +7,7 @@
7
7
  * Persistence (design §7): final-state children are written to
8
8
  * `<persistDir>/<agentId>.json` as snapshot + compacted message history, so a
9
9
  * later process can resume them via send_input. The on-disk schema carries
10
- * `finalReason` / `resumable` / `deliveredAt` / `tokenCap` — the fields the
10
+ * `finalReason` / `resumable` / `deliveredAt` — the fields the
11
11
  * reply protocol and delivery dedup depend on. Child transcripts never mix
12
12
  * into the parent transcript.
13
13
  */
@@ -7,7 +7,7 @@
7
7
  * Persistence (design §7): final-state children are written to
8
8
  * `<persistDir>/<agentId>.json` as snapshot + compacted message history, so a
9
9
  * later process can resume them via send_input. The on-disk schema carries
10
- * `finalReason` / `resumable` / `deliveredAt` / `tokenCap` — the fields the
10
+ * `finalReason` / `resumable` / `deliveredAt` — the fields the
11
11
  * reply protocol and delivery dedup depend on. Child transcripts never mix
12
12
  * into the parent transcript.
13
13
  */
@@ -84,7 +84,6 @@ export class SubagentStore {
84
84
  createdAt: record.createdAt,
85
85
  updatedAt: record.updatedAt,
86
86
  deliveredAt: record.deliveredAt,
87
- tokenCap: record.tokenCap,
88
87
  messages: record.agent?.messages ?? record.messages,
89
88
  };
90
89
  writeFileSync(join(this.persistDir, `${record.agentId}.json`), JSON.stringify(payload));
@@ -135,7 +134,6 @@ export class SubagentStore {
135
134
  createdAt: parsed.createdAt,
136
135
  updatedAt: parsed.updatedAt,
137
136
  deliveredAt: parsed.deliveredAt,
138
- tokenCap: parsed.tokenCap,
139
137
  abortController: new AbortController(),
140
138
  waiters: new Set(),
141
139
  messages: parsed.messages,
@@ -16,6 +16,8 @@ export interface WorkflowRunRecord {
16
16
  snapshots: SubagentThreadSnapshot[];
17
17
  logs: string[];
18
18
  result?: WorkflowResult;
19
+ /** Where the full rendered result was persisted (unset if the write failed). */
20
+ resultPath?: string;
19
21
  abortController: AbortController;
20
22
  waiters: Set<() => void>;
21
23
  promise?: Promise<void>;
@@ -30,8 +32,27 @@ export interface WorkflowRunSnapshot {
30
32
  status: WorkflowRunStatus;
31
33
  agentCount: number;
32
34
  result?: WorkflowResult;
35
+ resultPath?: string;
33
36
  logs: string[];
34
37
  snapshots: SubagentThreadSnapshot[];
35
38
  }
36
39
  /** System-reminder injected at the next turn when a background workflow finishes. */
40
+ /**
41
+ * Failed members must surface in EVERY channel that renders a finished
42
+ * workflow (delivery notice and wait_workflow): agent() converts failures to
43
+ * null inside the script, so without this line a "completed" workflow could
44
+ * silently omit items whose agents never delivered.
45
+ */
46
+ export declare function workflowMemberWarning(snapshot: WorkflowRunSnapshot): string | undefined;
47
+ /** Chars of the result shown inline; the full rendering is persisted to resultPath. */
48
+ export declare const WORKFLOW_RESULT_PREVIEW_LIMIT = 8000;
49
+ export declare function renderWorkflowResultValue(value: unknown): string;
50
+ /**
51
+ * Result block shared by every channel that hands a finished workflow to the
52
+ * parent (delivery notice and wait_workflow). Truncation must preserve line
53
+ * structure — the value is often pretty-printed JSON or markdown — and a cut
54
+ * preview must always point at the persisted full result plus the durable fix
55
+ * (distill inside the script), or the parent silently works from partial data.
56
+ */
57
+ export declare function buildWorkflowResultBlock(snapshot: WorkflowRunSnapshot, limit?: number): string[];
37
58
  export declare function buildWorkflowDeliveryNotice(snapshot: WorkflowRunSnapshot): string;