@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
@@ -0,0 +1,218 @@
1
+ /**
2
+ * xAI Grok subscription OAuth login (PKCE + local callback).
3
+ *
4
+ * Mirrors the public OAuth client used by the official Grok CLI so a Grok
5
+ * subscription can drive Bubble's native agent loop the same way ChatGPT
6
+ * OAuth does: browser sign-in once, then bearer + refresh tokens stored in
7
+ * ~/.bubble/auth.json.
8
+ */
9
+ import { createServer } from "node:http";
10
+ import { exec } from "node:child_process";
11
+ import { randomBytes, createHash } from "node:crypto";
12
+ import { readFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { chatGptFetch } from "../network/chatgpt-transport.js";
16
+ // Public OAuth client registered for the official Grok CLI ("Grok Build").
17
+ const CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
18
+ const AUTH_URL = "https://auth.x.ai/oauth2/authorize";
19
+ const TOKEN_URL = "https://auth.x.ai/oauth2/token";
20
+ const SCOPE = "openid profile email offline_access grok-cli:access api:access conversations:read conversations:write";
21
+ function generatePKCE() {
22
+ const codeVerifier = randomBytes(32).toString("base64url");
23
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
24
+ return { codeVerifier, codeChallenge };
25
+ }
26
+ function generateOpaqueId() {
27
+ return globalThis.crypto?.randomUUID?.() ?? randomBytes(16).toString("hex");
28
+ }
29
+ function openBrowser(url) {
30
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
31
+ exec(`${cmd} ${JSON.stringify(url)}`, () => {
32
+ // ignore errors; we show manual fallback in UI
33
+ });
34
+ }
35
+ /**
36
+ * xAI's OAuth client allows dynamic loopback ports, so listen on an ephemeral
37
+ * port and report the redirect URI back to the login flow once bound.
38
+ */
39
+ async function startCallbackServer(timeoutMs, onListening, onStatus) {
40
+ return new Promise((resolve, reject) => {
41
+ let resolved = false;
42
+ const server = createServer((req, res) => {
43
+ try {
44
+ const port = server.address().port;
45
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
46
+ if (url.pathname === "/callback") {
47
+ const code = url.searchParams.get("code");
48
+ const state = url.searchParams.get("state");
49
+ const error = url.searchParams.get("error");
50
+ const errorDesc = url.searchParams.get("error_description");
51
+ if (error) {
52
+ res.writeHead(400, { "Content-Type": "text/html" });
53
+ res.end(`<html><body><h1>Authorization failed</h1><p>${errorDesc || error}</p></body></html>`);
54
+ if (!resolved) {
55
+ resolved = true;
56
+ server.close(() => reject(new Error(`OAuth error: ${errorDesc || error}`)));
57
+ }
58
+ return;
59
+ }
60
+ if (code) {
61
+ res.writeHead(200, { "Content-Type": "text/html" });
62
+ res.end("<html><body><h1>Authorization successful</h1><p>You can close this window and return to the terminal.</p></body></html>");
63
+ if (!resolved) {
64
+ resolved = true;
65
+ server.close(() => resolve({ code, state: state || "" }));
66
+ }
67
+ return;
68
+ }
69
+ }
70
+ res.writeHead(404);
71
+ res.end("Not found");
72
+ }
73
+ catch {
74
+ res.writeHead(500);
75
+ res.end("Error");
76
+ }
77
+ });
78
+ server.listen(0, "127.0.0.1", () => {
79
+ const port = server.address().port;
80
+ onStatus?.(`Local server listening on http://127.0.0.1:${port}`);
81
+ onListening(`http://127.0.0.1:${port}/callback`);
82
+ });
83
+ setTimeout(() => {
84
+ if (!resolved) {
85
+ resolved = true;
86
+ server.close(() => reject(new Error(`OAuth login timed out after ${timeoutMs / 1000}s`)));
87
+ }
88
+ }, timeoutMs);
89
+ });
90
+ }
91
+ function parseTokenResponse(data, fallbackRefreshToken) {
92
+ if (!data.access_token || typeof data.access_token !== "string") {
93
+ throw new Error("xAI returned no access token.");
94
+ }
95
+ const refreshToken = data.refresh_token || fallbackRefreshToken;
96
+ if (!refreshToken) {
97
+ throw new Error("xAI returned no refresh token.");
98
+ }
99
+ const expiresIn = typeof data.expires_in === "number" && data.expires_in > 0 ? data.expires_in : 3600;
100
+ return {
101
+ accessToken: data.access_token,
102
+ refreshToken,
103
+ expiresAt: Date.now() + expiresIn * 1000,
104
+ };
105
+ }
106
+ export async function loginGrok(callbacks, options = {}) {
107
+ const fetchImpl = options.fetch ?? chatGptFetch;
108
+ callbacks?.onStatus("Starting xAI Grok OAuth login...");
109
+ const pkce = generatePKCE();
110
+ const state = generateOpaqueId();
111
+ const nonce = generateOpaqueId();
112
+ let redirectUri = "";
113
+ const serverPromise = startCallbackServer(5 * 60 * 1000, (uri) => {
114
+ redirectUri = uri;
115
+ }, callbacks?.onStatus);
116
+ // The listen callback fires before any request can arrive; give the event
117
+ // loop one tick so redirectUri is populated before building the URL.
118
+ await new Promise((resolve) => setImmediate(resolve));
119
+ if (!redirectUri) {
120
+ throw new Error("The local OAuth callback server did not start.");
121
+ }
122
+ const params = new URLSearchParams({
123
+ response_type: "code",
124
+ client_id: CLIENT_ID,
125
+ redirect_uri: redirectUri,
126
+ scope: SCOPE,
127
+ code_challenge: pkce.codeChallenge,
128
+ code_challenge_method: "S256",
129
+ state,
130
+ nonce,
131
+ referrer: "grok-build",
132
+ });
133
+ const authUrl = `${AUTH_URL}?${params.toString()}`;
134
+ callbacks?.onStatus("Opening browser for authorization...");
135
+ openBrowser(authUrl);
136
+ callbacks?.onStatus(`If your browser didn't open, copy this URL manually:\n${authUrl}`);
137
+ callbacks?.onStatus("Waiting for authorization (timeout: 5min)...");
138
+ const { code, state: returnedState } = await serverPromise;
139
+ if (returnedState !== state) {
140
+ throw new Error("OAuth state mismatch. Possible CSRF attack.");
141
+ }
142
+ callbacks?.onStatus("Exchanging authorization code for tokens...");
143
+ const response = await fetchImpl(TOKEN_URL, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
146
+ body: new URLSearchParams({
147
+ grant_type: "authorization_code",
148
+ code,
149
+ redirect_uri: redirectUri,
150
+ client_id: CLIENT_ID,
151
+ code_verifier: pkce.codeVerifier,
152
+ }),
153
+ });
154
+ if (!response.ok) {
155
+ const text = await response.text().catch(() => "Unknown error");
156
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${text}`);
157
+ }
158
+ const tokens = parseTokenResponse((await response.json()));
159
+ callbacks?.onStatus("Login successful!");
160
+ return tokens;
161
+ }
162
+ export async function refreshGrok(refreshToken, options = {}) {
163
+ const fetchImpl = options.fetch ?? chatGptFetch;
164
+ const response = await fetchImpl(TOKEN_URL, {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
167
+ body: new URLSearchParams({
168
+ grant_type: "refresh_token",
169
+ refresh_token: refreshToken,
170
+ client_id: CLIENT_ID,
171
+ }),
172
+ });
173
+ if (!response.ok) {
174
+ const text = await response.text().catch(() => "Unknown error");
175
+ throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${text}`);
176
+ }
177
+ return parseTokenResponse((await response.json()), refreshToken);
178
+ }
179
+ /**
180
+ * One-time credential import from the official Grok CLI's auth store (either
181
+ * Bubble's isolated runtime profile or the user's real ~/.grok). Users who
182
+ * already completed `grok login` skip the browser round-trip entirely.
183
+ */
184
+ export function importGrokCliCredentials(bubbleHome = join(homedir(), ".bubble")) {
185
+ const candidates = [
186
+ join(bubbleHome, "runtimes", "grok", "grok-home", "auth.json"),
187
+ join(homedir(), ".grok", "auth.json"),
188
+ ];
189
+ for (const path of candidates) {
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
193
+ }
194
+ catch {
195
+ continue;
196
+ }
197
+ if (!parsed || typeof parsed !== "object")
198
+ continue;
199
+ for (const [key, value] of Object.entries(parsed)) {
200
+ if (!key.startsWith("https://auth.x.ai::"))
201
+ continue;
202
+ if (!value || typeof value !== "object")
203
+ continue;
204
+ const entry = value;
205
+ if (typeof entry.key !== "string" || !entry.key || typeof entry.refresh_token !== "string" || !entry.refresh_token) {
206
+ continue;
207
+ }
208
+ const expiresAt = typeof entry.expires_at === "string" ? Date.parse(entry.expires_at) : NaN;
209
+ return {
210
+ accessToken: entry.key,
211
+ refreshToken: entry.refresh_token,
212
+ // An unparseable expiry just means the first request refreshes.
213
+ expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
214
+ };
215
+ }
216
+ }
217
+ return undefined;
218
+ }
@@ -1,3 +1,4 @@
1
1
  export { AuthStorage } from "./storage.js";
