@genesislcap/ai-assistant 15.86.1 → 15.87.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 (84) hide show
  1. package/dist/ai-assistant.api.json +5026 -645
  2. package/dist/ai-assistant.d.ts +802 -0
  3. package/dist/chat-driver.cjs +1487 -132
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +1480 -132
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +2779 -657
  8. package/dist/dts/autopilot/autopilot-session.d.ts +25 -0
  9. package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -1
  10. package/dist/dts/chat-driver-node.d.ts +2 -0
  11. package/dist/dts/chat-driver-node.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/ai-driver.d.ts +15 -0
  13. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  14. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +6 -0
  15. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/interaction-controller.d.ts +12 -0
  19. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  20. package/dist/dts/headless/describe-interaction.d.ts +17 -0
  21. package/dist/dts/headless/describe-interaction.d.ts.map +1 -0
  22. package/dist/dts/headless/headless-debug.d.ts +28 -0
  23. package/dist/dts/headless/headless-debug.d.ts.map +1 -0
  24. package/dist/dts/headless/headless-persistence.d.ts +35 -0
  25. package/dist/dts/headless/headless-persistence.d.ts.map +1 -0
  26. package/dist/dts/headless/headless-session.d.ts +180 -0
  27. package/dist/dts/headless/headless-session.d.ts.map +1 -0
  28. package/dist/dts/headless/headless.types.d.ts +435 -0
  29. package/dist/dts/headless/headless.types.d.ts.map +1 -0
  30. package/dist/dts/headless/index.d.ts +5 -0
  31. package/dist/dts/headless/index.d.ts.map +1 -0
  32. package/dist/dts/headless-server/compact.d.ts +52 -0
  33. package/dist/dts/headless-server/compact.d.ts.map +1 -0
  34. package/dist/dts/headless-server/index.d.ts +23 -0
  35. package/dist/dts/headless-server/index.d.ts.map +1 -0
  36. package/dist/dts/headless-server/server.d.ts +13 -0
  37. package/dist/dts/headless-server/server.d.ts.map +1 -0
  38. package/dist/dts/headless-server/types.d.ts +210 -0
  39. package/dist/dts/headless-server/types.d.ts.map +1 -0
  40. package/dist/dts/index.d.ts +2 -0
  41. package/dist/dts/index.d.ts.map +1 -1
  42. package/dist/dts/main/main.d.ts.map +1 -1
  43. package/dist/dts/state/debug-event-log.d.ts +1 -1
  44. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  45. package/dist/dts/state/decision-surface.d.ts +5 -4
  46. package/dist/dts/state/decision-surface.d.ts.map +1 -1
  47. package/dist/dts/state/persistence/debug-log-assembly.d.ts +49 -0
  48. package/dist/dts/state/persistence/debug-log-assembly.d.ts.map +1 -0
  49. package/dist/dts/state/persistence/index.d.ts +1 -0
  50. package/dist/dts/state/persistence/index.d.ts.map +1 -1
  51. package/dist/dts/state/persistence/session-persister.d.ts +7 -0
  52. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  53. package/dist/dts/types/interaction-surface.d.ts +30 -0
  54. package/dist/dts/types/interaction-surface.d.ts.map +1 -1
  55. package/dist/esm/autopilot/autopilot-session.js +31 -16
  56. package/dist/esm/chat-driver-node.js +8 -0
  57. package/dist/esm/components/ai-driver/base-driver-decorator.js +12 -0
  58. package/dist/esm/components/chat-driver/chat-driver.js +8 -0
  59. package/dist/esm/components/chat-driver/interaction-controller.js +66 -0
  60. package/dist/esm/headless/describe-interaction.js +55 -0
  61. package/dist/esm/headless/headless-debug.js +62 -0
  62. package/dist/esm/headless/headless-persistence.js +192 -0
  63. package/dist/esm/headless/headless-session.js +808 -0
  64. package/dist/esm/headless/headless.types.js +1 -0
  65. package/dist/esm/headless/index.js +3 -0
  66. package/dist/esm/headless-server/compact.js +160 -0
  67. package/dist/esm/headless-server/index.js +21 -0
  68. package/dist/esm/headless-server/server.js +746 -0
  69. package/dist/esm/headless-server/types.js +3 -0
  70. package/dist/esm/index.js +3 -0
  71. package/dist/esm/main/main.js +18 -37
  72. package/dist/esm/state/debug-event-log.js +6 -1
  73. package/dist/esm/state/decision-surface.js +75 -48
  74. package/dist/esm/state/persistence/debug-log-assembly.js +69 -0
  75. package/dist/esm/state/persistence/index.js +1 -0
  76. package/dist/esm/state/persistence/session-persister.js +16 -0
  77. package/dist/headless-server.cjs +853 -0
  78. package/dist/headless-server.cjs.map +7 -0
  79. package/dist/headless-server.mjs +819 -0
  80. package/dist/headless-server.mjs.map +7 -0
  81. package/package.json +28 -18
  82. package/scripts/build-headless-server-node.mjs +42 -0
  83. package/scripts/smoke-chat-driver-node.mjs +3 -0
  84. package/scripts/smoke-headless-server-node.mjs +112 -0
