@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,882 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { verifyGrokBinary } from "./grok-binary.js";
|
|
3
|
+
import { acquireGrokProfileLock, buildGrokChildEnv, clearGrokGeneratedExtensions, clearGrokRuntimeData, getGrokProfile, grokProfileHasAuth, prepareGrokProfile, prepareGrokProfileLockRoot, } from "./grok-profile.js";
|
|
4
|
+
import { collectGrokProjectSkills, runGrokInspectJson, runGrokPreflight, validateGrokInspect, } from "./grok-preflight.js";
|
|
5
|
+
import { defaultGrokSpawn, runGrokCommand, } from "./grok-process.js";
|
|
6
|
+
import { createDefaultGrokAcpClient, } from "./grok-acp.js";
|
|
7
|
+
import { GrokRuntimeError, safeErrorMessage } from "./grok-errors.js";
|
|
8
|
+
import { resolveGrokNetworkRoute } from "./grok-network.js";
|
|
9
|
+
export const GROK_RUNTIME_CAPABILITIES = Object.freeze({
|
|
10
|
+
chat: true,
|
|
11
|
+
tools: true,
|
|
12
|
+
memory: false,
|
|
13
|
+
subagents: false,
|
|
14
|
+
plan: false,
|
|
15
|
+
web: false,
|
|
16
|
+
sessionLoad: true,
|
|
17
|
+
workspace: true,
|
|
18
|
+
modelControl: true,
|
|
19
|
+
reasoningControl: true,
|
|
20
|
+
});
|
|
21
|
+
export function grokLoginFailureMessage(diagnostic) {
|
|
22
|
+
if (/(?:openid-configuration|error sending request|connection reset|connect(?:ion)? (?:failed|refused)|timed? out|dns error|name or service not known)/i
|
|
23
|
+
.test(diagnostic)) {
|
|
24
|
+
return "Could not reach xAI, so no sign-in page was created. Check your network and try Grok Subscription again.";
|
|
25
|
+
}
|
|
26
|
+
return "Grok login did not complete.";
|
|
27
|
+
}
|
|
28
|
+
const defaultGrokOAuthOpener = async (url, signal) => {
|
|
29
|
+
const result = await runGrokCommand(defaultGrokSpawn, "/usr/bin/open", [url], {
|
|
30
|
+
cwd: "/",
|
|
31
|
+
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" },
|
|
32
|
+
signal,
|
|
33
|
+
discardStdout: true,
|
|
34
|
+
maxStdoutBytes: 4 * 1024,
|
|
35
|
+
maxStderrBytes: 4 * 1024,
|
|
36
|
+
});
|
|
37
|
+
if (result.code !== 0) {
|
|
38
|
+
throw new GrokRuntimeError("not_authenticated", "Unable to open the xAI sign-in page.");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const END = Symbol("end");
|
|
42
|
+
class AsyncEventQueue {
|
|
43
|
+
values = [];
|
|
44
|
+
waiters = [];
|
|
45
|
+
terminalError;
|
|
46
|
+
ended = false;
|
|
47
|
+
push(value) {
|
|
48
|
+
if (this.ended)
|
|
49
|
+
return;
|
|
50
|
+
const waiter = this.waiters.shift();
|
|
51
|
+
if (waiter)
|
|
52
|
+
waiter.resolve(value);
|
|
53
|
+
else
|
|
54
|
+
this.values.push(value);
|
|
55
|
+
}
|
|
56
|
+
end() {
|
|
57
|
+
if (this.ended)
|
|
58
|
+
return;
|
|
59
|
+
this.ended = true;
|
|
60
|
+
const waiter = this.waiters.shift();
|
|
61
|
+
if (waiter)
|
|
62
|
+
waiter.resolve(END);
|
|
63
|
+
else
|
|
64
|
+
this.values.push(END);
|
|
65
|
+
for (const extra of this.waiters.splice(0))
|
|
66
|
+
extra.resolve(END);
|
|
67
|
+
}
|
|
68
|
+
dropPendingOutput() {
|
|
69
|
+
this.values = this.values.filter((value) => value === END || value.type === "turn_start");
|
|
70
|
+
}
|
|
71
|
+
fail(error, discardPendingOutput = false) {
|
|
72
|
+
if (this.ended)
|
|
73
|
+
return;
|
|
74
|
+
if (discardPendingOutput)
|
|
75
|
+
this.dropPendingOutput();
|
|
76
|
+
this.ended = true;
|
|
77
|
+
this.terminalError = error;
|
|
78
|
+
for (const waiter of this.waiters.splice(0))
|
|
79
|
+
waiter.reject(error);
|
|
80
|
+
}
|
|
81
|
+
async next() {
|
|
82
|
+
const value = this.values.shift();
|
|
83
|
+
if (value !== undefined)
|
|
84
|
+
return value;
|
|
85
|
+
if (this.terminalError !== undefined)
|
|
86
|
+
throw this.terminalError;
|
|
87
|
+
if (this.ended)
|
|
88
|
+
return END;
|
|
89
|
+
return await new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function notificationGeneration(meta) {
|
|
93
|
+
if (!meta)
|
|
94
|
+
return undefined;
|
|
95
|
+
for (const key of ["generation", "bubbleGeneration", "bubble_generation"]) {
|
|
96
|
+
if (typeof meta[key] === "number")
|
|
97
|
+
return meta[key];
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
function externalToolArgs(input) {
|
|
102
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
103
|
+
return input;
|
|
104
|
+
}
|
|
105
|
+
return input === undefined ? {} : { input };
|
|
106
|
+
}
|
|
107
|
+
function externalToolOutput(output) {
|
|
108
|
+
if (typeof output === "string")
|
|
109
|
+
return output.slice(0, 16 * 1024);
|
|
110
|
+
try {
|
|
111
|
+
return JSON.stringify(output ?? "", null, 2).slice(0, 16 * 1024);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return "Grok tool returned an unreadable result.";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export class GrokRuntimeManager {
|
|
118
|
+
deps;
|
|
119
|
+
spawn;
|
|
120
|
+
acpFactory;
|
|
121
|
+
oauthOpener;
|
|
122
|
+
profile;
|
|
123
|
+
workspace;
|
|
124
|
+
binary;
|
|
125
|
+
acp;
|
|
126
|
+
lock;
|
|
127
|
+
state = "unavailable";
|
|
128
|
+
currentSessionId;
|
|
129
|
+
attachedSessionId;
|
|
130
|
+
activeTurn;
|
|
131
|
+
nextGeneration = 0;
|
|
132
|
+
closingSidecar;
|
|
133
|
+
loginController;
|
|
134
|
+
loginOperation;
|
|
135
|
+
sessionLifecyclePhase = "idle";
|
|
136
|
+
networkRoute;
|
|
137
|
+
networkRouteOperation;
|
|
138
|
+
modelId;
|
|
139
|
+
reasoningEffort = "high";
|
|
140
|
+
modelCache;
|
|
141
|
+
activeTools = new Map();
|
|
142
|
+
constructor(deps = {}) {
|
|
143
|
+
this.deps = deps;
|
|
144
|
+
this.spawn = deps.spawn ?? defaultGrokSpawn;
|
|
145
|
+
this.acpFactory = deps.acpFactory ?? createDefaultGrokAcpClient;
|
|
146
|
+
this.oauthOpener = deps.oauthOpener ?? defaultGrokOAuthOpener;
|
|
147
|
+
this.profile = getGrokProfile(deps.bubbleHome);
|
|
148
|
+
this.workspace = resolve(deps.workspace ?? process.cwd());
|
|
149
|
+
}
|
|
150
|
+
status(message) {
|
|
151
|
+
return {
|
|
152
|
+
provider: "grok",
|
|
153
|
+
state: this.state,
|
|
154
|
+
capabilities: GROK_RUNTIME_CAPABILITIES,
|
|
155
|
+
binary: this.binary,
|
|
156
|
+
sessionId: this.currentSessionId,
|
|
157
|
+
message,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async ensureNetworkRoute() {
|
|
161
|
+
if (this.networkRoute)
|
|
162
|
+
return this.networkRoute;
|
|
163
|
+
if (!this.networkRouteOperation) {
|
|
164
|
+
const resolver = this.deps.networkResolver ?? resolveGrokNetworkRoute;
|
|
165
|
+
this.networkRouteOperation = resolver()
|
|
166
|
+
.then((route) => {
|
|
167
|
+
this.networkRoute = route;
|
|
168
|
+
return route;
|
|
169
|
+
})
|
|
170
|
+
.finally(() => {
|
|
171
|
+
this.networkRouteOperation = undefined;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return await this.networkRouteOperation;
|
|
175
|
+
}
|
|
176
|
+
async prepareAndVerify(signal) {
|
|
177
|
+
await prepareGrokProfile(this.profile, this.deps.uid ?? process.getuid?.());
|
|
178
|
+
const env = buildGrokChildEnv(this.profile, this.deps.parentEnv ?? process.env, this.networkRoute?.proxy);
|
|
179
|
+
const binary = await verifyGrokBinary({
|
|
180
|
+
...this.deps,
|
|
181
|
+
readVersion: this.deps.readVersion ?? (async (path) => {
|
|
182
|
+
const result = await runGrokCommand(this.spawn, path, ["--version"], {
|
|
183
|
+
cwd: this.profile.workspace,
|
|
184
|
+
env,
|
|
185
|
+
signal,
|
|
186
|
+
maxStdoutBytes: 4096,
|
|
187
|
+
});
|
|
188
|
+
if (result.code !== 0) {
|
|
189
|
+
throw new GrokRuntimeError("binary_version_mismatch", "Unable to verify the Grok CLI version.", result.stderr);
|
|
190
|
+
}
|
|
191
|
+
return result.stdout;
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
this.binary = binary;
|
|
195
|
+
return { profile: this.profile, env, binary };
|
|
196
|
+
}
|
|
197
|
+
async acquireProfileLock() {
|
|
198
|
+
await prepareGrokProfileLockRoot(this.profile, this.deps.uid ?? process.getuid?.());
|
|
199
|
+
return await acquireGrokProfileLock(this.profile);
|
|
200
|
+
}
|
|
201
|
+
async inspect() {
|
|
202
|
+
if (this.acp)
|
|
203
|
+
return this.status();
|
|
204
|
+
let temporaryLock;
|
|
205
|
+
try {
|
|
206
|
+
temporaryLock = await this.acquireProfileLock();
|
|
207
|
+
const { profile, env, binary } = await this.prepareAndVerify();
|
|
208
|
+
await clearGrokGeneratedExtensions(profile);
|
|
209
|
+
await runGrokPreflight(this.spawn, binary.path, profile, env, profile.workspace);
|
|
210
|
+
await clearGrokGeneratedExtensions(profile);
|
|
211
|
+
this.state = (await grokProfileHasAuth(profile)) ? "ready" : "signed_out";
|
|
212
|
+
return this.status();
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
this.state = error instanceof GrokRuntimeError && error.code === "not_authenticated" ? "signed_out" : "unavailable";
|
|
216
|
+
return this.status(safeErrorMessage(error));
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
// Never mutate a profile owned by another Bubble process. Cleanup is
|
|
220
|
+
// permitted only after this inspect call acquired the runtime lock.
|
|
221
|
+
if (temporaryLock) {
|
|
222
|
+
await clearGrokGeneratedExtensions(this.profile).catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
await temporaryLock?.release();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async login(signal, onBrowserOpened) {
|
|
228
|
+
if (this.loginOperation) {
|
|
229
|
+
throw new GrokRuntimeError("protocol_error", "A Grok login is already in progress.");
|
|
230
|
+
}
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
const relayAbort = () => controller.abort(signal?.reason);
|
|
233
|
+
if (signal?.aborted)
|
|
234
|
+
relayAbort();
|
|
235
|
+
else
|
|
236
|
+
signal?.addEventListener("abort", relayAbort, { once: true });
|
|
237
|
+
const operation = this.performLogin(controller, onBrowserOpened);
|
|
238
|
+
this.loginController = controller;
|
|
239
|
+
this.loginOperation = operation;
|
|
240
|
+
try {
|
|
241
|
+
await operation;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
signal?.removeEventListener("abort", relayAbort);
|
|
245
|
+
if (this.loginOperation === operation)
|
|
246
|
+
this.loginOperation = undefined;
|
|
247
|
+
if (this.loginController === controller)
|
|
248
|
+
this.loginController = undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async performLogin(controller, onBrowserOpened) {
|
|
252
|
+
const signal = controller.signal;
|
|
253
|
+
const profile = this.profile;
|
|
254
|
+
await this.disconnect();
|
|
255
|
+
if (signal.aborted)
|
|
256
|
+
throw new GrokRuntimeError("cancelled", "Grok login cancelled.");
|
|
257
|
+
await this.ensureNetworkRoute();
|
|
258
|
+
const lock = await this.acquireProfileLock();
|
|
259
|
+
let openerPromise;
|
|
260
|
+
let openerFailed = false;
|
|
261
|
+
try {
|
|
262
|
+
const { env, binary } = await this.prepareAndVerify(signal);
|
|
263
|
+
await clearGrokGeneratedExtensions(profile);
|
|
264
|
+
await runGrokPreflight(this.spawn, binary.path, profile, env, profile.workspace, signal);
|
|
265
|
+
await clearGrokGeneratedExtensions(profile);
|
|
266
|
+
await clearGrokRuntimeData(profile, true);
|
|
267
|
+
if (signal.aborted)
|
|
268
|
+
throw new GrokRuntimeError("cancelled", "Grok login cancelled.");
|
|
269
|
+
const result = await runGrokCommand(this.spawn, binary.path, ["--no-auto-update", "--cwd", profile.workspace, "login", "--oauth"], {
|
|
270
|
+
cwd: profile.workspace,
|
|
271
|
+
env,
|
|
272
|
+
signal,
|
|
273
|
+
discardStdout: true,
|
|
274
|
+
onOAuthAuthorizeUrl: (url) => {
|
|
275
|
+
if (openerPromise)
|
|
276
|
+
return;
|
|
277
|
+
openerPromise = Promise.resolve()
|
|
278
|
+
.then(async () => await this.oauthOpener(url, signal))
|
|
279
|
+
.then(() => {
|
|
280
|
+
try {
|
|
281
|
+
onBrowserOpened?.();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// UI status reporting must never invalidate a successful
|
|
285
|
+
// browser handoff or abort the OAuth process.
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
.catch((error) => {
|
|
289
|
+
openerFailed = true;
|
|
290
|
+
if (!signal.aborted)
|
|
291
|
+
controller.abort(error);
|
|
292
|
+
throw error;
|
|
293
|
+
});
|
|
294
|
+
// The command may remain blocked waiting for browser completion;
|
|
295
|
+
// attach a handler immediately so opener failure never becomes an
|
|
296
|
+
// unhandled rejection while abort tears the child down.
|
|
297
|
+
void openerPromise.catch(() => undefined);
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
await openerPromise;
|
|
301
|
+
if (result.code !== 0 || !(await grokProfileHasAuth(profile))) {
|
|
302
|
+
this.state = "signed_out";
|
|
303
|
+
throw new GrokRuntimeError("not_authenticated", grokLoginFailureMessage(result.stderr), result.stderr);
|
|
304
|
+
}
|
|
305
|
+
await clearGrokRuntimeData(profile, false);
|
|
306
|
+
await clearGrokGeneratedExtensions(profile);
|
|
307
|
+
this.state = "ready";
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.state = "signed_out";
|
|
311
|
+
await clearGrokRuntimeData(profile, true);
|
|
312
|
+
await clearGrokGeneratedExtensions(profile);
|
|
313
|
+
if (openerFailed) {
|
|
314
|
+
throw new GrokRuntimeError("not_authenticated", "Unable to open the xAI sign-in page.");
|
|
315
|
+
}
|
|
316
|
+
if (signal.aborted) {
|
|
317
|
+
throw new GrokRuntimeError("cancelled", "Grok login cancelled.");
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
await lock.release();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async logout() {
|
|
326
|
+
await this.disconnect();
|
|
327
|
+
const profile = this.profile;
|
|
328
|
+
const lock = await this.acquireProfileLock();
|
|
329
|
+
try {
|
|
330
|
+
await prepareGrokProfile(profile, this.deps.uid ?? process.getuid?.());
|
|
331
|
+
try {
|
|
332
|
+
const { env, binary } = await this.prepareAndVerify();
|
|
333
|
+
await runGrokCommand(this.spawn, binary.path, ["--no-auto-update", "--cwd", profile.workspace, "logout"], { cwd: profile.workspace, env });
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Local credential deletion below is authoritative for Bubble's
|
|
337
|
+
// isolated profile. A missing/broken CLI must not make logout
|
|
338
|
+
// impossible or leave the session binding stranded.
|
|
339
|
+
}
|
|
340
|
+
await clearGrokRuntimeData(profile, true);
|
|
341
|
+
await clearGrokGeneratedExtensions(profile);
|
|
342
|
+
if (await grokProfileHasAuth(profile)) {
|
|
343
|
+
throw new GrokRuntimeError("profile_unsafe", "Grok credentials could not be removed from the isolated profile.");
|
|
344
|
+
}
|
|
345
|
+
this.currentSessionId = undefined;
|
|
346
|
+
this.attachedSessionId = undefined;
|
|
347
|
+
this.state = "signed_out";
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
await lock.release();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async ensureConnected() {
|
|
354
|
+
await this.closingSidecar;
|
|
355
|
+
if (this.acp)
|
|
356
|
+
return this.acp;
|
|
357
|
+
await this.ensureNetworkRoute();
|
|
358
|
+
const lock = await this.acquireProfileLock();
|
|
359
|
+
let client;
|
|
360
|
+
try {
|
|
361
|
+
const { profile, env, binary } = await this.prepareAndVerify();
|
|
362
|
+
await clearGrokGeneratedExtensions(profile);
|
|
363
|
+
// The pinned CLI always lists workspace project skills in inspect, even
|
|
364
|
+
// once loading is disabled. Discover them first, write every name into
|
|
365
|
+
// the profile's [skills] disabled list, then verify the workspace again
|
|
366
|
+
// against exactly that allowlist before the sidecar may start.
|
|
367
|
+
let payload = await runGrokInspectJson(this.spawn, binary.path, profile, env, this.workspace);
|
|
368
|
+
const workspaceSkills = await collectGrokProjectSkills(payload, this.workspace);
|
|
369
|
+
if (workspaceSkills.length > 0) {
|
|
370
|
+
await prepareGrokProfile(this.profile, this.deps.uid ?? process.getuid?.(), workspaceSkills);
|
|
371
|
+
await clearGrokGeneratedExtensions(profile);
|
|
372
|
+
payload = await runGrokInspectJson(this.spawn, binary.path, profile, env, this.workspace);
|
|
373
|
+
}
|
|
374
|
+
await validateGrokInspect(payload, profile, this.workspace, {
|
|
375
|
+
allowedProjectSkills: new Set(workspaceSkills),
|
|
376
|
+
allowCompatMcpServers: true,
|
|
377
|
+
});
|
|
378
|
+
await clearGrokGeneratedExtensions(profile);
|
|
379
|
+
if (!(await grokProfileHasAuth(profile))) {
|
|
380
|
+
this.state = "signed_out";
|
|
381
|
+
throw new GrokRuntimeError("not_authenticated", "Sign in to Grok before starting a subscription session.");
|
|
382
|
+
}
|
|
383
|
+
client = this.acpFactory({
|
|
384
|
+
binary: binary.path,
|
|
385
|
+
profile,
|
|
386
|
+
workspace: this.workspace,
|
|
387
|
+
modelId: this.modelId,
|
|
388
|
+
reasoningEffort: this.reasoningEffort,
|
|
389
|
+
permissionMode: this.deps.getPermissionMode?.() ?? "default",
|
|
390
|
+
env,
|
|
391
|
+
spawn: this.spawn,
|
|
392
|
+
handlers: {
|
|
393
|
+
onSessionUpdate: (notification) => this.handleSessionUpdate(notification),
|
|
394
|
+
onPermissionRequest: async (params) => {
|
|
395
|
+
const reject = params.options.find((option) => option.kind === "reject_once")
|
|
396
|
+
?? params.options.find((option) => option.kind === "reject_always");
|
|
397
|
+
if (!this.deps.approvalController)
|
|
398
|
+
return reject?.optionId;
|
|
399
|
+
const toolCall = params.toolCall;
|
|
400
|
+
const decision = await this.deps.approvalController.request({
|
|
401
|
+
type: "external_tool",
|
|
402
|
+
toolCallId: toolCall.toolCallId,
|
|
403
|
+
title: toolCall.title?.trim() || toolCall.kind || "Grok tool",
|
|
404
|
+
kind: toolCall.kind || "other",
|
|
405
|
+
rawInput: toolCall.rawInput,
|
|
406
|
+
locations: toolCall.locations?.map((location) => ({
|
|
407
|
+
path: location.path,
|
|
408
|
+
line: location.line,
|
|
409
|
+
})),
|
|
410
|
+
});
|
|
411
|
+
const kind = decision.action === "approve" ? "allow_once" : "reject_once";
|
|
412
|
+
return params.options.find((option) => option.kind === kind)?.optionId
|
|
413
|
+
?? reject?.optionId;
|
|
414
|
+
},
|
|
415
|
+
onPermissionViolation: async (sessionId) => {
|
|
416
|
+
await this.handlePolicyViolation(sessionId, "Grok requested an unsupported host capability.");
|
|
417
|
+
},
|
|
418
|
+
onCrash: (error) => this.handleCrash(error),
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
this.sessionLifecyclePhase = "initializing";
|
|
422
|
+
try {
|
|
423
|
+
await client.initialize();
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
this.sessionLifecyclePhase = "idle";
|
|
427
|
+
}
|
|
428
|
+
this.acp = client;
|
|
429
|
+
this.lock = lock;
|
|
430
|
+
this.attachedSessionId = undefined;
|
|
431
|
+
this.state = "ready";
|
|
432
|
+
return client;
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
await client?.dispose();
|
|
436
|
+
await clearGrokGeneratedExtensions(this.profile).catch(() => undefined);
|
|
437
|
+
await lock.release();
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
applyAcpModels(models) {
|
|
442
|
+
if (!models)
|
|
443
|
+
return;
|
|
444
|
+
this.modelCache = models.availableModels.map((model) => {
|
|
445
|
+
const reasoningLevels = model.id === "grok-4.5"
|
|
446
|
+
? ["low", "medium", "high"]
|
|
447
|
+
: ["off"];
|
|
448
|
+
return {
|
|
449
|
+
id: model.id,
|
|
450
|
+
name: model.name,
|
|
451
|
+
reasoningLevels,
|
|
452
|
+
defaultReasoningLevel: model.id === "grok-4.5" ? "high" : "off",
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
this.modelId = models.currentModelId;
|
|
456
|
+
const current = this.modelCache.find((model) => model.id === this.modelId);
|
|
457
|
+
if (current && !current.reasoningLevels.includes(this.reasoningEffort)) {
|
|
458
|
+
this.reasoningEffort = current.defaultReasoningLevel;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async newSession() {
|
|
462
|
+
const client = await this.ensureConnected();
|
|
463
|
+
try {
|
|
464
|
+
this.sessionLifecyclePhase = "creating";
|
|
465
|
+
const session = await client.newSession();
|
|
466
|
+
const id = session.id;
|
|
467
|
+
this.applyAcpModels(session.models);
|
|
468
|
+
this.currentSessionId = id;
|
|
469
|
+
this.attachedSessionId = id;
|
|
470
|
+
this.state = "ready";
|
|
471
|
+
return {
|
|
472
|
+
id,
|
|
473
|
+
provider: "grok",
|
|
474
|
+
modelId: this.modelId,
|
|
475
|
+
reasoningEffort: this.reasoningEffort,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
throw error instanceof GrokRuntimeError
|
|
480
|
+
? error
|
|
481
|
+
: new GrokRuntimeError("protocol_error", "Grok could not create a session.");
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
this.sessionLifecyclePhase = "idle";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async loadSession(id) {
|
|
488
|
+
if (!id.trim())
|
|
489
|
+
throw new GrokRuntimeError("protocol_error", "A Grok session ID is required.");
|
|
490
|
+
const client = await this.ensureConnected();
|
|
491
|
+
try {
|
|
492
|
+
this.sessionLifecyclePhase = "loading";
|
|
493
|
+
const session = await client.loadSession(id);
|
|
494
|
+
const loadedId = session.id;
|
|
495
|
+
this.applyAcpModels(session.models);
|
|
496
|
+
this.currentSessionId = loadedId;
|
|
497
|
+
this.attachedSessionId = loadedId;
|
|
498
|
+
this.state = "ready";
|
|
499
|
+
return {
|
|
500
|
+
id: loadedId,
|
|
501
|
+
provider: "grok",
|
|
502
|
+
modelId: this.modelId,
|
|
503
|
+
reasoningEffort: this.reasoningEffort,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
throw error instanceof GrokRuntimeError
|
|
508
|
+
? error
|
|
509
|
+
: new GrokRuntimeError("protocol_error", "Grok could not load that session.");
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
this.sessionLifecyclePhase = "idle";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async hydrateSession(id, modelId, reasoningEffort) {
|
|
516
|
+
const loaded = await this.loadSession(id);
|
|
517
|
+
if (modelId && (this.modelId !== modelId || (reasoningEffort !== undefined && this.reasoningEffort !== reasoningEffort))) {
|
|
518
|
+
const selection = await this.setModel(modelId, reasoningEffort);
|
|
519
|
+
return { id, provider: "grok", ...selection };
|
|
520
|
+
}
|
|
521
|
+
return loaded;
|
|
522
|
+
}
|
|
523
|
+
async listModels() {
|
|
524
|
+
if (this.modelCache)
|
|
525
|
+
return this.modelCache.map((model) => ({ ...model, reasoningLevels: [...model.reasoningLevels] }));
|
|
526
|
+
if (this.activeTurn)
|
|
527
|
+
throw new GrokRuntimeError("protocol_error", "Models cannot be refreshed during a Grok turn.");
|
|
528
|
+
await this.ensureNetworkRoute();
|
|
529
|
+
let temporaryLock;
|
|
530
|
+
try {
|
|
531
|
+
if (!this.lock)
|
|
532
|
+
temporaryLock = await this.acquireProfileLock();
|
|
533
|
+
const { profile, env, binary } = await this.prepareAndVerify();
|
|
534
|
+
const result = await runGrokCommand(this.spawn, binary.path, ["--no-auto-update", "--cwd", this.workspace, "models"], { cwd: this.workspace, env, maxStdoutBytes: 64 * 1024 });
|
|
535
|
+
if (result.code !== 0) {
|
|
536
|
+
throw new GrokRuntimeError("protocol_error", "Grok could not list subscription models.", result.stderr);
|
|
537
|
+
}
|
|
538
|
+
const defaultId = /^Default model:\s*(\S+)\s*$/m.exec(result.stdout)?.[1];
|
|
539
|
+
const ids = [...result.stdout.matchAll(/^\s*[*-]\s+(\S+)(?:\s+\(default\))?\s*$/gm)].map((match) => match[1]);
|
|
540
|
+
if (ids.length === 0)
|
|
541
|
+
throw new GrokRuntimeError("protocol_error", "Grok returned an empty model list.");
|
|
542
|
+
this.modelCache = [...new Set(ids)].map((id) => {
|
|
543
|
+
const reasoningLevels = id === "grok-4.5"
|
|
544
|
+
? ["low", "medium", "high"]
|
|
545
|
+
: ["off"];
|
|
546
|
+
return {
|
|
547
|
+
id,
|
|
548
|
+
name: id,
|
|
549
|
+
reasoningLevels,
|
|
550
|
+
defaultReasoningLevel: id === "grok-4.5" ? "high" : "off",
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
if (!this.modelId || !this.modelCache.some((model) => model.id === this.modelId)) {
|
|
554
|
+
this.modelId = this.modelCache.find((model) => model.id === defaultId)?.id ?? this.modelCache[0].id;
|
|
555
|
+
const selected = this.modelCache.find((model) => model.id === this.modelId);
|
|
556
|
+
this.reasoningEffort = selected.defaultReasoningLevel;
|
|
557
|
+
}
|
|
558
|
+
return this.modelCache.map((model) => ({ ...model, reasoningLevels: [...model.reasoningLevels] }));
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
await temporaryLock?.release();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
getModelSelection() {
|
|
565
|
+
return { modelId: this.modelId, reasoningEffort: this.reasoningEffort };
|
|
566
|
+
}
|
|
567
|
+
async setModel(modelId, reasoningEffort) {
|
|
568
|
+
const models = await this.listModels();
|
|
569
|
+
const model = models.find((candidate) => candidate.id === modelId);
|
|
570
|
+
if (!model)
|
|
571
|
+
throw new GrokRuntimeError("protocol_error", `Grok model \"${modelId}\" is unavailable.`);
|
|
572
|
+
const nextEffort = reasoningEffort ?? model.defaultReasoningLevel;
|
|
573
|
+
if (!model.reasoningLevels.includes(nextEffort)) {
|
|
574
|
+
throw new GrokRuntimeError("protocol_error", `Reasoning effort \"${nextEffort}\" is unavailable for ${modelId}.`);
|
|
575
|
+
}
|
|
576
|
+
if (this.modelId === modelId && this.reasoningEffort === nextEffort)
|
|
577
|
+
return this.getModelSelection();
|
|
578
|
+
const previous = this.getModelSelection();
|
|
579
|
+
const sessionId = this.currentSessionId;
|
|
580
|
+
if (!sessionId) {
|
|
581
|
+
throw new GrokRuntimeError("protocol_error", "Load the Grok session before switching its model.");
|
|
582
|
+
}
|
|
583
|
+
const client = await this.ensureConnected();
|
|
584
|
+
try {
|
|
585
|
+
const state = await client.setModel(sessionId, modelId);
|
|
586
|
+
this.applyAcpModels(state);
|
|
587
|
+
this.modelId = modelId;
|
|
588
|
+
this.reasoningEffort = nextEffort;
|
|
589
|
+
if (previous.reasoningEffort !== nextEffort) {
|
|
590
|
+
await this.disconnect();
|
|
591
|
+
await this.loadSession(sessionId);
|
|
592
|
+
this.modelId = modelId;
|
|
593
|
+
this.reasoningEffort = nextEffort;
|
|
594
|
+
}
|
|
595
|
+
return this.getModelSelection();
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
await this.disconnect().catch(() => undefined);
|
|
599
|
+
this.modelId = previous.modelId;
|
|
600
|
+
this.reasoningEffort = previous.reasoningEffort;
|
|
601
|
+
try {
|
|
602
|
+
const rollback = await this.loadSession(sessionId);
|
|
603
|
+
if (previous.modelId) {
|
|
604
|
+
const rollbackClient = await this.ensureConnected();
|
|
605
|
+
this.applyAcpModels(await rollbackClient.setModel(rollback.id, previous.modelId));
|
|
606
|
+
}
|
|
607
|
+
this.modelId = previous.modelId;
|
|
608
|
+
this.reasoningEffort = previous.reasoningEffort;
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
// Preserve the original switch failure; the next prompt will retry the
|
|
612
|
+
// persisted Bubble selection against the same external session.
|
|
613
|
+
}
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async ensureSession(requested) {
|
|
618
|
+
const desired = requested ?? this.currentSessionId;
|
|
619
|
+
if (!desired)
|
|
620
|
+
return (await this.newSession()).id;
|
|
621
|
+
if (this.attachedSessionId === desired)
|
|
622
|
+
return desired;
|
|
623
|
+
return (await this.loadSession(desired)).id;
|
|
624
|
+
}
|
|
625
|
+
async *run(prompt, options = {}) {
|
|
626
|
+
if (this.activeTurn)
|
|
627
|
+
throw new GrokRuntimeError("protocol_error", "A Grok turn is already running.");
|
|
628
|
+
const client = await this.ensureConnected();
|
|
629
|
+
const sessionId = await this.ensureSession(options.sessionId);
|
|
630
|
+
const generation = options.generation ?? ++this.nextGeneration;
|
|
631
|
+
const queue = new AsyncEventQueue();
|
|
632
|
+
const turn = { sessionId, generation, queue, cancelled: false };
|
|
633
|
+
this.activeTurn = turn;
|
|
634
|
+
this.activeTools.clear();
|
|
635
|
+
this.state = "running";
|
|
636
|
+
queue.push({ type: "turn_start" });
|
|
637
|
+
const abort = () => {
|
|
638
|
+
turn.cancelled = true;
|
|
639
|
+
turn.queue.dropPendingOutput();
|
|
640
|
+
void client.cancel(sessionId).catch(() => undefined);
|
|
641
|
+
};
|
|
642
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
643
|
+
if (options.signal?.aborted)
|
|
644
|
+
abort();
|
|
645
|
+
void client.prompt(sessionId, prompt, generation, options.signal).then(async (response) => {
|
|
646
|
+
if (this.activeTurn !== turn)
|
|
647
|
+
return;
|
|
648
|
+
this.activeTurn = undefined;
|
|
649
|
+
this.activeTools.clear();
|
|
650
|
+
try {
|
|
651
|
+
await this.closeSidecar();
|
|
652
|
+
if (turn.cancelled || response.stopReason === "cancelled") {
|
|
653
|
+
turn.cancelled = true;
|
|
654
|
+
queue.fail(new GrokRuntimeError("cancelled", "Grok turn cancelled."), true);
|
|
655
|
+
this.state = "ready";
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
queue.push({ type: "turn_end" });
|
|
659
|
+
queue.end();
|
|
660
|
+
this.state = "ready";
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
queue.fail(new GrokRuntimeError("protocol_error", "Grok sidecar cleanup failed."));
|
|
664
|
+
this.state = "failed";
|
|
665
|
+
}
|
|
666
|
+
}, async (promptError) => {
|
|
667
|
+
if (this.activeTurn !== turn)
|
|
668
|
+
return;
|
|
669
|
+
this.activeTurn = undefined;
|
|
670
|
+
this.activeTools.clear();
|
|
671
|
+
await this.closeSidecar().catch(() => undefined);
|
|
672
|
+
const error = turn.cancelled
|
|
673
|
+
? new GrokRuntimeError("cancelled", "Grok turn cancelled.")
|
|
674
|
+
: promptError instanceof GrokRuntimeError
|
|
675
|
+
? promptError
|
|
676
|
+
: new GrokRuntimeError("protocol_error", "Grok prompt failed.");
|
|
677
|
+
queue.fail(error, turn.cancelled);
|
|
678
|
+
this.state = turn.cancelled ? "ready" : "failed";
|
|
679
|
+
});
|
|
680
|
+
try {
|
|
681
|
+
while (true) {
|
|
682
|
+
const event = await queue.next();
|
|
683
|
+
if (event === END)
|
|
684
|
+
break;
|
|
685
|
+
yield event;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
options.signal?.removeEventListener("abort", abort);
|
|
690
|
+
if (this.activeTurn === turn) {
|
|
691
|
+
turn.cancelled = true;
|
|
692
|
+
turn.queue.dropPendingOutput();
|
|
693
|
+
this.activeTurn = undefined;
|
|
694
|
+
this.activeTools.clear();
|
|
695
|
+
this.state = "ready";
|
|
696
|
+
await client.cancel(sessionId).catch(() => undefined);
|
|
697
|
+
await this.closeSidecar();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async cancel(sessionId = this.activeTurn?.sessionId ?? this.currentSessionId) {
|
|
702
|
+
if (!sessionId || !this.acp)
|
|
703
|
+
return;
|
|
704
|
+
if (this.activeTurn?.sessionId === sessionId) {
|
|
705
|
+
this.activeTurn.cancelled = true;
|
|
706
|
+
this.activeTurn.queue.dropPendingOutput();
|
|
707
|
+
this.activeTools.clear();
|
|
708
|
+
}
|
|
709
|
+
await this.acp.cancel(sessionId);
|
|
710
|
+
}
|
|
711
|
+
async handlePolicyViolation(_sessionId, message, diagnostic) {
|
|
712
|
+
const turn = this.activeTurn;
|
|
713
|
+
const error = new GrokRuntimeError("policy_violation", message, diagnostic);
|
|
714
|
+
if (!turn) {
|
|
715
|
+
this.state = "failed";
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
turn.cancelled = true;
|
|
719
|
+
this.activeTurn = undefined;
|
|
720
|
+
this.state = "failed";
|
|
721
|
+
await this.acp?.cancel(turn.sessionId).catch(() => undefined);
|
|
722
|
+
await this.closeSidecar().catch(() => undefined);
|
|
723
|
+
turn.queue.fail(error, true);
|
|
724
|
+
}
|
|
725
|
+
async handleSessionUpdate(notification) {
|
|
726
|
+
const update = notification.update;
|
|
727
|
+
// Grok 0.2.93 advertises slash metadata while initializing/loading. It is
|
|
728
|
+
// inert in Bubble's constrained composer and is the sole update allowed
|
|
729
|
+
// to disappear when no prompt turn is active. Built-in commands carry no
|
|
730
|
+
// _meta; skill-backed commands always do (scope + SKILL.md path), so any
|
|
731
|
+
// annotated command means an extension escaped the disabled list — for
|
|
732
|
+
// example a skill created inside the workspace after the preflight ran.
|
|
733
|
+
if (update.sessionUpdate === "available_commands_update") {
|
|
734
|
+
const commands = update.availableCommands;
|
|
735
|
+
const advertised = Array.isArray(commands) ? commands : [];
|
|
736
|
+
for (const command of advertised) {
|
|
737
|
+
const meta = command && typeof command === "object" ? command._meta : undefined;
|
|
738
|
+
if (meta !== undefined && meta !== null) {
|
|
739
|
+
await this.handlePolicyViolation(notification.sessionId, "Grok loaded a workspace skill that Bubble did not disable.");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// session/load legitimately replays the stored transcript, including
|
|
746
|
+
// historical tool/plan records. These notifications are inert data: no
|
|
747
|
+
// Bubble turn exists, nothing is rendered or persisted, and any actual
|
|
748
|
+
// host capability request still takes the separate fail-closed path.
|
|
749
|
+
if (!this.activeTurn && this.sessionLifecyclePhase === "loading")
|
|
750
|
+
return;
|
|
751
|
+
const turn = this.activeTurn;
|
|
752
|
+
const safeTurnUpdate = update.sessionUpdate === "agent_message_chunk"
|
|
753
|
+
|| update.sessionUpdate === "agent_thought_chunk"
|
|
754
|
+
|| update.sessionUpdate === "user_message_chunk"
|
|
755
|
+
|| update.sessionUpdate === "usage_update"
|
|
756
|
+
|| update.sessionUpdate === "tool_call"
|
|
757
|
+
|| update.sessionUpdate === "tool_call_update";
|
|
758
|
+
if (!safeTurnUpdate) {
|
|
759
|
+
const message = "Grok emitted an unsupported ACP session update.";
|
|
760
|
+
const updateKind = /^[a-z_]{1,64}$/.test(update.sessionUpdate)
|
|
761
|
+
? `sessionUpdate=${update.sessionUpdate}`
|
|
762
|
+
: undefined;
|
|
763
|
+
await this.handlePolicyViolation(notification.sessionId, message, updateKind);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!turn) {
|
|
767
|
+
// ACP session/load may replay transcript and usage chunks. They are not
|
|
768
|
+
// part of a Bubble prompt generation, so consume them without exposing
|
|
769
|
+
// or persisting them. Dangerous/unknown updates were rejected above.
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (turn.cancelled || notification.sessionId !== turn.sessionId)
|
|
773
|
+
return;
|
|
774
|
+
const generation = notificationGeneration(notification._meta);
|
|
775
|
+
if (generation !== undefined && generation !== turn.generation)
|
|
776
|
+
return;
|
|
777
|
+
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
|
|
778
|
+
turn.queue.push({ type: "text_delta", content: update.content.text });
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (update.sessionUpdate === "agent_thought_chunk" && update.content.type === "text") {
|
|
782
|
+
turn.queue.push({ type: "reasoning_delta", content: update.content.text });
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (update.sessionUpdate === "user_message_chunk" && update.content.type === "text") {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (update.sessionUpdate === "usage_update") {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
|
|
792
|
+
const existing = this.activeTools.get(update.toolCallId);
|
|
793
|
+
const name = update.title?.trim() || existing?.name || update.kind || "Grok tool";
|
|
794
|
+
const args = update.rawInput !== undefined ? externalToolArgs(update.rawInput) : existing?.args ?? {};
|
|
795
|
+
if (!existing) {
|
|
796
|
+
this.activeTools.set(update.toolCallId, { name, args });
|
|
797
|
+
turn.queue.push({ type: "tool_start", id: update.toolCallId, name, args });
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
this.activeTools.set(update.toolCallId, { name, args });
|
|
801
|
+
}
|
|
802
|
+
if (update.status === "completed" || update.status === "failed") {
|
|
803
|
+
turn.queue.push({
|
|
804
|
+
type: "tool_end",
|
|
805
|
+
id: update.toolCallId,
|
|
806
|
+
name,
|
|
807
|
+
result: {
|
|
808
|
+
content: externalToolOutput(update.rawOutput ?? update.content),
|
|
809
|
+
isError: update.status === "failed",
|
|
810
|
+
status: update.status === "failed" ? "command_error" : "success",
|
|
811
|
+
metadata: { kind: "internal", externalRuntime: "grok" },
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
this.activeTools.delete(update.toolCallId);
|
|
815
|
+
}
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
handleCrash(error) {
|
|
820
|
+
this.state = "failed";
|
|
821
|
+
this.activeTurn?.queue.fail(error);
|
|
822
|
+
this.activeTurn = undefined;
|
|
823
|
+
this.activeTools.clear();
|
|
824
|
+
void this.closeSidecar().catch(() => undefined);
|
|
825
|
+
}
|
|
826
|
+
async closeSidecar() {
|
|
827
|
+
if (this.closingSidecar)
|
|
828
|
+
return await this.closingSidecar;
|
|
829
|
+
const client = this.acp;
|
|
830
|
+
const lock = this.lock;
|
|
831
|
+
this.acp = undefined;
|
|
832
|
+
this.lock = undefined;
|
|
833
|
+
this.attachedSessionId = undefined;
|
|
834
|
+
const closing = (async () => {
|
|
835
|
+
try {
|
|
836
|
+
await client?.dispose();
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
try {
|
|
840
|
+
if (client)
|
|
841
|
+
await clearGrokGeneratedExtensions(this.profile);
|
|
842
|
+
}
|
|
843
|
+
finally {
|
|
844
|
+
await lock?.release();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
})();
|
|
848
|
+
this.closingSidecar = closing;
|
|
849
|
+
try {
|
|
850
|
+
await closing;
|
|
851
|
+
}
|
|
852
|
+
finally {
|
|
853
|
+
if (this.closingSidecar === closing)
|
|
854
|
+
this.closingSidecar = undefined;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async disconnect() {
|
|
858
|
+
if (this.activeTurn) {
|
|
859
|
+
this.activeTurn.cancelled = true;
|
|
860
|
+
this.activeTurn.queue.fail(new GrokRuntimeError("cancelled", "Grok runtime disconnected."), true);
|
|
861
|
+
this.activeTurn = undefined;
|
|
862
|
+
}
|
|
863
|
+
this.activeTools.clear();
|
|
864
|
+
await this.closeSidecar();
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Closes the current sidecar and releases the profile lock. The manager is
|
|
868
|
+
* intentionally reusable: a later new/load/run call starts a fresh sidecar.
|
|
869
|
+
*/
|
|
870
|
+
async dispose() {
|
|
871
|
+
const login = this.loginOperation;
|
|
872
|
+
if (login) {
|
|
873
|
+
this.loginController?.abort(new GrokRuntimeError("cancelled", "Grok login cancelled during shutdown."));
|
|
874
|
+
await login.catch(() => undefined);
|
|
875
|
+
}
|
|
876
|
+
await this.disconnect();
|
|
877
|
+
this.state = "disposed";
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
export function createGrokRuntimeManager(deps = {}) {
|
|
881
|
+
return new GrokRuntimeManager(deps);
|
|
882
|
+
}
|