@addai/node 0.4.0

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 (133) hide show
  1. package/README.md +96 -0
  2. package/dist/agent-auth.d.ts +9 -0
  3. package/dist/agent-auth.js +96 -0
  4. package/dist/attachments.d.ts +20 -0
  5. package/dist/attachments.js +164 -0
  6. package/dist/capabilities.d.ts +64 -0
  7. package/dist/capabilities.js +406 -0
  8. package/dist/claude-binary.d.ts +16 -0
  9. package/dist/claude-binary.js +145 -0
  10. package/dist/claude-config.d.ts +11 -0
  11. package/dist/claude-config.js +144 -0
  12. package/dist/claude-print.d.ts +35 -0
  13. package/dist/claude-print.js +312 -0
  14. package/dist/claude-spawn.d.ts +40 -0
  15. package/dist/claude-spawn.js +220 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +137 -0
  18. package/dist/codex-binary.d.ts +4 -0
  19. package/dist/codex-binary.js +70 -0
  20. package/dist/codex-spawn.d.ts +47 -0
  21. package/dist/codex-spawn.js +320 -0
  22. package/dist/command-runner.d.ts +9 -0
  23. package/dist/command-runner.js +592 -0
  24. package/dist/config.d.ts +7 -0
  25. package/dist/config.js +38 -0
  26. package/dist/control-server.d.ts +5 -0
  27. package/dist/control-server.js +292 -0
  28. package/dist/diskguard.d.ts +96 -0
  29. package/dist/diskguard.js +435 -0
  30. package/dist/error-classify.d.ts +20 -0
  31. package/dist/error-classify.js +159 -0
  32. package/dist/events.d.ts +26 -0
  33. package/dist/events.js +97 -0
  34. package/dist/gemini-binary.d.ts +4 -0
  35. package/dist/gemini-binary.js +64 -0
  36. package/dist/gemini-spawn.d.ts +52 -0
  37. package/dist/gemini-spawn.js +369 -0
  38. package/dist/git-identity.d.ts +18 -0
  39. package/dist/git-identity.js +104 -0
  40. package/dist/grok-binary.d.ts +4 -0
  41. package/dist/grok-binary.js +70 -0
  42. package/dist/grok-spawn.d.ts +41 -0
  43. package/dist/grok-spawn.js +495 -0
  44. package/dist/guardrails.d.ts +32 -0
  45. package/dist/guardrails.js +125 -0
  46. package/dist/hang-watchdog.d.ts +17 -0
  47. package/dist/hang-watchdog.js +48 -0
  48. package/dist/harness-registry.d.ts +55 -0
  49. package/dist/harness-registry.js +180 -0
  50. package/dist/heartbeat.d.ts +13 -0
  51. package/dist/heartbeat.js +95 -0
  52. package/dist/index.d.ts +12 -0
  53. package/dist/index.js +440 -0
  54. package/dist/install.d.ts +63 -0
  55. package/dist/install.js +467 -0
  56. package/dist/jsonl-tail.d.ts +20 -0
  57. package/dist/jsonl-tail.js +192 -0
  58. package/dist/kimi-binary.d.ts +4 -0
  59. package/dist/kimi-binary.js +68 -0
  60. package/dist/kimi-spawn.d.ts +47 -0
  61. package/dist/kimi-spawn.js +319 -0
  62. package/dist/lockfile.d.ts +30 -0
  63. package/dist/lockfile.js +169 -0
  64. package/dist/mcp-config.d.ts +23 -0
  65. package/dist/mcp-config.js +133 -0
  66. package/dist/memory-capture.d.ts +20 -0
  67. package/dist/memory-capture.js +45 -0
  68. package/dist/memory-pack.d.ts +34 -0
  69. package/dist/memory-pack.js +171 -0
  70. package/dist/model-filter.d.ts +4 -0
  71. package/dist/model-filter.js +34 -0
  72. package/dist/node-health.d.ts +18 -0
  73. package/dist/node-health.js +74 -0
  74. package/dist/pairing.d.ts +20 -0
  75. package/dist/pairing.js +129 -0
  76. package/dist/paths.d.ts +9 -0
  77. package/dist/paths.js +48 -0
  78. package/dist/precompact-hook.d.ts +27 -0
  79. package/dist/precompact-hook.js +136 -0
  80. package/dist/projects.d.ts +25 -0
  81. package/dist/projects.js +255 -0
  82. package/dist/prompt-keystrokes.d.ts +3 -0
  83. package/dist/prompt-keystrokes.js +56 -0
  84. package/dist/pty-helper.d.ts +26 -0
  85. package/dist/pty-helper.js +156 -0
  86. package/dist/request-pump.d.ts +12 -0
  87. package/dist/request-pump.js +249 -0
  88. package/dist/run-retry.d.ts +28 -0
  89. package/dist/run-retry.js +81 -0
  90. package/dist/self-update.d.ts +75 -0
  91. package/dist/self-update.js +192 -0
  92. package/dist/session-runner.d.ts +130 -0
  93. package/dist/session-runner.js +2271 -0
  94. package/dist/sleep-detector.d.ts +11 -0
  95. package/dist/sleep-detector.js +32 -0
  96. package/dist/state-migrate.d.ts +8 -0
  97. package/dist/state-migrate.js +91 -0
  98. package/dist/store.d.ts +5 -0
  99. package/dist/store.js +82 -0
  100. package/dist/supabase-client.d.ts +15 -0
  101. package/dist/supabase-client.js +119 -0
  102. package/dist/time-context.d.ts +10 -0
  103. package/dist/time-context.js +71 -0
  104. package/dist/tokens.d.ts +4 -0
  105. package/dist/tokens.js +33 -0
  106. package/dist/tui/app.d.ts +35 -0
  107. package/dist/tui/app.js +82 -0
  108. package/dist/tui/console-capture.d.ts +13 -0
  109. package/dist/tui/console-capture.js +62 -0
  110. package/dist/tui/dashboard.d.ts +42 -0
  111. package/dist/tui/dashboard.js +205 -0
  112. package/dist/tui/data.d.ts +57 -0
  113. package/dist/tui/data.js +62 -0
  114. package/dist/tui/harnesses.d.ts +16 -0
  115. package/dist/tui/harnesses.js +354 -0
  116. package/dist/tui/render.d.ts +21 -0
  117. package/dist/tui/render.js +116 -0
  118. package/dist/tui/requests.d.ts +18 -0
  119. package/dist/tui/requests.js +127 -0
  120. package/dist/tui/run.d.ts +13 -0
  121. package/dist/tui/run.js +178 -0
  122. package/dist/tui/transcript.d.ts +39 -0
  123. package/dist/tui/transcript.js +234 -0
  124. package/dist/tui.d.ts +1 -0
  125. package/dist/tui.js +314 -0
  126. package/dist/types.d.ts +65 -0
  127. package/dist/types.js +3 -0
  128. package/dist/win.d.ts +67 -0
  129. package/dist/win.js +317 -0
  130. package/package.json +60 -0
  131. package/scripts/fix-pty-helper.js +28 -0
  132. package/scripts/precompact-capture.js +292 -0
  133. package/scripts/smoke-test.sh +74 -0