@@ -0,0 +1,819 @@
1
+ // src/headless-server/compact.ts
2
+ var MESSAGE_LINES = 3;
3
+ var MESSAGE_CHARS = 300;
4
+ var TURN_TEXT_CHARS = 1500;
5
+ function firstLines(text, lines, chars) {
6
+ const kept = text.split("\n").slice(0, lines).join("\n");
7
+ const cut = kept.length > chars ? `${kept.slice(0, chars)}\u2026` : kept;
8
+ return cut === text ? { text } : { text: cut, chars: text.length };
9
+ }
10
+ function present(source) {
11
+ const out = {};
12
+ for (const [key, value] of Object.entries(source)) {
13
+ if (value === void 0 || value === null || value === "" || value === false) continue;
14
+ if (Array.isArray(value) && value.length === 0) continue;
15
+ out[key] = value;
16
+ }
17
+ return out;
18
+ }
19
+ function compactEvent(event, options = {}) {
20
+ switch (event.type) {
21
+ case "decision":
22
+ case "working":
23
+ return void 0;
24
+ case "settled":
25
+ return {
26
+ event: "settled",
27
+ component: event.component,
28
+ ...present({
29
+ answeredBy: event.answeredBy,
30
+ auto: event.auto,
31
+ raw: event.raw,
32
+ resolvedItself: event.resolvedItself
33
+ })
34
+ };
35
+ case "message": {
36
+ const { text, chars } = firstLines(event.text, MESSAGE_LINES, MESSAGE_CHARS);
37
+ return { event: "message", text, ...present({ chars: chars ?? event.chars }) };
38
+ }
39
+ case "turn": {
40
+ const out = {
41
+ event: "turn",
42
+ reason: event.reason,
43
+ ...present({ failureReason: event.failureReason })
44
+ };
45
+ if (event.text) {
46
+ const { text, chars } = firstLines(event.text, Number.MAX_SAFE_INTEGER, TURN_TEXT_CHARS);
47
+ out.text = text;
48
+ if (chars ?? event.chars) out.chars = chars ?? event.chars;
49
+ }
50
+ return out;
51
+ }
52
+ case "autopilot":
53
+ if (event.mode === "unavailable" && !event.needsUser) return void 0;
54
+ return {
55
+ event: "autopilot",
56
+ mode: event.mode,
57
+ status: event.status,
58
+ ...present({ needsUser: event.needsUser, working: event.working, reason: event.reason })
59
+ };
60
+ case "error":
61
+ return { event: "error", where: event.where, message: event.message };
62
+ case "probe": {
63
+ const view = options.probeEvent;
64
+ if (!view) {
65
+ const changed = Object.fromEntries(event.changed.map((key) => [key, event.probe[key]]));
66
+ return { event: "probe", ...changed };
67
+ }
68
+ if (!event.changed.some((key) => view.keys.includes(key))) return void 0;
69
+ return {
70
+ event: view.name,
71
+ ...Object.fromEntries(view.keys.map((key) => [key, event.probe[key] ?? null]))
72
+ };
73
+ }
74
+ default:
75
+ return void 0;
76
+ }
77
+ }
78
+ var PREVIEW_MARK = " (shown below as Now)";
79
+ var NOW_PREFIX = "Now: ";
80
+ var CUT_PREFIX = "[Cut at ";
81
+ function leanDecisionText(text, views) {
82
+ const names = Object.keys(views ?? {});
83
+ if (!names.length) return text;
84
+ const lines = text.split("\n");
85
+ const marked = lines.findIndex((line) => line.endsWith(PREVIEW_MARK));
86
+ if (marked < 0) return text;
87
+ const startsEntry = (line, name) => line === ` ${name}${PREVIEW_MARK}` || line.startsWith(` ${name} \u2014 `);
88
+ let entry = marked;
89
+ while (entry >= 0 && !names.some((n) => startsEntry(lines[entry], n))) entry -= 1;
90
+ const view = entry < 0 ? void 0 : names.find((n) => startsEntry(lines[entry], n));
91
+ if (view === void 0) return text;
92
+ const now = lines.findIndex((line, i) => i > marked && line.startsWith(NOW_PREFIX));
93
+ if (now < 0) return text;
94
+ const end = lines[now + 1]?.startsWith(CUT_PREFIX) ? now + 2 : now + 1;
95
+ lines.splice(
96
+ now,
97
+ end - now,
98
+ `(The "${view}" view is not repeated here: read it with read ${view}.)`
99
+ );
100
+ return lines.join("\n");
101
+ }
102
+ function compactDecision(description, decisionText = "full") {
103
+ const actions = description.actions.map((a) => ({
104
+ ...a.n !== void 0 ? { n: a.n } : {},
105
+ name: a.name,
106
+ ...present({ label: a.label !== a.name ? a.label : void 0 }),
107
+ enabled: a.enabled,
108
+ ...present({
109
+ unavailable: a.unavailable,
110
+ args: a.args,
111
+ argsSchema: a.args === "fields" ? a.argsSchema : void 0
112
+ })
113
+ }));
114
+ return {
115
+ interactionId: description.interactionId,
116
+ component: description.component,
117
+ text: decisionText === "lean" ? leanDecisionText(description.text, description.views) : description.text,
118
+ actions,
119
+ ...description.views && Object.keys(description.views).length ? { views: { ...description.views } } : {},
120
+ ...description.controller ? {} : { controller: false }
121
+ };
122
+ }
123
+ function shortDecision(interactionId, component, actions) {
124
+ return {
125
+ interactionId,
126
+ component,
127
+ actions: actions.map((a) => ({
128
+ ...a.n !== void 0 ? { n: a.n } : {},
129
+ name: a.name,
130
+ enabled: a.enabled
131
+ }))
132
+ };
133
+ }
134
+ var OUTPUT_CHARS = 4e3;
135
+ function capOutput(output) {
136
+ let json;
137
+ try {
138
+ json = JSON.stringify(output);
139
+ } catch {
140
+ return String(output);
141
+ }
142
+ if (json === void 0 || json.length <= OUTPUT_CHARS) return output;
143
+ return { truncated: `${json.slice(0, OUTPUT_CHARS)}\u2026`, chars: json.length };
144
+ }
145
+
146
+ // src/headless-server/server.ts
147
+ import { createWriteStream, mkdirSync } from "node:fs";
148
+ import { writeFile } from "node:fs/promises";
149
+ import { createServer } from "node:http";
150
+ import { join, resolve } from "node:path";
151
+
152
+ // src/headless-server/types.ts
153
+ var DECISION_TEXTS = ["full", "lean"];
154
+ var DEFAULT_DECISION_TEXT = "lean";
155
+
156
+ // src/headless-server/server.ts
157
+ var DEFAULT_MAX_WAIT_MS = 1e5;
158
+ var MAX_WAIT_CAP_MS = 36e5;
159
+ var BUILTIN_OPS = [
160
+ "send",
161
+ "act",
162
+ "resolve",
163
+ "autopilot",
164
+ "wait",
165
+ "state",
166
+ "read",
167
+ "debugLog",
168
+ "stop"
169
+ ];
170
+ var BYTES_PER_KB = 1024;
171
+ var MAX_BODY_MB = 16;
172
+ var MAX_BODY_BYTES = MAX_BODY_MB * BYTES_PER_KB * BYTES_PER_KB;
173
+ var HTTP_OK = 200;
174
+ var HTTP_BAD_REQUEST = 400;
175
+ var HTTP_FORBIDDEN = 403;
176
+ var HTTP_NOT_FOUND = 404;
177
+ var HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
178
+ var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]", "::1"];
179
+ var HTTP_SERVER_ERROR = 500;
180
+ var CLOSE_GRACE_MS = 500;
181
+ var MONEY_SCALE = 1e4;
182
+ var WATCH_MS = 2e3;
183
+ var ALL_EVENT_TYPES = [
184
+ "decision",
185
+ "working",
186
+ "settled",
187
+ "message",
188
+ "turn",
189
+ "autopilot",
190
+ "probe",
191
+ "error"
192
+ ];
193
+ function untilOf(value) {
194
+ if (value === void 0) return void 0;
195
+ const names = (Array.isArray(value) ? value : [value]).map(String);
196
+ return {
197
+ types: names.filter(
198
+ (n) => ALL_EVENT_TYPES.includes(n)
199
+ ),
200
+ idle: names.includes("idle")
201
+ };
202
+ }
203
+ function waitMsOf(value, fallback) {
204
+ const n = Number(value ?? fallback);
205
+ return Number.isFinite(n) ? Math.min(Math.max(0, n), MAX_WAIT_CAP_MS) : fallback;
206
+ }
207
+ var hasOwn = (target, key) => !!target && Object.prototype.hasOwnProperty.call(target, key);
208
+ var money = (usd) => Math.round(usd * MONEY_SCALE) / MONEY_SCALE;
209
+ function createHeadlessServer(options) {
210
+ const runDir = resolve(options.runDir);
211
+ mkdirSync(runDir, { recursive: true });
212
+ const defaultMaxWaitMs = waitMsOf(options.defaultMaxWaitMs, DEFAULT_MAX_WAIT_MS);
213
+ let decisionText = options.decisionText ?? DEFAULT_DECISION_TEXT;
214
+ const announce = options.announce ?? ((line) => console.log(line));
215
+ const events = createWriteStream(join(runDir, "events.jsonl"), { flags: "a" });
216
+ const startedAt = Date.now();
217
+ let session;
218
+ let http;
219
+ let closing = false;
220
+ let loggedSeq = 0;
221
+ let applying = Promise.resolve();
222
+ let flushing = Promise.resolve();
223
+ let resolveClosed;
224
+ const closed = new Promise((r) => {
225
+ resolveClosed = r;
226
+ });
227
+ const appendLog = (line) => {
228
+ if (!events.writableEnded) events.write(`${JSON.stringify(line)}
229
+ `);
230
+ };
231
+ function drainLog() {
232
+ if (!session) return;
233
+ for (const event of session.eventsSince(loggedSeq)) {
234
+ appendLog({ kind: "event", ...event });
235
+ loggedSeq = event.seq;
236
+ }
237
+ }
238
+ function flushAll() {
239
+ flushing = flushing.then(async () => {
240
+ try {
241
+ await options.flush?.();
242
+ await session?.flush();
243
+ } catch (e) {
244
+ appendLog({
245
+ kind: "error",
246
+ where: "flush",
247
+ message: e instanceof Error ? e.message : String(e)
248
+ });
249
+ }
250
+ });
251
+ return flushing;
252
+ }
253
+ async function watch(s) {
254
+ let cursor = s.state().lastSeq;
255
+ drainLog();
256
+ const watching = () => !closing && session === s;
257
+ while (watching()) {
258
+ try {
259
+ const woken = await s.wait({ since: cursor, until: ALL_EVENT_TYPES, maxWaitMs: WATCH_MS });
260
+ cursor = woken.nextSeq;
261
+ drainLog();
262
+ if (woken.events.some((e) => e.type === "turn")) await flushAll();
263
+ if (woken.status === "closed") break;
264
+ } catch (e) {
265
+ appendLog({
266
+ kind: "error",
267
+ where: "watch",
268
+ message: e instanceof Error ? e.message : String(e)
269
+ });
270
+ break;
271
+ }
272
+ }
273
+ }
274
+ const handle = {
275
+ get session() {
276
+ return session;
277
+ },
278
+ runDir,
279
+ get decisionText() {
280
+ return decisionText;
281
+ },
282
+ setDecisionText(mode) {
283
+ decisionText = mode;
284
+ },
285
+ attach(next) {
286
+ if (session === next) return;
287
+ session = next;
288
+ loggedSeq = 0;
289
+ void watch(next);
290
+ },
291
+ listen,
292
+ handle: runCommand,
293
+ shutdown,
294
+ closed
295
+ };
296
+ if (options.session) handle.attach(options.session);
297
+ const composerOf = (state) => {
298
+ if (!state) return "blocked:not-started";
299
+ const blocked = options.composerBlockedReason?.();
300
+ if (blocked) return `blocked:${blocked}`;
301
+ return state.busy ? "busy" : "open";
302
+ };
303
+ function statusNow(state) {
304
+ if (!state) return "idle";
305
+ if (state.pending) return "decision";
306
+ if (options.isEnded?.()) return "ended";
307
+ if (state.autopilot?.needsUser) return "needs-user";
308
+ return state.busy ? "working" : "idle";
309
+ }
310
+ const probeView = (state) => {
311
+ const view = options.probeEvent;
312
+ if (!view) return void 0;
313
+ return Object.fromEntries(view.keys.map((key) => [key, state.probe[key] ?? null]));
314
+ };
315
+ function stageOf(state, other) {
316
+ if (other) {
317
+ let stage;
318
+ try {
319
+ stage = options.describeWorking?.(other.interactionId, other.component);
320
+ } catch {
321
+ }
322
+ if (stage) return stage;
323
+ const idle = other.heldBy === "autopilot" && state.autopilot?.mode !== "running";
324
+ return other.heldBy !== "user" && !idle ? `${other.heldBy} is answering it` : void 0;
325
+ }
326
+ if (state.autopilot?.working && state.autopilot.mode === "running") {
327
+ return "autopilot is working";
328
+ }
329
+ return state.busy ? "assistant is thinking (no widget open)" : void 0;
330
+ }
331
+ function workingInfo(state, startedMs) {
332
+ const other = state.others.find((o) => !o.ready || o.heldBy !== "user") ?? state.others[0];
333
+ return {
334
+ ...other ? { component: other.component, heldBy: other.heldBy } : {},
335
+ elapsedMs: Date.now() - startedMs,
336
+ ...present({ probe: probeView(state), stage: stageOf(state, other) })
337
+ };
338
+ }
339
+ const decisionTextFor = (command) => DECISION_TEXTS.includes(command?.decisionText) ? command.decisionText : decisionText;
340
+ function reply(command, status, since, extra = {}, startedMs = Date.now()) {
341
+ const state = session?.state();
342
+ const compact = since.map((e) => compactEvent(e, { probeEvent: options.probeEvent })).filter((e) => !!e);
343
+ return {
344
+ ok: extra.ok ?? true,
345
+ ...command?.id !== void 0 ? { id: command.id } : {},
346
+ status,
347
+ ...compact.length ? { since: compact } : {},
348
+ ...state?.pending ? { decision: compactDecision(state.pending, decisionTextFor(command)) } : {},
349
+ ...status === "working" && state ? { working: workingInfo(state, startedMs) } : {},
350
+ composer: composerOf(state),
351
+ cost: { usd: money(state?.cost.costUsd ?? 0) },
352
+ nextSeq: state?.lastSeq ?? 0,
353
+ ...extra
354
+ };
355
+ }
356
+ function refuse(command, error, reason, since = void 0, shortFor) {
357
+ const state = session?.state();
358
+ const missed = session && since !== void 0 ? session.eventsSince(since) : [];
359
+ const pending = state?.pending;
360
+ return reply(command, statusNow(state), missed, {
361
+ ok: false,
362
+ error,
363
+ ...reason ? { reason } : {},
364
+ ...pending && shortFor !== void 0 && pending.interactionId === shortFor ? { decision: shortDecision(pending.interactionId, pending.component, pending.actions) } : {}
365
+ });
366
+ }
367
+ function edited(command, interactionId, result, cursor) {
368
+ const s = session;
369
+ const state = s.state();
370
+ const open = state.pending?.interactionId === interactionId ? state.pending : void 0;
371
+ const actions = result.description?.actions ?? open?.actions ?? result.actions;
372
+ const component = result.description?.component ?? open?.component ?? state.others.find((o) => o.interactionId === interactionId)?.component ?? "";
373
+ return reply(command, "edited", s.eventsSince(cursor), {
374
+ decision: shortDecision(interactionId, component, actions),
375
+ ...result.output !== void 0 ? { output: capOutput(result.output) } : {}
376
+ });
377
+ }
378
+ async function poll(command, cursor, startedMs, signal) {
379
+ const s = session;
380
+ if (!s) return reply(command, "idle", [], {}, startedMs);
381
+ const maxWaitMs = waitMsOf(command.maxWaitMs, defaultMaxWaitMs);
382
+ const deadline = startedMs + maxWaitMs;
383
+ const until = untilOf(command.until);
384
+ let waitFrom = cursor;
385
+ for (; ; ) {
386
+ const since = s.eventsSince(cursor);
387
+ const state = s.state();
388
+ const verdict = terminal(since, state, until);
389
+ if (verdict) {
390
+ if (since.some((e) => e.type === "turn") || verdict === "ended") await flushAll();
391
+ drainLog();
392
+ return reply(command, verdict, s.eventsSince(cursor), {}, startedMs);
393
+ }
394
+ const remaining = deadline - Date.now();
395
+ if (remaining <= 0 || signal?.aborted) {
396
+ drainLog();
397
+ return reply(command, "working", since, {}, startedMs);
398
+ }
399
+ const woken = await s.wait({
400
+ since: waitFrom,
401
+ ...until ? { until: until.types } : {},
402
+ maxWaitMs: remaining,
403
+ ...signal ? { signal } : {}
404
+ });
405
+ waitFrom = woken.nextSeq;
406
+ if (woken.status === "closed") {
407
+ drainLog();
408
+ return reply(command, "ended", s.eventsSince(cursor), {}, startedMs);
409
+ }
410
+ }
411
+ }
412
+ function terminal(since, state, until) {
413
+ const turnEnded = since.some((e) => e.type === "turn");
414
+ const failed = since.some((e) => e.type === "error" || e.type === "turn" && e.failureReason);
415
+ if (options.isEnded?.() && !state.pending && (turnEnded || !state.busy)) return "ended";
416
+ if (until) {
417
+ if (state.pending && until.types.includes("decision")) return "decision";
418
+ if (since.some((e) => until.types.includes(e.type))) {
419
+ return since.some((e) => e.type === "error") ? "error" : turnEnded ? "turn" : statusNow(state);
420
+ }
421
+ if (until.idle && !state.busy && !state.pending) return "idle";
422
+ return void 0;
423
+ }
424
+ if (state.pending) return "decision";
425
+ if (failed) return "error";
426
+ if (turnEnded) return "turn";
427
+ if (state.autopilot?.needsUser) return "needs-user";
428
+ return void 0;
429
+ }
430
+ const isOpen = (state, interactionId) => state.pending?.interactionId === interactionId || state.others.some((o) => o.interactionId === interactionId);
431
+ const gone = (command, interactionId, since) => refuse(
432
+ command,
433
+ `interaction ${interactionId} is not pending: it has settled, or never existed`,
434
+ "ended",
435
+ since
436
+ );
437
+ const notStarted = (command) => refuse(
438
+ command,
439
+ options.extraOps?.start ? 'not started: send {"op":"start", \u2026} first' : "no session is attached",
440
+ "not-started"
441
+ );
442
+ const str = (value) => typeof value === "string" ? value : void 0;
443
+ const actionOf = (value) => typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
444
+ async function applyOp(command, s, cursor) {
445
+ const state = s.state();
446
+ switch (command.op) {
447
+ case "send": {
448
+ const text = str(command.text);
449
+ if (text === void 0) return refuse(command, "`send` needs `text`", "invalid-command");
450
+ const paths = Array.isArray(command.files) ? command.files.map(String) : [];
451
+ let attachments;
452
+ if (paths.length) {
453
+ if (!options.readAttachment) {
454
+ return refuse(command, "this server cannot read files", "invalid-command");
455
+ }
456
+ try {
457
+ attachments = await Promise.all(paths.map((p) => options.readAttachment(p)));
458
+ } catch (e) {
459
+ return refuse(
460
+ command,
461
+ `could not read a file: ${e instanceof Error ? e.message : String(e)}`,
462
+ "invalid-command"
463
+ );
464
+ }
465
+ }
466
+ const sent = s.send(text, {
467
+ ...attachments ? { attachments } : {},
468
+ ...str(command.agent) ? { agent: str(command.agent) } : {}
469
+ });
470
+ return sent.ok ? "poll" : refuse(command, sent.error, "refused", cursor);
471
+ }
472
+ case "act": {
473
+ const interactionId = str(command.interactionId);
474
+ if (!interactionId || command.action === void 0) {
475
+ return refuse(command, "`act` needs `interactionId` and `action`", "invalid-command");
476
+ }
477
+ if (!isOpen(state, interactionId)) return gone(command, interactionId, cursor);
478
+ const result = await s.act(interactionId, actionOf(command.action), {
479
+ ...str(command.text) !== void 0 ? { text: str(command.text) } : {},
480
+ ...Array.isArray(command.options) ? { options: command.options.map(String) } : {},
481
+ ...command.fields !== void 0 ? { fields: command.fields } : {}
482
+ });
483
+ if (!result.ok) return refuse(command, result.message, result.error, cursor, interactionId);
484
+ if (result.finished === false) return edited(command, interactionId, result, cursor);
485
+ return "poll";
486
+ }
487
+ case "resolve": {
488
+ const interactionId = str(command.interactionId);
489
+ if (!interactionId)
490
+ return refuse(command, "`resolve` needs `interactionId` and `result`", "invalid-command");
491
+ if (!isOpen(state, interactionId)) return gone(command, interactionId, cursor);
492
+ const resolved = s.resolve(interactionId, command.result);
493
+ return resolved.ok ? "poll" : refuse(command, resolved.error, "refused", cursor);
494
+ }
495
+ case "autopilot": {
496
+ const mode = {
497
+ pause: "pause",
498
+ paused: "pause",
499
+ resume: "resume",
500
+ running: "resume",
501
+ stop: "stop"
502
+ }[String(command.mode)];
503
+ if (!mode)
504
+ return refuse(
505
+ command,
506
+ "`autopilot` needs `mode`: running, paused or stop",
507
+ "invalid-command"
508
+ );
509
+ const done = s.autopilot(mode);
510
+ return done.ok ? "poll" : refuse(command, done.error, "refused", cursor);
511
+ }
512
+ case "wait":
513
+ return "poll";
514
+ default:
515
+ return refuse(command, `unknown op "${command.op}"`, "unknown-op");
516
+ }
517
+ }
518
+ async function readOp(command, s) {
519
+ switch (command.op) {
520
+ case "state": {
521
+ const state = s.state();
522
+ return reply(command, statusNow(state), [], {
523
+ ...state.others.length ? { others: state.others } : {},
524
+ ...state.autopilot ? { autopilot: state.autopilot } : {},
525
+ ...Object.keys(state.probe).length ? { probe: state.probe } : {},
526
+ busy: state.busy
527
+ });
528
+ }
529
+ case "read": {
530
+ const interactionId = str(command.interactionId);
531
+ if (!interactionId)
532
+ return refuse(command, "`read` needs `interactionId`", "invalid-command");
533
+ const read = await s.read(interactionId, {
534
+ ...str(command.view) ? { view: str(command.view) } : {},
535
+ ...str(command.tool) ? { tool: str(command.tool) } : {},
536
+ ...command.args && typeof command.args === "object" ? { args: command.args } : {}
537
+ });
538
+ return read.ok ? {
539
+ ok: true,
540
+ ...command.id !== void 0 ? { id: command.id } : {},
541
+ content: read.content
542
+ } : {
543
+ ok: false,
544
+ ...command.id !== void 0 ? { id: command.id } : {},
545
+ error: read.error
546
+ };
547
+ }
548
+ case "debugLog": {
549
+ await flushAll();
550
+ const log = await s.debugLog();
551
+ const path = join(runDir, "debug-log.json");
552
+ await writeFile(path, JSON.stringify(log));
553
+ const byKind = {};
554
+ for (const entry of log.timeline) {
555
+ const kind = String(entry.kind ?? "unknown");
556
+ byKind[kind] = (byKind[kind] ?? 0) + 1;
557
+ }
558
+ const usd = log.sessionUsage?.costUsd ?? s.state().cost.costUsd;
559
+ return reply(command, statusNow(s.state()), [], {
560
+ path,
561
+ entries: { total: log.timeline.length, byKind },
562
+ cost: { usd: money(usd) }
563
+ });
564
+ }
565
+ default:
566
+ return void 0;
567
+ }
568
+ }
569
+ async function runCommand(raw, signal) {
570
+ const startedMs = Date.now();
571
+ const command = raw;
572
+ if (!command || typeof command !== "object" || typeof command.op !== "string") {
573
+ return reply(void 0, statusNow(session?.state()), [], {
574
+ ok: false,
575
+ error: "a command is a JSON object with a string `op`",
576
+ reason: "invalid-command"
577
+ });
578
+ }
579
+ appendLog({ kind: "command", at: (/* @__PURE__ */ new Date()).toISOString(), command });
580
+ if (command.decisionText !== void 0 && !DECISION_TEXTS.includes(command.decisionText)) {
581
+ return refuse(
582
+ command,
583
+ `\`decisionText\` must be one of ${DECISION_TEXTS.join(", ")} (got ${JSON.stringify(command.decisionText)})`,
584
+ "invalid-command"
585
+ );
586
+ }
587
+ try {
588
+ const out = await dispatch(command, startedMs, signal);
589
+ drainLog();
590
+ return out;
591
+ } catch (e) {
592
+ drainLog();
593
+ appendLog({
594
+ kind: "error",
595
+ where: `op:${command.op}`,
596
+ message: e instanceof Error ? e.message : String(e)
597
+ });
598
+ return reply(command, statusNow(session?.state()), [], {
599
+ ok: false,
600
+ status: "error",
601
+ error: e instanceof Error ? e.message : String(e),
602
+ reason: "threw"
603
+ });
604
+ }
605
+ }
606
+ async function dispatch(command, startedMs, signal) {
607
+ if (command.op === "stop") {
608
+ session?.stop();
609
+ const out = reply(command, "ended", [], { stopped: true });
610
+ setTimeout(() => void shutdown(), 0);
611
+ return out;
612
+ }
613
+ const isBuiltin = BUILTIN_OPS.includes(command.op);
614
+ const extra = !isBuiltin && hasOwn(options.extraOps, command.op) ? options.extraOps[command.op] : void 0;
615
+ if (!extra && !isBuiltin) {
616
+ return refuse(command, `unknown op "${command.op}"`, "unknown-op");
617
+ }
618
+ if (command.op === "state" || command.op === "read" || command.op === "debugLog") {
619
+ if (!session) return notStarted(command);
620
+ return await readOp(command, session);
621
+ }
622
+ const cursor = typeof command.since === "number" ? command.since : session?.state().lastSeq ?? 0;
623
+ if (command.op === "wait") {
624
+ if (!session) return notStarted(command);
625
+ return poll(command, cursor, startedMs, signal);
626
+ }
627
+ const previous = applying;
628
+ let release;
629
+ applying = new Promise((r) => {
630
+ release = r;
631
+ });
632
+ let applied;
633
+ try {
634
+ await previous;
635
+ if (extra) {
636
+ applied = await extra(command, handle);
637
+ } else {
638
+ if (!session) return notStarted(command);
639
+ applied = await applyOp(command, session, cursor);
640
+ }
641
+ } finally {
642
+ release();
643
+ }
644
+ if (applied === "poll") return poll(command, cursor, startedMs, signal);
645
+ if ("status" in applied) return applied;
646
+ if (!applied.ok)
647
+ return refuse(
648
+ command,
649
+ applied.error ?? "refused",
650
+ "refused",
651
+ typeof command.since === "number" ? cursor : void 0
652
+ );
653
+ const wait = applied.poll ?? !!session;
654
+ if (!wait || !session) {
655
+ const pending = session?.state().pending;
656
+ return reply(
657
+ command,
658
+ statusNow(session?.state()),
659
+ [],
660
+ {
661
+ ...applied.fields,
662
+ ...applied.shortDecision && pending ? { decision: shortDecision(pending.interactionId, pending.component, pending.actions) } : {}
663
+ },
664
+ startedMs
665
+ );
666
+ }
667
+ const polled = await poll(command, cursor, startedMs, signal);
668
+ return { ...polled, ...applied.fields };
669
+ }
670
+ function readBody(req) {
671
+ return new Promise((resolveBody, reject) => {
672
+ const chunks = [];
673
+ let size = 0;
674
+ req.on("data", (chunk) => {
675
+ size += chunk.length;
676
+ if (size > MAX_BODY_BYTES) {
677
+ reject(new Error("the command is too large"));
678
+ req.destroy();
679
+ return;
680
+ }
681
+ chunks.push(chunk);
682
+ });
683
+ req.on("end", () => resolveBody(Buffer.concat(chunks).toString("utf8")));
684
+ req.on("error", reject);
685
+ });
686
+ }
687
+ function send(res, status, body) {
688
+ const json = JSON.stringify(body);
689
+ res.writeHead(status, {
690
+ "content-type": "application/json; charset=utf-8",
691
+ "content-length": Buffer.byteLength(json)
692
+ });
693
+ res.end(json);
694
+ }
695
+ function refusal(req, port) {
696
+ if (req.headers.origin !== void 0) {
697
+ return { status: HTTP_FORBIDDEN, error: "a request from a web page is refused" };
698
+ }
699
+ const bound = options.host ?? "127.0.0.1";
700
+ if (LOOPBACK_HOSTS.includes(bound)) {
701
+ const host = (req.headers.host ?? "").toLowerCase();
702
+ if (!LOOPBACK_HOSTS.some((name) => host === `${name}:${port}`)) {
703
+ return {
704
+ status: HTTP_FORBIDDEN,
705
+ error: `the Host must be 127.0.0.1:${port} or localhost:${port}`
706
+ };
707
+ }
708
+ }
709
+ if (req.method === "POST") {
710
+ const type = (req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
711
+ if (type !== "application/json") {
712
+ return {
713
+ status: HTTP_UNSUPPORTED_MEDIA_TYPE,
714
+ error: "POST /cmd takes content-type: application/json"
715
+ };
716
+ }
717
+ }
718
+ return void 0;
719
+ }
720
+ async function onRequest(req, res) {
721
+ const refused = refusal(req, http.address().port);
722
+ if (refused) {
723
+ send(res, refused.status, { ok: false, status: "error", error: refused.error });
724
+ return;
725
+ }
726
+ const url = (req.url ?? "/").split("?")[0];
727
+ if (req.method === "GET" && url === "/health") {
728
+ const state = session?.state();
729
+ send(res, HTTP_OK, {
730
+ ok: true,
731
+ started: !!session,
732
+ runDir,
733
+ pid: process.pid,
734
+ uptimeMs: Date.now() - startedAt,
735
+ ...state ? { status: statusNow(state), nextSeq: state.lastSeq } : {}
736
+ });
737
+ return;
738
+ }
739
+ if (req.method === "POST" && url === "/cmd") {
740
+ let command;
741
+ try {
742
+ command = JSON.parse(await readBody(req));
743
+ } catch (e) {
744
+ send(res, HTTP_BAD_REQUEST, {
745
+ ok: false,
746
+ status: "error",
747
+ error: `bad command: ${e instanceof Error ? e.message : String(e)}`
748
+ });
749
+ return;
750
+ }
751
+ const gone2 = new AbortController();
752
+ res.on("close", () => {
753
+ if (!res.writableEnded) gone2.abort();
754
+ });
755
+ const out = await runCommand(command, gone2.signal);
756
+ if (!gone2.signal.aborted) send(res, HTTP_OK, out);
757
+ return;
758
+ }
759
+ send(res, HTTP_NOT_FOUND, {
760
+ ok: false,
761
+ error: "GET /health, or POST /cmd with one JSON command"
762
+ });
763
+ }
764
+ function listen() {
765
+ return new Promise((resolveListen, reject) => {
766
+ http = createServer((req, res) => {
767
+ onRequest(req, res).catch((e) => {
768
+ if (!res.headersSent)
769
+ send(res, HTTP_SERVER_ERROR, { ok: false, status: "error", error: String(e) });
770
+ else res.end();
771
+ });
772
+ });
773
+ http.once("error", reject);
774
+ http.listen(options.port ?? 0, options.host ?? "127.0.0.1", () => {
775
+ const { port } = http.address();
776
+ announce(`HEADLESS_READY port=${port} runDir=${runDir}`);
777
+ resolveListen({ port, runDir });
778
+ });
779
+ });
780
+ }
781
+ async function shutdown() {
782
+ if (closing) return closed;
783
+ closing = true;
784
+ try {
785
+ session?.stop();
786
+ } catch {
787
+ }
788
+ await flushAll();
789
+ drainLog();
790
+ await new Promise((r) => {
791
+ if (!http) {
792
+ r();
793
+ return;
794
+ }
795
+ http.close(() => r());
796
+ http.closeIdleConnections?.();
797
+ setTimeout(() => http?.closeAllConnections?.(), CLOSE_GRACE_MS).unref();
798
+ });
799
+ await new Promise((r) => {
800
+ events.end(r);
801
+ });
802
+ await options.onClosed?.();
803
+ resolveClosed();
804
+ return closed;
805
+ }
806
+ return handle;
807
+ }
808
+ export {
809
+ DECISION_TEXTS,
810
+ DEFAULT_DECISION_TEXT,
811
+ capOutput,
812
+ compactDecision,
813
+ compactEvent,
814
+ createHeadlessServer,
815
+ firstLines,
816
+ leanDecisionText,
817
+ shortDecision
818
+ };
819
+ //# sourceMappingURL=headless-server.mjs.map