@bubblebrain-ai/bubble 0.0.35 → 0.0.37
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.
- package/README.md +27 -7
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -0
- package/dist/agent/profiles.js +25 -11
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/workflow/control.d.ts +14 -0
- package/dist/agent/workflow/control.js +30 -9
- package/dist/agent.d.ts +75 -5
- package/dist/agent.js +313 -61
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +105 -27
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +87 -41
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +17 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.js +22 -3
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +19 -0
- package/dist/prompt/task-reminders.js +49 -0
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +28 -5
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.js +9 -11
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +649 -233
- package/dist/tui-ink/approval/approval-dialog.js +36 -0
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +35 -1
- package/dist/tui-ink/input-box.js +215 -122
- package/dist/tui-ink/message-list.js +5 -5
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +486 -133
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +31 -0
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +9 -0
- package/dist/variant/variant-resolver.js +17 -0
- package/package.json +2 -1
- package/dist/tools/task.d.ts +0 -2
- 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
|
+
}
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/oauth/index.js
CHANGED
package/dist/oauth/storage.d.ts
CHANGED
|
@@ -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;
|
package/dist/oauth/storage.js
CHANGED
|
@@ -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,
|
|
8
|
-
import { orchestrationRequestReminder, 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() {
|
|
@@ -34,6 +34,13 @@ export function createDefaultHooks() {
|
|
|
34
34
|
if (orchestrationReminder) {
|
|
35
35
|
ctx.queueReminder(orchestrationReminder);
|
|
36
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
|
+
}
|
|
37
44
|
// Small-task hint: counterweight to the default protocol's exploration
|
|
38
45
|
// bias, only fires once per run on focused one-shot requests like
|
|
39
46
|
// "写个 HTML 介绍元旦". Don't issue for the same input twice.
|
|
@@ -147,9 +154,6 @@ export function createDefaultHooks() {
|
|
|
147
154
|
// edit was correct. CC's approach is the opposite: verify when there
|
|
148
155
|
// is something real to verify, say so explicitly when there isn't, and
|
|
149
156
|
// trust the model to judge. We follow that.
|
|
150
|
-
if (ctx.toolCall.name === "task") {
|
|
151
|
-
ctx.queueReminder(buildTaskSummaryReminder());
|
|
152
|
-
}
|
|
153
157
|
if (ctx.state.governor) {
|
|
154
158
|
for (const reminder of ctx.state.governor.consumePendingReminders()) {
|
|
155
159
|
ctx.queueReminder(reminder);
|
|
@@ -163,6 +167,13 @@ export function createDefaultHooks() {
|
|
|
163
167
|
ctx.queueReminder(reminder);
|
|
164
168
|
}
|
|
165
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
|
+
}
|
|
166
177
|
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker?.isCoreCoverageComplete()) {
|
|
167
178
|
ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
|
|
168
179
|
return;
|
|
@@ -196,8 +207,7 @@ function isSubagentLifecycleTool(name) {
|
|
|
196
207
|
return name === "spawn_agent"
|
|
197
208
|
|| name === "wait_agent"
|
|
198
209
|
|| name === "send_input"
|
|
199
|
-
|| name === "close_agent"
|
|
200
|
-
|| name === "task";
|
|
210
|
+
|| name === "close_agent";
|
|
201
211
|
}
|
|
202
212
|
function hashEditCall(toolCall) {
|
|
203
213
|
// Cheap fingerprint that identifies "same edit/write call". JSON of the
|
package/dist/prompt/compose.d.ts
CHANGED
|
@@ -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;
|
package/dist/prompt/compose.js
CHANGED
|
@@ -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");
|
|
@@ -33,8 +33,12 @@ Delegate when:
|
|
|
33
33
|
filtered before they reach this conversation, or control flow between
|
|
34
34
|
stages (pipelines, loops, retry rounds) favor a run_workflow script — each
|
|
35
35
|
member's full handoff stays out of your context and the script returns one
|
|
36
|
-
digested result.
|
|
37
|
-
items came back null instead of silently dropping them.
|
|
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.
|
|
38
42
|
- A side-investigation is independent of your current main-line work and can
|
|
39
43
|
run in the background while you continue.
|
|
40
44
|
|
|
@@ -66,7 +70,22 @@ When in doubt about a one-off task, do it yourself. When a task is clearly
|
|
|
66
70
|
the same read-only operation over three or more independent items — where
|
|
67
71
|
each item alone would take more than a couple of tool calls — prefer
|
|
68
72
|
run_workflow over doing them sequentially yourself. For just two small items,
|
|
69
|
-
do them yourself with parallel tool calls
|
|
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.`;
|
|
70
89
|
/**
|
|
71
90
|
* Returns the delegation policy section when the agent actually has the
|
|
72
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
|
package/dist/prompt/reminders.js
CHANGED
|
@@ -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,
|
|
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
|
+
}
|
package/dist/prompt/runtime.js
CHANGED
|
@@ -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,
|
|
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,7 +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;
|
|
7
26
|
export declare function orchestrationRequestReminder(input: string | import("../types.js").ContentPart[], canRunWorkflow: boolean): string | undefined;
|