@@ -0,0 +1,495 @@
1
+ "use strict";
2
+ // grok -p --output-format streaming-json adapter.
3
+ //
4
+ // Mirrors claude-print.ts / kimi-spawn.ts: spawns `grok` non-interactively
5
+ // with a JSONL output stream, parses each line into our normalised
6
+ // RuntimeEvent shape, and resolves on exit.
7
+ //
8
+ // Two things make grok different from the other agents and drive the
9
+ // design here:
10
+ //
11
+ // 1. No `--mcp-config` flag. Grok discovers MCP servers from
12
+ // `config.toml` (user + project scope), `~/.claude.json`, and
13
+ // `.mcp.json` — the Claude-Code compatibility layer. To give an
14
+ // entity ONLY its own servers we (a) write the entity's servers to a
15
+ // per-session project `<cwd>/.grok/config.toml`, and (b) run grok
16
+ // under a sandbox $HOME whose `.claude.json` is empty, so the host's
17
+ // global Claude MCPs don't leak in. The sandbox HOME symlinks every
18
+ // other real-home entry so git / npm / ssh still find their configs
19
+ // (the lesson from kimi: never point HOME at an empty dir).
20
+ //
21
+ // 2. The account lives in `~/.grok/auth.json` and its OAuth token is
22
+ // refreshed in place. So GROK_HOME stays pointed at the REAL
23
+ // `~/.grok` — the entity uses the same logged-in account the user
24
+ // set up, and token refresh keeps working — while only $HOME is
25
+ // sandboxed.
26
+ //
27
+ // grok's streaming-json emits `thought` (reasoning), `text` (assistant
28
+ // output) and a final `end` (usage + sessionId). It does not surface
29
+ // per-tool events, so unlike claude we can't render individual tool
30
+ // bubbles; unknown types are forwarded as `grok:<type>` for forward-compat.
31
+ //
32
+ // Reference: grok --help, ~/.grok/README.md (Claude Code Compatibility).
33
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ var desc = Object.getOwnPropertyDescriptor(m, k);
36
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
37
+ desc = { enumerable: true, get: function() { return m[k]; } };
38
+ }
39
+ Object.defineProperty(o, k2, desc);
40
+ }) : (function(o, m, k, k2) {
41
+ if (k2 === undefined) k2 = k;
42
+ o[k2] = m[k];
43
+ }));
44
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
45
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
46
+ }) : function(o, v) {
47
+ o["default"] = v;
48
+ });
49
+ var __importStar = (this && this.__importStar) || (function () {
50
+ var ownKeys = function(o) {
51
+ ownKeys = Object.getOwnPropertyNames || function (o) {
52
+ var ar = [];
53
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
54
+ return ar;
55
+ };
56
+ return ownKeys(o);
57
+ };
58
+ return function (mod) {
59
+ if (mod && mod.__esModule) return mod;
60
+ var result = {};
61
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
62
+ __setModuleDefault(result, mod);
63
+ return result;
64
+ };
65
+ })();
66
+ Object.defineProperty(exports, "__esModule", { value: true });
67
+ exports.spawnGrok = spawnGrok;
68
+ const child_process_1 = require("child_process");
69
+ const fs = __importStar(require("fs"));
70
+ const os = __importStar(require("os"));
71
+ const path = __importStar(require("path"));
72
+ const grok_binary_1 = require("./grok-binary");
73
+ const win_1 = require("./win");
74
+ const events_1 = require("./events");
75
+ /** Real config dir for the logged-in account. GROK_HOME points here so the
76
+ * entity uses the same account and token refresh works. */
77
+ function realGrokHome() {
78
+ return path.join(os.homedir(), '.grok');
79
+ }
80
+ // Home entries that are MCP / harness discovery sources for grok's
81
+ // Claude-Code compatibility layer. Masked in the sandbox so an entity only
82
+ // sees its own servers, never the host's global Claude/Cursor MCPs.
83
+ const MASKED_HOME_ENTRIES = new Set(['.claude.json', '.claude', '.mcp.json', '.cursor']);
84
+ /** Build a sandbox $HOME: bridge every real-home entry (so git/npm/ssh
85
+ * keep working) EXCEPT the Claude/Cursor MCP sources, then drop an empty
86
+ * `.claude.json` so host MCPs resolve to nothing. Caller cleans it up.
87
+ *
88
+ * POSIX: plain symlinks. win32: directory junctions (work unprivileged)
89
+ * and symlink-or-copy for files (file symlinks need admin/Dev Mode). */
90
+ function writeGrokSandboxHome() {
91
+ const home = os.homedir();
92
+ const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-grok-home-'));
93
+ let entries = [];
94
+ try {
95
+ entries = fs.readdirSync(home);
96
+ }
97
+ catch { /* best-effort */ }
98
+ for (const name of entries) {
99
+ if (MASKED_HOME_ENTRIES.has(name))
100
+ continue;
101
+ const real = path.join(home, name);
102
+ const dest = path.join(sandbox, name);
103
+ if (!win_1.IS_WINDOWS) {
104
+ try {
105
+ fs.symlinkSync(real, dest);
106
+ }
107
+ catch { /* skip */ }
108
+ continue;
109
+ }
110
+ try {
111
+ if (fs.statSync(real).isDirectory())
112
+ (0, win_1.linkDir)(real, dest);
113
+ else
114
+ (0, win_1.linkOrCopyFile)(real, dest);
115
+ }
116
+ catch { /* unreadable entry — skip */ }
117
+ }
118
+ // Empty Claude config → no host MCPs leak through the compat layer.
119
+ try {
120
+ fs.writeFileSync(path.join(sandbox, '.claude.json'), JSON.stringify({ mcpServers: {} }), { mode: 0o600 });
121
+ }
122
+ catch { /* non-fatal */ }
123
+ return sandbox;
124
+ }
125
+ /** Env overrides pointing the child's "home" at the sandbox. POSIX tools
126
+ * read HOME; on Windows the profile is USERPROFILE (+ HOMEDRIVE/HOMEPATH),
127
+ * and HOME alone would change nothing. */
128
+ function sandboxHomeEnv(sandboxHome) {
129
+ const env = { HOME: sandboxHome };
130
+ if (win_1.IS_WINDOWS) {
131
+ env.USERPROFILE = sandboxHome;
132
+ const parsed = path.parse(sandboxHome);
133
+ if (parsed.root) {
134
+ env.HOMEDRIVE = parsed.root.replace(/[\\/]+$/, '');
135
+ env.HOMEPATH = sandboxHome.slice(env.HOMEDRIVE.length) || '\\';
136
+ }
137
+ }
138
+ return env;
139
+ }
140
+ /** Escape a string for a TOML double-quoted value. */
141
+ function tomlStr(s) {
142
+ return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
143
+ }
144
+ /** Write the entity's MCP servers as a project-scoped `<cwd>/.grok/config.toml`.
145
+ * Project scope only supports `[mcp_servers]`, which is exactly what we need.
146
+ * Returns the config path (for logging) or null when there are no servers. */
147
+ function writeProjectMcpConfig(workingDirectory, servers) {
148
+ const usable = servers.filter(s => s.slug && s.command);
149
+ if (usable.length === 0) {
150
+ // No usable servers: remove any stale config.toml from a prior turn.
151
+ // Returning null WITHOUT deleting leaves grok loading the old servers
152
+ // (project-scoped config persists in the cwd across turns).
153
+ try {
154
+ fs.rmSync(path.join(workingDirectory, '.grok', 'config.toml'), { force: true });
155
+ }
156
+ catch { /* nothing to remove */ }
157
+ return null;
158
+ }
159
+ const lines = [];
160
+ for (const s of usable) {
161
+ // `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
162
+ const wrapped = (0, win_1.wrapMcpCommandForPlatform)(s.command, Array.isArray(s.args) ? s.args.map(String) : []);
163
+ lines.push(`[mcp_servers.${tomlStr(s.slug)}]`);
164
+ lines.push(`command = ${tomlStr(wrapped.command)}`);
165
+ if (wrapped.args.length > 0) {
166
+ lines.push(`args = [${wrapped.args.map(a => tomlStr(a)).join(', ')}]`);
167
+ }
168
+ if (s.env && Object.keys(s.env).length > 0) {
169
+ const kv = Object.entries(s.env)
170
+ .filter(([, v]) => typeof v === 'string' && v.length > 0)
171
+ .map(([k, v]) => `${/^[A-Za-z0-9_]+$/.test(k) ? k : tomlStr(k)} = ${tomlStr(String(v))}`)
172
+ .join(', ');
173
+ lines.push(`env = { ${kv} }`);
174
+ }
175
+ lines.push('enabled = true');
176
+ // Cap MCP startup so a slow/broken server fails the tool instead of
177
+ // hanging the whole agent (the "thinking forever" class of bug), but
178
+ // allow enough for a cold `npx` fetch of the server package.
179
+ lines.push('startup_timeout_sec = 30');
180
+ lines.push('');
181
+ }
182
+ const dir = path.join(workingDirectory, '.grok');
183
+ try {
184
+ fs.mkdirSync(dir, { recursive: true });
185
+ const target = path.join(dir, 'config.toml');
186
+ fs.writeFileSync(target, lines.join('\n'), { mode: 0o600 });
187
+ return target;
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ /** grok issues its own session ids as UUIDv7 (time-ordered, version nibble
194
+ * '7', e.g. 019f60ef-6aa4-7fd2-...). A conversation's stored session id may
195
+ * come from a DIFFERENT agent (claude etc. use UUIDv4) or the chat framework.
196
+ * Passing such an id to `grok --resume` makes grok try to restore it from its
197
+ * remote registry, get a 404, and exit 1 — which is why grok failed on
198
+ * chatflows continuation turns. Only resume when the id is plausibly a grok
199
+ * session; otherwise start fresh (grok couldn't resume a foreign session
200
+ * anyway). Degrades safely: an unrecognised id just means a new session. */
201
+ function isGrokSessionId(id) {
202
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
203
+ }
204
+ /** Map our effort levels to grok's `--reasoning-effort` accepted values. */
205
+ function grokEffort(level) {
206
+ switch (level) {
207
+ case 'low': return 'low';
208
+ case 'medium': return 'medium';
209
+ case 'high':
210
+ case 'xhigh':
211
+ case 'max': return 'high';
212
+ default: return null;
213
+ }
214
+ }
215
+ /** Parse one streaming-json line into 0+ RuntimeEvents. grok emits
216
+ * `thought` (reasoning tokens), `text` (assistant output deltas), a final
217
+ * `end` (usage + sessionId), and `error`. Tool calls happen internally and
218
+ * are not surfaced as discrete events. Anything unrecognised is forwarded
219
+ * as `grok:<type>`. */
220
+ function lineToEvents(line) {
221
+ const type = typeof line.type === 'string' ? line.type : '';
222
+ const data = typeof line.data === 'string' ? line.data : '';
223
+ // Assistant output deltas.
224
+ if (type === 'text') {
225
+ return data ? [{ type: 'assistant_text', delta: data }] : [];
226
+ }
227
+ // Reasoning tokens — normalized 'thinking' deltas (coalesced below like
228
+ // text). Kept out of the visible answer; the chat relay aggregates them
229
+ // into a collapsible "thinking" attachment. The old per-token
230
+ // `grok:thought` passthrough flooded the events table (~1 row/token).
231
+ if (type === 'thought') {
232
+ return data ? [{ type: 'thinking', delta: data }] : [];
233
+ }
234
+ // Forward-compat: if a future grok build surfaces tool events, map them.
235
+ if (type === 'tool_use' || type === 'tool_call') {
236
+ return [{
237
+ type: 'tool_use',
238
+ id: String(line.id ?? line.toolCallId ?? ''),
239
+ name: String(line.name ?? line.tool ?? ''),
240
+ input: line.input ?? line.args ?? line.arguments,
241
+ }];
242
+ }
243
+ if (type === 'tool_result') {
244
+ return [{
245
+ type: 'tool_result',
246
+ id: String(line.id ?? line.toolCallId ?? ''),
247
+ content: line.content ?? line.result ?? line.output,
248
+ isError: line.isError === true || line.is_error === true,
249
+ }];
250
+ }
251
+ // Turn / session end.
252
+ if (type === 'end' || type === 'result' || type === 'turn_complete') {
253
+ return [{
254
+ type: 'turn_complete',
255
+ usage: line.usage ?? undefined,
256
+ stopReason: typeof line.stopReason === 'string'
257
+ ? line.stopReason
258
+ : (typeof line.stop_reason === 'string' ? line.stop_reason : undefined),
259
+ }];
260
+ }
261
+ // Errors.
262
+ if (type === 'error' || type === 'turn_failed') {
263
+ const message = line.error?.message
264
+ ?? (typeof line.message === 'string' ? line.message : 'grok error');
265
+ return [
266
+ { type: 'error', code: 'grok_error', message },
267
+ { type: 'turn_complete', stopReason: 'failed' },
268
+ ];
269
+ }
270
+ return [{ type: `grok:${type || 'unknown'}`, raw: line }];
271
+ }
272
+ function spawnGrok(input) {
273
+ const bin = (0, grok_binary_1.findGrokBinary)();
274
+ if (!bin) {
275
+ // Surface a "grok not installed" error via the event stream so the
276
+ // chat fails cleanly instead of hanging. bufferedEvents delivers even
277
+ // though the runner attaches its listener after an await.
278
+ const events = (0, events_1.bufferedEvents)([
279
+ { type: 'error', code: 'grok_not_installed', message: 'grok CLI is not installed on this runtime. Install it from https://grok.com and run `grok login`.' },
280
+ { type: 'turn_complete', stopReason: 'failed' },
281
+ ]);
282
+ return {
283
+ pid: undefined,
284
+ sessionId: null,
285
+ kill: () => { },
286
+ onEvent: events.onEvent,
287
+ onActivity: () => { },
288
+ done: Promise.resolve(127),
289
+ };
290
+ }
291
+ const sandboxHome = writeGrokSandboxHome();
292
+ writeProjectMcpConfig(input.workingDirectory, input.mcpServers ?? []);
293
+ const args = [
294
+ '-p', input.prompt,
295
+ '--output-format', 'streaming-json',
296
+ '--cwd', input.workingDirectory,
297
+ // Autonomous entity: no human to answer approvals in headless mode.
298
+ '--always-approve',
299
+ // Trust the working folder so its repo-local MCP servers (the entity's
300
+ // <cwd>/.grok/config.toml) actually START. Without this, grok's
301
+ // folder-trust gate silently skips repo-local MCP servers in a headless
302
+ // untrusted dir, and the agent can't call its tools.
303
+ '--trust',
304
+ ];
305
+ // Only resume genuine grok sessions — a foreign (e.g. claude UUIDv4) id
306
+ // would make grok 404 on remote restore and exit 1.
307
+ if (input.resumeSessionId && isGrokSessionId(input.resumeSessionId)) {
308
+ args.push('--resume', input.resumeSessionId);
309
+ }
310
+ if (input.model) {
311
+ args.push('--model', input.model);
312
+ }
313
+ const effort = grokEffort(input.effortLevel);
314
+ if (effort) {
315
+ args.push('--reasoning-effort', effort);
316
+ }
317
+ // grok appends `--rules` on top of its base system prompt — same
318
+ // semantics as claude `--append-system-prompt`.
319
+ if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
320
+ args.push('--rules', input.appendSystemPrompt.trim());
321
+ }
322
+ const childEnv = {
323
+ ...process.env,
324
+ ...sandboxHomeEnv(sandboxHome), // masks host ~/.claude.json MCPs (HOME; +USERPROFILE on win32)
325
+ GROK_HOME: realGrokHome(), // real account + live token refresh
326
+ TERM: 'dumb',
327
+ };
328
+ let proc;
329
+ try {
330
+ // npm .cmd shim → node script (win32); identity on POSIX. Both this
331
+ // resolution and spawn() itself can throw synchronously on Windows —
332
+ // fail the request cleanly instead of crashing the daemon.
333
+ const inv = (0, win_1.resolveCliInvocation)(bin, args);
334
+ proc = (0, child_process_1.spawn)(inv.file, inv.args, {
335
+ cwd: input.workingDirectory,
336
+ env: childEnv,
337
+ stdio: ['ignore', 'pipe', 'pipe'],
338
+ });
339
+ }
340
+ catch (err) {
341
+ const msg = err.message || String(err);
342
+ try {
343
+ fs.rmSync(sandboxHome, { recursive: true, force: true });
344
+ }
345
+ catch { }
346
+ const events = (0, events_1.bufferedEvents)([
347
+ { type: 'error', code: 'grok_spawn_failed', message: msg },
348
+ { type: 'turn_complete', stopReason: 'failed' },
349
+ ]);
350
+ return {
351
+ pid: undefined,
352
+ sessionId: null,
353
+ kill: () => { },
354
+ onEvent: events.onEvent,
355
+ onActivity: () => { },
356
+ done: Promise.resolve(-1),
357
+ };
358
+ }
359
+ const listeners = [];
360
+ const emit = (e) => { for (const l of listeners)
361
+ try {
362
+ l(e);
363
+ }
364
+ catch { } };
365
+ let sessionId = null;
366
+ let buffer = '';
367
+ // Coalesce assistant text. grok streams one `text` event PER TOKEN — a
368
+ // short reply is 30-40 events. Emitting each one as its own row floods
369
+ // the event table AND, because the daemon inserts events fire-and-forget,
370
+ // rapid per-token rows race and land out of order (the reply renders
371
+ // scrambled). Buffering deltas into a handful of well-spaced chunks fixes
372
+ // both: fewer rows, and enough spacing that inserts don't overlap.
373
+ const FLUSH_MS = 90;
374
+ const MAX_BUF = 320;
375
+ // Thinking flushes lazily (bigger chunks, longer window) — it's collapsed
376
+ // in the UI, so latency doesn't matter; fewer rows do.
377
+ const THINK_FLUSH_MS = 700;
378
+ const THINK_MAX_BUF = 1500;
379
+ let textBuf = '';
380
+ let thinkBuf = '';
381
+ let flushTimer = null;
382
+ let thinkTimer = null;
383
+ const flushThinking = () => {
384
+ if (thinkTimer) {
385
+ clearTimeout(thinkTimer);
386
+ thinkTimer = null;
387
+ }
388
+ if (thinkBuf) {
389
+ const delta = thinkBuf;
390
+ thinkBuf = '';
391
+ emit({ type: 'thinking', delta });
392
+ }
393
+ };
394
+ const flushText = () => {
395
+ if (flushTimer) {
396
+ clearTimeout(flushTimer);
397
+ flushTimer = null;
398
+ }
399
+ if (textBuf) {
400
+ const delta = textBuf;
401
+ textBuf = '';
402
+ emit({ type: 'assistant_text', delta });
403
+ }
404
+ };
405
+ const emitCoalesced = (e) => {
406
+ if (e.type === 'assistant_text') {
407
+ // Thoughts precede the text they produced — flush them first.
408
+ flushThinking();
409
+ textBuf += e.delta;
410
+ if (textBuf.length >= MAX_BUF)
411
+ flushText();
412
+ else if (!flushTimer)
413
+ flushTimer = setTimeout(flushText, FLUSH_MS);
414
+ }
415
+ else if (e.type === 'thinking') {
416
+ thinkBuf += e.delta;
417
+ if (thinkBuf.length >= THINK_MAX_BUF)
418
+ flushThinking();
419
+ else if (!thinkTimer)
420
+ thinkTimer = setTimeout(flushThinking, THINK_FLUSH_MS);
421
+ }
422
+ else {
423
+ // Any other event must appear AFTER the stream so far — flush both.
424
+ flushThinking();
425
+ flushText();
426
+ emit(e);
427
+ }
428
+ };
429
+ const activityListeners = [];
430
+ const touchActivity = () => { for (const l of activityListeners)
431
+ try {
432
+ l();
433
+ }
434
+ catch { } };
435
+ proc.stdout?.on('data', (chunk) => {
436
+ touchActivity();
437
+ buffer += chunk.toString('utf8');
438
+ let nl;
439
+ while ((nl = buffer.indexOf('\n')) !== -1) {
440
+ const raw = buffer.slice(0, nl).trim();
441
+ buffer = buffer.slice(nl + 1);
442
+ if (!raw)
443
+ continue;
444
+ let parsed;
445
+ try {
446
+ parsed = JSON.parse(raw);
447
+ }
448
+ catch {
449
+ continue;
450
+ }
451
+ if (!sessionId) {
452
+ const sid = parsed.sessionId ?? parsed.session_id;
453
+ if (typeof sid === 'string')
454
+ sessionId = sid;
455
+ }
456
+ for (const ev of lineToEvents(parsed))
457
+ emitCoalesced(ev);
458
+ }
459
+ });
460
+ proc.stderr?.on('data', (chunk) => {
461
+ touchActivity();
462
+ const txt = chunk.toString('utf8').trim();
463
+ if (txt)
464
+ emit({ type: 'error', code: 'grok_stderr', message: txt.slice(0, 4096) });
465
+ });
466
+ const done = new Promise((resolve, reject) => {
467
+ proc.once('error', (err) => {
468
+ // Async spawn failure (ENOENT/EACCES from a stale cached binary path):
469
+ // the exit handler never fires, so clean up the sandbox HOME here too —
470
+ // otherwise it leaks (on Windows we copy files up to 8MB into it).
471
+ try {
472
+ fs.rmSync(sandboxHome, { recursive: true, force: true });
473
+ }
474
+ catch { }
475
+ reject(err);
476
+ });
477
+ proc.once('exit', (code) => {
478
+ flushThinking(); // emit buffered tails before we resolve
479
+ flushText();
480
+ try {
481
+ fs.rmSync(sandboxHome, { recursive: true, force: true });
482
+ }
483
+ catch { }
484
+ resolve(code ?? -1);
485
+ });
486
+ });
487
+ return {
488
+ pid: proc.pid,
489
+ get sessionId() { return sessionId; },
490
+ kill: () => { (0, win_1.killProcessTree)(proc); },
491
+ onEvent: (cb) => { listeners.push(cb); },
492
+ onActivity: (cb) => { activityListeners.push(cb); },
493
+ done,
494
+ };
495
+ }
@@ -0,0 +1,32 @@
1
+ export interface GuardrailDef {
2
+ id: string;
3
+ name: string;
4
+ rule: string;
5
+ mode: 'fast' | 'runtime';
6
+ action: 'retry' | 'block' | 'flag';
7
+ max_retries: number;
8
+ }
9
+ export interface GuardrailVerdict {
10
+ id: string;
11
+ verdict: 'pass' | 'fail';
12
+ reasoning: string;
13
+ checker_error?: boolean;
14
+ }
15
+ export interface GuardrailIo {
16
+ loadGuardrails(): Promise<GuardrailDef[]>;
17
+ fetchOutput(): Promise<string | null>;
18
+ checkFast(output: string, defs: GuardrailDef[]): Promise<GuardrailVerdict[]>;
19
+ checkRuntime(output: string, def: GuardrailDef): Promise<GuardrailVerdict>;
20
+ /** Continuation turn on the same session. null = retry unavailable. */
21
+ runRetry(feedback: string): Promise<{
22
+ ok: boolean;
23
+ text: string;
24
+ } | null>;
25
+ setResult(meta: Record<string, unknown>, events: Array<Record<string, unknown>>): Promise<void>;
26
+ emit(type: string, payload: unknown): Promise<void>;
27
+ }
28
+ export declare function buildRetryFeedback(failures: Array<{
29
+ def: GuardrailDef;
30
+ verdict: GuardrailVerdict;
31
+ }>): string;
32
+ export declare function enforceGuardrails(io: GuardrailIo): Promise<void>;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ // Guardrails — post-request checks an entity's output passes through before
3
+ // the daemon writes the terminal `completed` status. Holding the terminal
4
+ // status is the whole enforcement mechanism: every delivery path (bot-egress,
5
+ // flows resume, chatflows queue promotion) triggers on terminal status, so
6
+ // nothing is delivered until enforceGuardrails resolves.
7
+ //
8
+ // The state machine is pure over an injected IO surface (GuardrailIo) so it
9
+ // is unit-testable without a network; session-runner.ts supplies the real IO
10
+ // (RPCs + edge fn + retry spawn). Contract: enforceGuardrails NEVER throws
11
+ // and fails OPEN — a broken checker degrades to `flag`, never to a blocked
12
+ // or wedged reply.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.buildRetryFeedback = buildRetryFeedback;
15
+ exports.enforceGuardrails = enforceGuardrails;
16
+ // Global ceiling on revision rounds regardless of per-guardrail max_retries —
17
+ // bounds worst-case latency added before delivery.
18
+ const GLOBAL_MAX_RETRY_ROUNDS = 3;
19
+ function buildRetryFeedback(failures) {
20
+ const lines = failures.map(f => `- "${f.def.name}": ${f.def.rule}${f.verdict.reasoning ? ` (violation: ${f.verdict.reasoning})` : ''}`);
21
+ return [
22
+ `Your previous response violated ${failures.length === 1 ? 'a guardrail' : `${failures.length} guardrails`}:`,
23
+ ...lines,
24
+ '',
25
+ 'Revise your response so it fully complies with every guardrail above.',
26
+ 'Reply ONLY with the corrected response — do not mention the guardrails, this revision process, or apologize for the previous version.',
27
+ ].join('\n');
28
+ }
29
+ async function judgeAll(io, output, defs) {
30
+ const fastDefs = defs.filter(d => d.mode === 'fast');
31
+ const runtimeDefs = defs.filter(d => d.mode === 'runtime');
32
+ const failOpen = (d, why) => ({ id: d.id, verdict: 'pass', reasoning: `checker_error: ${why}`, checker_error: true });
33
+ const fastP = fastDefs.length === 0 ? Promise.resolve([]) :
34
+ io.checkFast(output, fastDefs).then(vs => fastDefs.map(d => ({ def: d, verdict: vs.find(v => v.id === d.id) ?? failOpen(d, 'missing verdict') })), err => fastDefs.map(d => ({ def: d, verdict: failOpen(d, err.message) })));
35
+ const runtimeP = Promise.all(runtimeDefs.map(d => io.checkRuntime(output, d).then(v => ({ def: d, verdict: v }), err => ({ def: d, verdict: failOpen(d, err.message) }))));
36
+ const [fast, rt] = await Promise.all([fastP, runtimeP]);
37
+ return [...fast, ...rt];
38
+ }
39
+ async function enforceGuardrails(io) {
40
+ try {
41
+ const defs = await io.loadGuardrails();
42
+ if (!defs || defs.length === 0)
43
+ return;
44
+ void io.emit('guardrails_checking', { count: defs.length });
45
+ let output = (await io.fetchOutput())?.trim() ?? '';
46
+ if (!output)
47
+ return; // nothing produced → nothing to judge
48
+ const events = [];
49
+ let attempt = 0;
50
+ let retried = false;
51
+ let judged = await judgeAll(io, output, defs);
52
+ // Retry loop: only rules with action='retry' trigger a revision round.
53
+ for (;;) {
54
+ const retryable = judged.filter(j => j.verdict.verdict === 'fail' && !j.verdict.checker_error &&
55
+ j.def.action === 'retry' && attempt < j.def.max_retries);
56
+ if (retryable.length === 0 || attempt >= GLOBAL_MAX_RETRY_ROUNDS)
57
+ break;
58
+ for (const j of retryable) {
59
+ events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'retried', attempt, reasoning: j.verdict.reasoning });
60
+ }
61
+ void io.emit('guardrail_retry', { attempt, guardrails: retryable.map(j => j.def.name) });
62
+ const res = await io.runRetry(buildRetryFeedback(retryable)).catch(() => null);
63
+ if (!res || !res.ok || !res.text.trim())
64
+ break; // retry unavailable/failed → resolve with what we have
65
+ output = res.text.trim();
66
+ retried = true;
67
+ attempt += 1;
68
+ judged = await judgeAll(io, output, defs);
69
+ }
70
+ // Final resolution over the last round of verdicts.
71
+ const blocked = [];
72
+ const flagged = [];
73
+ for (const j of judged) {
74
+ if (j.verdict.checker_error) {
75
+ flagged.push(j);
76
+ events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'flagged', attempt, reasoning: j.verdict.reasoning });
77
+ }
78
+ else if (j.verdict.verdict === 'fail' && j.def.action === 'block') {
79
+ blocked.push(j);
80
+ events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'blocked', attempt, reasoning: j.verdict.reasoning });
81
+ }
82
+ else if (j.verdict.verdict === 'fail') {
83
+ // action 'flag', or 'retry' with retries exhausted → deliver + warn
84
+ flagged.push(j);
85
+ events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'flagged', attempt, reasoning: j.verdict.reasoning });
86
+ }
87
+ else {
88
+ events.push({ guardrail_id: j.def.id, verdict: 'pass', action_taken: 'passed', attempt, reasoning: j.verdict.reasoning || null });
89
+ }
90
+ }
91
+ const meta = {
92
+ checked: defs.length,
93
+ passed: defs.length - blocked.length - flagged.length,
94
+ attempt,
95
+ };
96
+ if (flagged.length)
97
+ meta.flags = flagged.map(j => j.def.name);
98
+ if (blocked.length) {
99
+ meta.blocked_by = blocked.map(j => j.def.name);
100
+ meta.original_text = output;
101
+ meta.final_text = `⛔ This response was blocked by the guardrail “${blocked[0].def.name}”.`;
102
+ }
103
+ else if (retried) {
104
+ // Aggregation would otherwise concatenate original + revision deltas —
105
+ // pin assistant_text to the revision the checks actually passed on.
106
+ meta.final_text = output;
107
+ }
108
+ await io.setResult(meta, events);
109
+ void io.emit('guardrails_result', {
110
+ checked: defs.length,
111
+ passed: meta.passed,
112
+ flagged: flagged.map(j => j.def.name),
113
+ blocked: blocked.map(j => j.def.name),
114
+ attempt,
115
+ });
116
+ }
117
+ catch (err) {
118
+ // Fail open — guardrails must never prevent completion/delivery.
119
+ try {
120
+ void io.emit('guardrails_result', { error: err.message });
121
+ }
122
+ catch { /* ignore */ }
123
+ console.error(`[guardrails] enforce failed (failing open): ${err.message}`);
124
+ }
125
+ }
@@ -0,0 +1,17 @@
1
+ export interface HangWatchdogOpts {
2
+ idleMs: number;
3
+ /** When provided, consulted on every (re)arm — lets the caller widen the
4
+ * window mid-run (e.g. grok: once output has been seen, silent tool work
5
+ * is expected and gets a longer leash than a dead-silent spawn). */
6
+ idleMsFn?: () => number;
7
+ onHang: () => void;
8
+ setTimer?: (fn: () => void, ms: number) => unknown;
9
+ clearTimer?: (handle: unknown) => void;
10
+ }
11
+ export interface HangWatchdog {
12
+ /** Call on every agent event — rearms the idle timer. */
13
+ touch(): void;
14
+ /** Disarm permanently (run finished / terminal). */
15
+ stop(): void;
16
+ }
17
+ export declare function createHangWatchdog(opts: HangWatchdogOpts): HangWatchdog;