2
2
  export { loginOpenAICodex, refreshOpenAICodex, type OpenAICodexLoginCallbacks } from "./openai-codex.js";
3
+ export { loginGrok, refreshGrok, importGrokCliCredentials, type GrokLoginCallbacks } from "./grok.js";
3
4
  export type { OAuthTokens, OAuthCredentials } from "./types.js";
@@ -1,2 +1,3 @@
1
1
  export { AuthStorage } from "./storage.js";
2
2
  export { loginOpenAICodex, refreshOpenAICodex } from "./openai-codex.js";
3
+ export { loginGrok, refreshGrok, importGrokCliCredentials } from "./grok.js";
@@ -4,7 +4,16 @@
4
4
  import type { OAuthCredentials } from "./types.js";
5
5
  export declare class AuthStorage {
6
6
  private data;
7
+ private mutationListeners;
7
8
  constructor();
9
+ /**
10
+ * Observe credential writes/removals. Callers across the codebase mutate
11
+ * this storage directly (login/logout flows), so consumers that must react
12
+ * to credential-identity changes (ProviderRegistry's routing revision)
13
+ * subscribe here instead of wrapping every call site.
14
+ */
15
+ onMutation(listener: (providerId: string) => void): () => void;
16
+ private notifyMutation;
8
17
  private load;
9
18
  private save;
10
19
  getPath(): string;
@@ -7,9 +7,32 @@ import { homedir } from "node:os";
7
7
  const AUTH_PATH = join(homedir(), ".bubble", "auth.json");
8
8
  export class AuthStorage {
9
9
  data = {};
10
+ mutationListeners = [];
10
11
  constructor() {
11
12
  this.load();
12
13
  }
14
+ /**
15
+ * Observe credential writes/removals. Callers across the codebase mutate
16
+ * this storage directly (login/logout flows), so consumers that must react
17
+ * to credential-identity changes (ProviderRegistry's routing revision)
18
+ * subscribe here instead of wrapping every call site.
19
+ */
20
+ onMutation(listener) {
21
+ this.mutationListeners.push(listener);
22
+ return () => {
23
+ this.mutationListeners = this.mutationListeners.filter((item) => item !== listener);
24
+ };
25
+ }
26
+ notifyMutation(providerId) {
27
+ for (const listener of this.mutationListeners) {
28
+ try {
29
+ listener(providerId);
30
+ }
31
+ catch {
32
+ // Listeners must never break credential writes.
33
+ }
34
+ }
35
+ }
13
36
  load() {
14
37
  if (!existsSync(AUTH_PATH))
15
38
  return;
@@ -37,10 +60,12 @@ export class AuthStorage {
37
60
  set(providerId, creds) {
38
61
  this.data[providerId] = creds;
39
62
  this.save();
63
+ this.notifyMutation(providerId);
40
64
  }
41
65
  remove(providerId) {
42
66
  delete this.data[providerId];
43
67
  this.save();
68
+ this.notifyMutation(providerId);
44
69
  }
45
70
  has(providerId) {
46
71
  return !!this.data[providerId];
@@ -4,8 +4,8 @@ import { EvidenceTracker } from "../agent/evidence-tracker.js";
4
4
  import { ExecutionGovernor } from "../agent/execution-governor.js";
5
5
  import { DiscoveryBarrier } from "../agent/discovery-barrier.js";
6
6
  import { arbitrateToolCall } from "../agent/tool-arbiter.js";
7
- import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildTaskSummaryReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
8
- import { reminderForTaskType } from "../prompt/task-reminders.js";
7
+ import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
8
+ import { orchestrationRequestReminder, reminderForTaskType, userNamedModelReminder } from "../prompt/task-reminders.js";
9
9
  import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
10
10
  import { buildSubagentLifecycleReminder } from "../agent/subagent-lifecycle-reminder.js";
11
11
  export function createDefaultHooks() {
@@ -28,6 +28,19 @@ export function createDefaultHooks() {
28
28
  if (taskReminder) {
29
29
  ctx.queueReminder(taskReminder);
30
30
  }
31
+ // User named the mechanism ("agent team" / workflow / 编排): remind at
32
+ // the decision turn, where it outweighs the parallel-spawns prior.
33
+ const orchestrationReminder = orchestrationRequestReminder(ctx.input, ctx.agent.hasToolAvailable("run_workflow"));
34
+ if (orchestrationReminder) {
35
+ ctx.queueReminder(orchestrationReminder);
36
+ }
37
+ // User named a configured model ("gpt 5.6 sol"): resolve it against
38
+ // the routable catalog and hand over the exact id at the decision
39
+ // point — never let the parent retype ids from priors (v3.6).
40
+ const namedModelReminder = userNamedModelReminder(ctx.input, ctx.agent.listRoutableModels?.(), ctx.agent.hasToolAvailable("spawn_agent"));
41
+ if (namedModelReminder) {
42
+ ctx.queueReminder(namedModelReminder);
43
+ }
31
44
  // Small-task hint: counterweight to the default protocol's exploration
32
45
  // bias, only fires once per run on focused one-shot requests like
33
46
  // "写个 HTML 介绍元旦". Don't issue for the same input twice.
@@ -141,9 +154,6 @@ export function createDefaultHooks() {
141
154
  // edit was correct. CC's approach is the opposite: verify when there
142
155
  // is something real to verify, say so explicitly when there isn't, and
143
156
  // trust the model to judge. We follow that.
144
- if (ctx.toolCall.name === "task") {
145
- ctx.queueReminder(buildTaskSummaryReminder());
146
- }
147
157
  if (ctx.state.governor) {
148
158
  for (const reminder of ctx.state.governor.consumePendingReminders()) {
149
159
  ctx.queueReminder(reminder);
@@ -157,6 +167,13 @@ export function createDefaultHooks() {
157
167
  ctx.queueReminder(reminder);
158
168
  }
159
169
  }
170
+ // Routing detector note (model-routing design §6): rides the same
171
+ // channel as the lifecycle reminder, once per session. Consuming also
172
+ // closes the per-fan-out counting window.
173
+ const routingReminder = ctx.agent.consumePendingRoutingReminder?.();
174
+ if (routingReminder) {
175
+ ctx.queueReminder(routingReminder);
176
+ }
160
177
  if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker?.isCoreCoverageComplete()) {
161
178
  ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
162
179
  return;
@@ -190,8 +207,7 @@ function isSubagentLifecycleTool(name) {
190
207
  return name === "spawn_agent"
191
208
  || name === "wait_agent"
192
209
  || name === "send_input"
193
- || name === "close_agent"
194
- || name === "task";
210
+ || name === "close_agent";
195
211
  }
196
212
  function hashEditCall(toolCall) {
197
213
  // Cheap fingerprint that identifies "same edit/write call". JSON of the
@@ -9,5 +9,8 @@ export interface ComposeSystemPromptOptions extends EnvironmentPromptOptions {
9
9
  skills?: SkillSummary[];
10
10
  memoryPrompt?: string;
11
11
  agentProfilePrompt?: string;
12
+ /** Rendered subagent model-routing menu (design §4). Placed right after the
13
+ * delegation policy and gated the same way: spawn_agent present. */
14
+ modelRoutingPrompt?: string;
12
15
  }
13
16
  export declare function composeSystemPrompt(options?: ComposeSystemPromptOptions): string;
@@ -27,11 +27,14 @@ export function composeSystemPrompt(options = {}) {
27
27
  guidelines: buildGuidelines(options.tools ?? defaultToolNames, options.guidelines ?? []),
28
28
  });
29
29
  const delegationPrompt = buildDelegationPolicyPrompt(options.tools ?? defaultToolNames);
30
+ // Same gate as the delegation policy: children (no spawn_agent) never see the menu.
31
+ const modelRoutingPrompt = delegationPrompt ? options.modelRoutingPrompt : undefined;
30
32
  return [
31
33
  providerPrompt,
32
34
  environmentPrompt,
33
35
  runtimePrompt,
34
36
  delegationPrompt,
37
+ modelRoutingPrompt,
35
38
  options.agentProfilePrompt,
36
39
  options.memoryPrompt,
37
40
  ].filter(Boolean).join("\n\n");
@@ -5,7 +5,7 @@
5
5
  * and read-only-scoped, negative clauses get equal weight — the user's hard
6
6
  * constraint is "proactive, but never delegate-everything". Gated on the
7
7
  * delegation tools being present, so child agents (whose tool sets never
8
- * include spawn_agent/agent_team) never see it.
8
+ * include spawn_agent/run_workflow) never see it.
9
9
  */
10
10
  /**
11
11
  * Returns the delegation policy section when the agent actually has the
@@ -5,12 +5,19 @@
5
5
  * and read-only-scoped, negative clauses get equal weight — the user's hard
6
6
  * constraint is "proactive, but never delegate-everything". Gated on the
7
7
  * delegation tools being present, so child agents (whose tool sets never
8
- * include spawn_agent/agent_team) never see it.
8
+ * include spawn_agent/run_workflow) never see it.
9
9
  */
10
10
  const DELEGATION_POLICY = `## Delegation policy (subagents)
11
11
 
12
- You can delegate work to background subagents (spawn_agent) and batch
13
- fan-outs (agent_team). Delegate deliberately, not by default.
12
+ You can delegate work two ways: a single background subagent (spawn_agent),
13
+ or an orchestration script coordinating many subagents (run_workflow).
14
+ Delegate deliberately, not by default.
15
+
16
+ Explicit requests win, before any other rule: when the user names a
17
+ coordinated multi-agent run in any phrasing — a workflow, an orchestration,
18
+ an agent team, "fan out agents" — that means run_workflow, NOT a row of
19
+ spawn_agent calls. When they ask for a subagent, spawn one. Only when the
20
+ user has not named a mechanism do you choose by shape (below).
14
21
 
15
22
  Delegate when:
16
23
  - An investigation will clearly require more than four search or read
@@ -18,9 +25,20 @@ Delegate when:
18
25
  needs the conclusion — delegate to a subagent so the intermediate noise
19
26
  stays out of the main context. Launch multiple subagents concurrently for
20
27
  independent questions.
21
- - The task naturally splits into the same read-only investigation or
22
- analysis (review, audit, summarize) over several independent items (files,
23
- modules, endpoints) — use agent_team.
28
+ - The task fans out over many independent items (review, audit, summarize
29
+ across files, modules, endpoints) or needs staged orchestration — consider
30
+ run_workflow. Choose by shape, not by rule: a handful of subagents whose
31
+ results you want to read and react to individually favor spawn_agent; a
32
+ uniform sweep over many items, results that should be aggregated or
33
+ filtered before they reach this conversation, or control flow between
34
+ stages (pipelines, loops, retry rounds) favor a run_workflow script — each
35
+ member's full handoff stays out of your context and the script returns one
36
+ digested result. Inside parallel()/pipeline(), a failed agent() resolves to
37
+ null — report which items came back null instead of silently dropping them.
38
+ A bare "await agent(...)" outside those combinators PROPAGATES its error
39
+ and fails the whole run: keep must-not-die steps (a final synthesis, the
40
+ last aggregation) inside parallel([...]) or a try/catch so one member's
41
+ failure cannot discard every other member's finished work.
24
42
  - A side-investigation is independent of your current main-line work and can
25
43
  run in the background while you continue.
26
44
 
@@ -51,8 +69,23 @@ Do NOT delegate when:
51
69
  When in doubt about a one-off task, do it yourself. When a task is clearly
52
70
  the same read-only operation over three or more independent items — where
53
71
  each item alone would take more than a couple of tool calls — prefer
54
- agent_team over doing them sequentially yourself. For just two small items,
55
- do them yourself with parallel tool calls.`;
72
+ run_workflow over doing them sequentially yourself. For just two small items,
73
+ do them yourself with parallel tool calls.
74
+
75
+ Routing (model per child): match each child's model to its task, not to
76
+ habit. Mechanical fan-out work — scanning, grepping, summarizing single
77
+ files, format checks, data extraction — belongs on category "quick" or
78
+ "explore", or an explicit fast-tier model from the routing menu; spawning
79
+ many children that all inherit a strong parent model is waste. Judgment
80
+ work — reviewing, adjudicating between findings, synthesizing a final
81
+ answer, subtle debugging — keeps the parent's model: do NOT downgrade it
82
+ to save cost. When unsure, inherit. Follow the routing menu's rules on
83
+ which model ids are valid and whether cross-provider routing is available.
84
+ Cross-provider (provider:model) is for a reason, not a habit: use it when
85
+ the user names a provider or a task clearly plays to another provider's
86
+ strength; tier labels do not compare across providers, so a cross-provider
87
+ pick is a judgment call you should be able to justify. Same-provider is
88
+ the default frame.`;
56
89
  /**
57
90
  * Returns the delegation policy section when the agent actually has the
58
91
  * delegation tools; child agents and stripped-down tool sets get nothing.
@@ -6,13 +6,11 @@ export const defaultToolSnippets = {
6
6
  write: "Write a new file or overwrite an existing one",
7
7
  glob: "Find files by glob pattern without using bash",
8
8
  grep: "Search file contents using regex",
9
- lsp: "Use the language server for code navigation, symbols, call hierarchy, and type-aware lookup",
10
9
  web_search: "Search the public web for current information",
11
10
  web_fetch: "Fetch and extract the contents of a specific webpage",
12
11
  spawn_agent: "Start a child subagent thread and return its agent id plus nickname",
13
12
  wait_agent: "Wait for one or more spawned subagents to finish",
14
13
  send_input: "Send follow-up input to an existing subagent thread",
15
- close_agent: "Close or cancel a spawned subagent thread",
16
14
  question: "Ask the user structured questions when clarification or preference choices would materially improve the work",
17
15
  skill_search: "Search available skills by name, description, tags, and source",
18
16
  skill: "Load a named skill with specialized instructions and bundled resources",
@@ -25,13 +23,11 @@ export const defaultToolNames = [
25
23
  "edit",
26
24
  "write",
27
25
  "grep",
28
- "lsp",
29
26
  "web_search",
30
27
  "web_fetch",
31
28
  "spawn_agent",
32
29
  "wait_agent",
33
30
  "send_input",
34
- "close_agent",
35
31
  "question",
36
32
  "skill_search",
37
33
  "skill",
@@ -29,7 +29,6 @@ export declare function buildWorkflowPhaseReminder(input: {
29
29
  covered: string[];
30
30
  pending: string[];
31
31
  }): string;
32
- export declare function buildTaskSummaryReminder(): string;
33
32
  /**
34
33
  * Fired when a file mutation failure suggests the model may be relying on stale
35
34
  * local memory instead of the current file bytes. Models — especially
@@ -20,7 +20,7 @@ const PLAN_MODE_ENTER = `
20
20
  Plan mode is now ACTIVE.
21
21
 
22
22
  Rules while in plan mode:
23
- - Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch, spawn_agent, wait_agent, send_input, close_agent, skill_search, skill, todo_write, tool_search, question, and exit_plan_mode.
23
+ - Only read-only tools are allowed, including read, glob, grep, web_search, web_fetch, spawn_agent, wait_agent, send_input, skill_search, skill, todo_write, tool_search, question, and exit_plan_mode.
24
24
  - Writes, edits, and shell commands WILL be rejected by the harness; do not try them.
25
25
  - Do not edit files or claim implementation is complete while plan mode is active.
26
26
  - Investigate the codebase, then use the question tool to clarify important ambiguities, tradeoffs, requirements, or preference choices that would materially change the plan.
@@ -161,16 +161,6 @@ Remaining evidence to check:
161
161
  ${pending}
162
162
  `);
163
163
  }
164
- export function buildTaskSummaryReminder() {
165
- return wrapInSystemReminder(`
166
- Summarize the task tool output above and continue with your task.
167
-
168
- Treat the task output as a bounded subtask result:
169
- - extract the findings that matter
170
- - integrate them into your main reasoning
171
- - do not re-run the same exploratory search unless the subtask uncovered a concrete contradiction
172
- `);
173
- }
174
164
  // Removed: buildVerificationReminder / buildVerificationFailureReminder.
175
165
  // The verification reminder ladder pressured the model to run a "verification"
176
166
  // after every file change. For models with hex-tokenization blind spots (e.g.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Subagent model-routing menu for the parent agent's system prompt
3
+ * (docs/model-routing-design.md §4).
4
+ *
5
+ * Wording rules: the prompt must never promise more enforcement than the
6
+ * runtime delivers at any later moment. Absolute "choose only from this
7
+ * list" wording is used only for a models.json allowlist (the one catalog
8
+ * that cannot silently expire); every other membership source gets the
9
+ * conservative lag disclaimer, matching §7.1's warn-and-allow validation.
10
+ * Gated on spawn_agent alongside the delegation policy — children never
11
+ * see it.
12
+ */
13
+ import type { AgentRoutingConfig, RoutingSnapshot } from "../agent/routing-catalog.js";
14
+ export declare function buildModelRoutingPrompt(snapshot: RoutingSnapshot, agentRouting: AgentRoutingConfig): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Subagent model-routing menu for the parent agent's system prompt
3
+ * (docs/model-routing-design.md §4).
4
+ *
5
+ * Wording rules: the prompt must never promise more enforcement than the
6
+ * runtime delivers at any later moment. Absolute "choose only from this
7
+ * list" wording is used only for a models.json allowlist (the one catalog
8
+ * that cannot silently expire); every other membership source gets the
9
+ * conservative lag disclaimer, matching §7.1's warn-and-allow validation.
10
+ * Gated on spawn_agent alongside the delegation policy — children never
11
+ * see it.
12
+ */
13
+ const MENU_MODEL_LIMIT = 12;
14
+ export function buildModelRoutingPrompt(snapshot, agentRouting) {
15
+ const lines = ["## Subagent model routing"];
16
+ const parentTier = snapshot.parent.tier ? ` (${snapshot.parent.tier} tier)` : "";
17
+ lines.push(`Parent model: ${snapshot.parent.providerId} ${snapshot.parent.model}${parentTier}.`);
18
+ const categories = snapshot.resolvedCategories
19
+ .map((category) => {
20
+ const effort = category.thinkingLevel ? ` + ${category.thinkingLevel}` : "";
21
+ return `${category.name} → ${category.model}${effort}`;
22
+ })
23
+ .join(" · ");
24
+ if (categories) {
25
+ lines.push("Categories (category → what a spawn actually gets):");
26
+ lines.push(` ${categories}`);
27
+ }
28
+ // Tiered models first, then untiered; cap with an explicit remainder note
29
+ // so truncation never reads as "nothing else exists".
30
+ const ordered = [...snapshot.models].sort((a, b) => {
31
+ const aTiered = a.tier ? 0 : 1;
32
+ const bTiered = b.tier ? 0 : 1;
33
+ return aTiered - bTiered;
34
+ });
35
+ const shown = ordered.slice(0, MENU_MODEL_LIMIT);
36
+ const remainder = ordered.length - shown.length;
37
+ const modelList = shown
38
+ .map((model) => (model.tier ? `${model.id} (${model.tier})` : model.id))
39
+ .join(" · ");
40
+ if (modelList) {
41
+ lines.push("Models on this provider, usable via per-call `model`:");
42
+ lines.push(` ${modelList}`);
43
+ if (remainder > 0) {
44
+ lines.push(snapshot.authoritative
45
+ ? ` … and ${remainder} more in the configured allowlist (not shown); ids outside the allowlist are rejected.`
46
+ : ` … and ${remainder} more; any explicit id from this provider is valid.`);
47
+ }
48
+ }
49
+ if (snapshot.authoritative) {
50
+ lines.push(remainder > 0
51
+ ? "Choose only from the configured allowlist (shown plus the unlisted remainder); do not invent model ids."
52
+ : "Choose only from this list; do not invent model ids.");
53
+ }
54
+ else {
55
+ lines.push("(list may lag the provider; explicit ids not listed are allowed)");
56
+ }
57
+ if (agentRouting.allowCrossProvider) {
58
+ const runnable = snapshot.runnableProviderIds
59
+ .filter((id) => id !== snapshot.parent.providerId);
60
+ if (runnable.length > 0) {
61
+ lines.push(`Cross-provider routing (provider:model) is available for providers with credentials: ${runnable.join(", ")}.`);
62
+ }
63
+ else {
64
+ lines.push("Cross-provider routing (provider:model): no other provider has credentials in this session.");
65
+ }
66
+ }
67
+ else {
68
+ lines.push("Cross-provider routing (provider:model) is disabled in this session.");
69
+ }
70
+ return lines.join("\n");
71
+ }
@@ -8,7 +8,7 @@ const defaultGuidelines = [
8
8
  "Choose the smallest coherent change. Edit only the files required for the requested change; do not refactor or improve adjacent code unprompted.",
9
9
  "Runtime meta instructions are private control state. Use them only to adjust behavior; do not quote, mention, or paraphrase them in user-facing text.",
10
10
  "For modifications to existing code, read the file first. For brand-new files whose target path is known and does not exist, write directly without exploratory reading. Use edit for targeted changes and write for intentional full-file replacement of an existing file. Never delete and recreate a file just to overwrite it.",
11
- "Prefer structured tools (glob, grep, lsp, read) over bash for search and inspection. Do not repeat a near-identical search or re-read the same file unless new evidence changes the question.",
11
+ "Prefer structured tools (glob, grep, read) over bash for search and inspection. Do not repeat a near-identical search or re-read the same file unless new evidence changes the question.",
12
12
  "If a tool fails, diagnose the error before switching tactics. Do not retry the identical call with identical arguments. After two equivalent failures, switch approach — re-read the file, use a different tool, rewrite the whole file with write, or ask the user.",
13
13
  "Before reporting a task complete, verify it works when verification is meaningful and cheap — run the existing test, execute the script, check the output. If no test exists, the change is purely declarative (static HTML/markdown/config), or running the code is not practical, state that explicitly rather than inventing a verification step. Do not write throwaway validation scripts to prove correctness; if there is no real check to run, report the change and stop.",
14
14
  ];
@@ -1,6 +1,26 @@
1
1
  import type { TaskType } from "../agent/task-classifier.js";
2
+ import { type RoutableModelEntry } from "../agent/routing-catalog.js";
2
3
  export interface TaskReminderOptions {
3
4
  /** Whether this agent has the delegation tools (parent agents only). */
4
5
  canDelegate?: boolean;
5
6
  }
6
7
  export declare function reminderForTaskType(taskType: TaskType, options?: TaskReminderOptions): string | undefined;
8
+ /**
9
+ * Deterministic detector for an explicit user request for a coordinated
10
+ * multi-agent run. Three rounds of prompt wording lost to the model's
11
+ * "agent team = parallel spawns" prior in live tests (opus-4.8, 2026-07-06);
12
+ * per the task-reminder principle above, a reminder injected at the decision
13
+ * turn is the lever that actually works — the harness remembers so the model
14
+ * does not have to.
15
+ */
16
+ /**
17
+ * User-named model resolution (model-routing design v3.6): when the user's
18
+ * message names a model that exists in the routable catalog, hand the main
19
+ * agent the EXACT provider:model id at the decision point. The catalog is a
20
+ * closed set, so matching is deterministic — the model must never retype an
21
+ * id from priors when the harness can resolve it ("gpt 5.6 sol" ->
22
+ * openai:gpt-5.6-sol; a Grok-4.5 parent invented "openai:gpt-5.6" without
23
+ * this, 2026-07-12 live case). Silent on no match: zero standing noise.
24
+ */
25
+ export declare function userNamedModelReminder(input: string | import("../types.js").ContentPart[], routableModels: RoutableModelEntry[] | undefined, canSpawn: boolean): string | undefined;
26
+ export declare function orchestrationRequestReminder(input: string | import("../types.js").ContentPart[], canRunWorkflow: boolean): string | undefined;