@axiom-lattice/cli-a2a 0.1.5 → 0.1.6
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/.turbo/turbo-build.log +34 -32
- package/CHANGELOG.md +20 -0
- package/README.md +85 -2
- package/__tests__/opencode-executor.test.ts +631 -31
- package/__tests__/session-store-path.test.ts +40 -0
- package/dist/{chunk-NQIDRU47.mjs → chunk-54SKDK3D.mjs} +78 -32
- package/dist/chunk-54SKDK3D.mjs.map +1 -0
- package/dist/chunk-BSNQOP2W.mjs +350 -0
- package/dist/chunk-BSNQOP2W.mjs.map +1 -0
- package/dist/chunk-DRFFQJII.mjs +195 -0
- package/dist/chunk-DRFFQJII.mjs.map +1 -0
- package/dist/chunk-URJDRXJW.mjs +378 -0
- package/dist/chunk-URJDRXJW.mjs.map +1 -0
- package/dist/{chunk-G7AGL2QA.mjs → chunk-WZB7FBSX.mjs} +23 -5
- package/dist/chunk-WZB7FBSX.mjs.map +1 -0
- package/dist/{chunk-LXL47XMZ.mjs → chunk-YGUPSPZO.mjs} +9 -1
- package/dist/chunk-YGUPSPZO.mjs.map +1 -0
- package/dist/{chunk-35NFMGMS.mjs → chunk-ZP3JNJQJ.mjs} +3 -3
- package/dist/chunk-ZP3JNJQJ.mjs.map +1 -0
- package/dist/cli.js +769 -156
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +7 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/executor-LLE5VEFJ.mjs +12 -0
- package/dist/executor-LO7P3KUW.mjs +12 -0
- package/dist/{executor-RVBGAWUF.mjs → executor-OOLGGXHS.mjs} +4 -3
- package/dist/{executors-QIIKBUMJ.mjs → executors-AQHOPGSR.mjs} +2 -2
- package/dist/index.d.mts +92 -21
- package/dist/index.d.ts +92 -21
- package/dist/index.js +731 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -7
- package/package.json +10 -3
- package/runtime/cli-path.cjs +3 -0
- package/runtime/cli-path.d.ts +1 -0
- package/runtime/cli-path.mjs +5 -0
- package/src/config/defaults.ts +3 -1
- package/src/config/types.ts +8 -3
- package/src/executors/claude/client.ts +44 -14
- package/src/executors/claude/executor.ts +65 -30
- package/src/executors/codex/client.ts +239 -16
- package/src/executors/codex/executor.ts +70 -37
- package/src/executors/events.ts +15 -1
- package/src/executors/index.ts +13 -6
- package/src/executors/opencode/client.ts +192 -18
- package/src/executors/opencode/executor.ts +65 -30
- package/src/index.ts +8 -0
- package/src/local-state.ts +53 -0
- package/src/server/agent-card.ts +1 -0
- package/src/server/index.ts +22 -7
- package/src/session-store.ts +262 -0
- package/dist/chunk-35NFMGMS.mjs.map +0 -1
- package/dist/chunk-G7AGL2QA.mjs.map +0 -1
- package/dist/chunk-LXL47XMZ.mjs.map +0 -1
- package/dist/chunk-NQIDRU47.mjs.map +0 -1
- package/dist/chunk-VSZ3DACI.mjs +0 -179
- package/dist/chunk-VSZ3DACI.mjs.map +0 -1
- package/dist/chunk-WNCDOYZS.mjs +0 -187
- package/dist/chunk-WNCDOYZS.mjs.map +0 -1
- package/dist/executor-OWPVDUXH.mjs +0 -11
- package/dist/executor-XWHWUVQ3.mjs +0 -11
- /package/dist/{executor-OWPVDUXH.mjs.map → executor-LLE5VEFJ.mjs.map} +0 -0
- /package/dist/{executor-RVBGAWUF.mjs.map → executor-LO7P3KUW.mjs.map} +0 -0
- /package/dist/{executor-XWHWUVQ3.mjs.map → executor-OOLGGXHS.mjs.map} +0 -0
- /package/dist/{executors-QIIKBUMJ.mjs.map → executors-AQHOPGSR.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -34,16 +34,19 @@ __export(index_exports, {
|
|
|
34
34
|
CodexExecutor: () => CodexExecutor,
|
|
35
35
|
LogLevel: () => LogLevel,
|
|
36
36
|
OpenCodeExecutor: () => OpenCodeExecutor,
|
|
37
|
+
SessionBindingStore: () => SessionBindingStore,
|
|
37
38
|
createA2AServer: () => createA2AServer,
|
|
38
39
|
createClaudeExecutor: () => createClaudeExecutor,
|
|
39
40
|
createCodexExecutor: () => createCodexExecutor,
|
|
40
41
|
createExecutor: () => createExecutor,
|
|
41
42
|
createOpenCodeExecutor: () => createOpenCodeExecutor,
|
|
43
|
+
generateContextId: () => generateContextId,
|
|
42
44
|
getExecutor: () => getExecutor,
|
|
43
45
|
loadConfigFile: () => loadConfigFile,
|
|
44
46
|
logger: () => logger,
|
|
45
47
|
registerExecutor: () => registerExecutor,
|
|
46
|
-
resolveConfig: () => resolveConfig
|
|
48
|
+
resolveConfig: () => resolveConfig,
|
|
49
|
+
resolveSessionStorePath: () => resolveSessionStorePath
|
|
47
50
|
});
|
|
48
51
|
module.exports = __toCommonJS(index_exports);
|
|
49
52
|
|
|
@@ -91,6 +94,8 @@ var DEFAULTS = Object.freeze({
|
|
|
91
94
|
session: {
|
|
92
95
|
titlePrefix: "A2A Session",
|
|
93
96
|
reuseByContext: true,
|
|
97
|
+
persist: true,
|
|
98
|
+
storePath: "",
|
|
94
99
|
ttl: 36e5,
|
|
95
100
|
cleanupInterval: 3e5
|
|
96
101
|
},
|
|
@@ -109,7 +114,7 @@ var DEFAULTS = Object.freeze({
|
|
|
109
114
|
level: "info"
|
|
110
115
|
},
|
|
111
116
|
opencode: {
|
|
112
|
-
baseUrl: "
|
|
117
|
+
baseUrl: "",
|
|
113
118
|
projectDirectory: "",
|
|
114
119
|
model: "",
|
|
115
120
|
agent: "",
|
|
@@ -284,7 +289,8 @@ function buildAgentCard(config) {
|
|
|
284
289
|
skills,
|
|
285
290
|
capabilities: {
|
|
286
291
|
streaming: agentCard.streaming ?? true,
|
|
287
|
-
pushNotifications: agentCard.pushNotifications ?? false
|
|
292
|
+
pushNotifications: agentCard.pushNotifications ?? false,
|
|
293
|
+
stateTransitionHistory: true
|
|
288
294
|
},
|
|
289
295
|
// Additional interfaces advertised to orchestrators
|
|
290
296
|
additionalInterfaces: [
|
|
@@ -306,24 +312,219 @@ function registerExecutor(provider, factory) {
|
|
|
306
312
|
function getExecutor(provider) {
|
|
307
313
|
return registry.get(provider);
|
|
308
314
|
}
|
|
309
|
-
function createExecutor(config) {
|
|
315
|
+
function createExecutor(config, sessionStore) {
|
|
310
316
|
const factory = registry.get(config.provider);
|
|
311
317
|
if (!factory) {
|
|
312
318
|
const available = Array.from(registry.keys()).join(", ");
|
|
313
319
|
throw new Error(`Unknown provider: ${config.provider}. Available: ${available}`);
|
|
314
320
|
}
|
|
315
|
-
return factory(config);
|
|
321
|
+
return factory(config, sessionStore);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/session-store.ts
|
|
325
|
+
var import_node_path2 = require("path");
|
|
326
|
+
var import_node_os = require("os");
|
|
327
|
+
var import_node_fs2 = require("fs");
|
|
328
|
+
var import_node_crypto = require("crypto");
|
|
329
|
+
var log3 = logger.child("session-store");
|
|
330
|
+
var DEFAULT_STORE_DIR = (0, import_node_path2.join)(
|
|
331
|
+
(0, import_node_os.homedir)(),
|
|
332
|
+
"Library",
|
|
333
|
+
"Application Support",
|
|
334
|
+
"axiom-lattice",
|
|
335
|
+
"cli-a2a"
|
|
336
|
+
);
|
|
337
|
+
var DEFAULT_STORE_PATH = (0, import_node_path2.join)(DEFAULT_STORE_DIR, "sessions.json");
|
|
338
|
+
var DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
339
|
+
function resolveSessionStorePath(options) {
|
|
340
|
+
if (options.configuredPath && options.configuredPath.length > 0) {
|
|
341
|
+
return options.configuredPath;
|
|
342
|
+
}
|
|
343
|
+
return (0, import_node_path2.join)(
|
|
344
|
+
options.storeDir ?? DEFAULT_STORE_DIR,
|
|
345
|
+
`sessions-${options.provider}-${options.port}.json`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
var SessionBindingStore = class {
|
|
349
|
+
constructor(opts) {
|
|
350
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
351
|
+
this.cleanupTimer = null;
|
|
352
|
+
this.dirty = false;
|
|
353
|
+
this.flushTimeout = null;
|
|
354
|
+
this.storePath = opts?.storePath && opts.storePath.length > 0 ? opts.storePath : DEFAULT_STORE_PATH;
|
|
355
|
+
this.persist = opts?.persist ?? true;
|
|
356
|
+
this.loadFromFile();
|
|
357
|
+
this.startCleanup(opts?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS);
|
|
358
|
+
}
|
|
359
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
360
|
+
get(contextId, provider) {
|
|
361
|
+
const key = this.makeKey(contextId, provider);
|
|
362
|
+
const binding = this.cache.get(key);
|
|
363
|
+
if (!binding) return null;
|
|
364
|
+
if (this.isExpired(binding)) {
|
|
365
|
+
this.cache.delete(key);
|
|
366
|
+
this.markDirty();
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return binding;
|
|
370
|
+
}
|
|
371
|
+
set(binding) {
|
|
372
|
+
const key = this.makeKey(binding.contextId, binding.provider);
|
|
373
|
+
this.cache.set(key, binding);
|
|
374
|
+
this.markDirty();
|
|
375
|
+
}
|
|
376
|
+
delete(contextId, provider) {
|
|
377
|
+
const key = this.makeKey(contextId, provider);
|
|
378
|
+
const existed = this.cache.delete(key);
|
|
379
|
+
if (existed) this.markDirty();
|
|
380
|
+
return existed;
|
|
381
|
+
}
|
|
382
|
+
clearActiveTask(contextId, provider) {
|
|
383
|
+
const binding = this.get(contextId, provider);
|
|
384
|
+
if (binding) {
|
|
385
|
+
binding.activeTaskId = void 0;
|
|
386
|
+
binding.activeTaskState = void 0;
|
|
387
|
+
binding.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
388
|
+
this.markDirty();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Remove all expired entries.
|
|
393
|
+
* Called automatically on the cleanup interval; call manually before shutdown.
|
|
394
|
+
*/
|
|
395
|
+
cleanup() {
|
|
396
|
+
let removed = 0;
|
|
397
|
+
for (const [key, binding] of this.cache) {
|
|
398
|
+
if (this.isExpired(binding)) {
|
|
399
|
+
this.cache.delete(key);
|
|
400
|
+
removed++;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (removed > 0) {
|
|
404
|
+
this.markDirty();
|
|
405
|
+
log3.info("TTL cleanup completed", { removed, remaining: this.cache.size });
|
|
406
|
+
}
|
|
407
|
+
return removed;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Graceful shutdown: stop cleanup timer, flush pending writes.
|
|
411
|
+
*/
|
|
412
|
+
shutdown() {
|
|
413
|
+
if (this.cleanupTimer) {
|
|
414
|
+
clearInterval(this.cleanupTimer);
|
|
415
|
+
this.cleanupTimer = null;
|
|
416
|
+
}
|
|
417
|
+
if (this.flushTimeout) {
|
|
418
|
+
clearTimeout(this.flushTimeout);
|
|
419
|
+
this.flushTimeout = null;
|
|
420
|
+
}
|
|
421
|
+
this.flushToFile();
|
|
422
|
+
log3.info("Session store shut down", { bindings: this.cache.size });
|
|
423
|
+
}
|
|
424
|
+
/** For testing: return the number of cached bindings. */
|
|
425
|
+
get size() {
|
|
426
|
+
return this.cache.size;
|
|
427
|
+
}
|
|
428
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
429
|
+
makeKey(contextId, provider) {
|
|
430
|
+
return `${provider}:${contextId}`;
|
|
431
|
+
}
|
|
432
|
+
isExpired(binding) {
|
|
433
|
+
if (!binding.expiresAt) return false;
|
|
434
|
+
return Date.now() >= new Date(binding.expiresAt).getTime();
|
|
435
|
+
}
|
|
436
|
+
startCleanup(intervalMs) {
|
|
437
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), intervalMs);
|
|
438
|
+
this.cleanupTimer.unref();
|
|
439
|
+
}
|
|
440
|
+
// ── Persistence ───────────────────────────────────────────────────────
|
|
441
|
+
markDirty() {
|
|
442
|
+
if (!this.persist) return;
|
|
443
|
+
this.dirty = true;
|
|
444
|
+
if (!this.flushTimeout) {
|
|
445
|
+
this.flushTimeout = setTimeout(() => {
|
|
446
|
+
this.flushToFile();
|
|
447
|
+
}, 1e3);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
flushToFile() {
|
|
451
|
+
this.flushTimeout = null;
|
|
452
|
+
if (!this.persist || !this.dirty) return;
|
|
453
|
+
this.dirty = false;
|
|
454
|
+
this.writeAtomically();
|
|
455
|
+
}
|
|
456
|
+
writeAtomically() {
|
|
457
|
+
const tmpPath = this.storePath + ".tmp";
|
|
458
|
+
const data = {
|
|
459
|
+
version: 1,
|
|
460
|
+
bindings: Array.from(this.cache.values())
|
|
461
|
+
};
|
|
462
|
+
try {
|
|
463
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(this.storePath), { recursive: true });
|
|
464
|
+
(0, import_node_fs2.writeFileSync)(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
465
|
+
(0, import_node_fs2.renameSync)(tmpPath, this.storePath);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
log3.error("Failed to write session store", { error: err.message });
|
|
468
|
+
try {
|
|
469
|
+
(0, import_node_fs2.unlinkSync)(tmpPath);
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
loadFromFile() {
|
|
475
|
+
if (!this.persist || !(0, import_node_fs2.existsSync)(this.storePath)) return;
|
|
476
|
+
try {
|
|
477
|
+
const raw = (0, import_node_fs2.readFileSync)(this.storePath, "utf-8");
|
|
478
|
+
const data = JSON.parse(raw);
|
|
479
|
+
if (!data || !Array.isArray(data.bindings)) {
|
|
480
|
+
throw new Error("Invalid store format: bindings array missing");
|
|
481
|
+
}
|
|
482
|
+
let loaded = 0;
|
|
483
|
+
let expired = 0;
|
|
484
|
+
for (const b of data.bindings) {
|
|
485
|
+
if (!b.provider || !b.contextId || !b.sessionId) continue;
|
|
486
|
+
if (this.isExpired(b)) {
|
|
487
|
+
expired++;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const key = this.makeKey(b.contextId, b.provider);
|
|
491
|
+
this.cache.set(key, b);
|
|
492
|
+
loaded++;
|
|
493
|
+
}
|
|
494
|
+
log3.info("Session store loaded", { path: this.storePath, loaded, expired });
|
|
495
|
+
} catch (err) {
|
|
496
|
+
log3.error("Failed to load session store \u2014 starting fresh", {
|
|
497
|
+
path: this.storePath,
|
|
498
|
+
error: err.message
|
|
499
|
+
});
|
|
500
|
+
this.cache.clear();
|
|
501
|
+
this.writeAtomically();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
function generateContextId() {
|
|
506
|
+
return (0, import_node_crypto.randomUUID)();
|
|
316
507
|
}
|
|
317
508
|
|
|
318
509
|
// src/server/index.ts
|
|
319
|
-
var
|
|
510
|
+
var log4 = logger.child("server");
|
|
320
511
|
async function createA2AServer(config) {
|
|
321
512
|
const srv = config.server;
|
|
322
513
|
const port = srv.port ?? 3e3;
|
|
323
514
|
const hostname = srv.hostname ?? "0.0.0.0";
|
|
324
515
|
const advertiseHost = srv.advertiseHost ?? "localhost";
|
|
325
516
|
const advertiseProto = srv.advertiseProtocol ?? "http";
|
|
326
|
-
const
|
|
517
|
+
const sessionConf = config.session;
|
|
518
|
+
const sessionStore = new SessionBindingStore({
|
|
519
|
+
persist: sessionConf.persist ?? true,
|
|
520
|
+
storePath: resolveSessionStorePath({
|
|
521
|
+
configuredPath: sessionConf.storePath,
|
|
522
|
+
provider: config.provider,
|
|
523
|
+
port
|
|
524
|
+
}),
|
|
525
|
+
cleanupIntervalMs: sessionConf.cleanupInterval
|
|
526
|
+
});
|
|
527
|
+
const executor = createExecutor(config, sessionStore);
|
|
327
528
|
await executor.initialize();
|
|
328
529
|
const agentCard = buildAgentCard(config);
|
|
329
530
|
const taskStore = new import_server.InMemoryTaskStore();
|
|
@@ -355,7 +556,7 @@ async function createA2AServer(config) {
|
|
|
355
556
|
app.use("/a2a/jsonrpc", (0, import_express2.jsonRpcHandler)({ requestHandler, userBuilder: import_express2.UserBuilder.noAuthentication }));
|
|
356
557
|
app.use("/a2a/rest", (0, import_express2.restHandler)({ requestHandler, userBuilder: import_express2.UserBuilder.noAuthentication }));
|
|
357
558
|
const httpServer = app.listen(port, hostname, () => {
|
|
358
|
-
|
|
559
|
+
log4.info("A2A server started", { bind: hostname, port, proto: advertiseProto });
|
|
359
560
|
const banner = [
|
|
360
561
|
"",
|
|
361
562
|
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
@@ -379,20 +580,20 @@ async function createA2AServer(config) {
|
|
|
379
580
|
async shutdown() {
|
|
380
581
|
httpServer.close();
|
|
381
582
|
await executor.shutdown();
|
|
382
|
-
|
|
583
|
+
sessionStore.shutdown();
|
|
584
|
+
log4.info("Server shut down");
|
|
383
585
|
}
|
|
384
586
|
};
|
|
385
587
|
}
|
|
386
588
|
|
|
387
589
|
// src/executors/opencode/client.ts
|
|
388
590
|
var import_node_child_process = require("child_process");
|
|
389
|
-
var
|
|
591
|
+
var log5 = logger.child("opencode:client");
|
|
390
592
|
var OpenCodeClient = class {
|
|
391
593
|
constructor(config) {
|
|
392
594
|
this.config = config;
|
|
393
595
|
this.currentChild = null;
|
|
394
596
|
}
|
|
395
|
-
/** Abort the currently running child process (if any). */
|
|
396
597
|
abort() {
|
|
397
598
|
if (this.currentChild) {
|
|
398
599
|
this.currentChild.kill("SIGTERM");
|
|
@@ -405,14 +606,32 @@ var OpenCodeClient = class {
|
|
|
405
606
|
}
|
|
406
607
|
}
|
|
407
608
|
/**
|
|
408
|
-
* Execute a prompt via the OpenCode CLI
|
|
609
|
+
* Execute a prompt via the OpenCode CLI.
|
|
610
|
+
*
|
|
611
|
+
* @param prompt - The user prompt text.
|
|
612
|
+
* @param sessionId - If provided, continues the session with `--session`.
|
|
613
|
+
* Otherwise creates a new session using `--format json` to capture the
|
|
614
|
+
* session ID.
|
|
615
|
+
* @param workingDirectory - Optional per-request working directory override.
|
|
616
|
+
* Falls back to config.workdir if not provided.
|
|
409
617
|
*/
|
|
410
|
-
async execute(prompt) {
|
|
618
|
+
async execute(prompt, sessionId, workingDirectory) {
|
|
619
|
+
if (sessionId) {
|
|
620
|
+
const text = await this.runResume(prompt, sessionId, workingDirectory);
|
|
621
|
+
return { text, sessionId };
|
|
622
|
+
}
|
|
623
|
+
return this.runNew(prompt, workingDirectory);
|
|
624
|
+
}
|
|
625
|
+
resolveWorkdir(override) {
|
|
626
|
+
return override || this.config.workdir || process.cwd();
|
|
627
|
+
}
|
|
628
|
+
// ── New session (first turn) ──────────────────────────────────────────
|
|
629
|
+
async runNew(prompt, workingDirectory) {
|
|
411
630
|
const cliPath = this.config.cliPath || "opencode";
|
|
412
631
|
const timeout = this.config.timeout ?? 6e5;
|
|
413
632
|
const MAX_PROMPT = 1e5;
|
|
414
633
|
if (prompt.length > MAX_PROMPT) {
|
|
415
|
-
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT})
|
|
634
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);
|
|
416
635
|
}
|
|
417
636
|
const env = {
|
|
418
637
|
PATH: process.env.PATH ?? "",
|
|
@@ -424,7 +643,7 @@ var OpenCodeClient = class {
|
|
|
424
643
|
for (const key of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENCODE_SERVER_PASSWORD", "OPENCODE_SERVER_USERNAME"]) {
|
|
425
644
|
if (process.env[key]) env[key] = process.env[key];
|
|
426
645
|
}
|
|
427
|
-
const args = ["run",
|
|
646
|
+
const args = ["run", "--format", "json"];
|
|
428
647
|
if (this.config.model) {
|
|
429
648
|
args.push("--model", this.config.model);
|
|
430
649
|
}
|
|
@@ -434,12 +653,12 @@ var OpenCodeClient = class {
|
|
|
434
653
|
if (this.config.attachUrl) {
|
|
435
654
|
args.push("--attach", this.config.attachUrl);
|
|
436
655
|
}
|
|
437
|
-
args.push("--
|
|
438
|
-
|
|
439
|
-
|
|
656
|
+
args.push("--dangerously-skip-permissions", prompt);
|
|
657
|
+
const workdir = this.resolveWorkdir(workingDirectory);
|
|
658
|
+
log5.info("Spawning opencode (new session, json)", { cliPath, workdir });
|
|
440
659
|
return new Promise((resolve2, reject) => {
|
|
441
660
|
const child = (0, import_node_child_process.spawn)(cliPath, args, {
|
|
442
|
-
cwd:
|
|
661
|
+
cwd: workdir,
|
|
443
662
|
env,
|
|
444
663
|
stdio: ["ignore", "pipe", "pipe"]
|
|
445
664
|
});
|
|
@@ -466,7 +685,92 @@ var OpenCodeClient = class {
|
|
|
466
685
|
});
|
|
467
686
|
child.stderr?.on("data", (chunk) => {
|
|
468
687
|
stderr += chunk.toString();
|
|
469
|
-
|
|
688
|
+
log5.debug("OpenCode stderr", { text: chunk.toString().trim() });
|
|
689
|
+
});
|
|
690
|
+
child.on("close", (code) => {
|
|
691
|
+
clearTimeout(timer);
|
|
692
|
+
this.currentChild = null;
|
|
693
|
+
if (code !== 0) {
|
|
694
|
+
const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
|
|
695
|
+
log5.warn("OpenCode non-zero exit", { code, stderr: errMsg });
|
|
696
|
+
reject(new Error(errMsg));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
const { text, sessionId } = this.parseJsonEvents(stdout);
|
|
701
|
+
resolve2({ text, sessionId });
|
|
702
|
+
} catch (err) {
|
|
703
|
+
reject(new Error(`Failed to parse OpenCode JSON output: ${err.message}`));
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
child.on("error", (err) => {
|
|
707
|
+
clearTimeout(timer);
|
|
708
|
+
log5.error("OpenCode spawn failed", { error: err.message });
|
|
709
|
+
reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
// ── Resume session ────────────────────────────────────────────────────
|
|
714
|
+
async runResume(prompt, sessionId, workingDirectory) {
|
|
715
|
+
const cliPath = this.config.cliPath || "opencode";
|
|
716
|
+
const timeout = this.config.timeout ?? 6e5;
|
|
717
|
+
const MAX_PROMPT = 1e5;
|
|
718
|
+
if (prompt.length > MAX_PROMPT) {
|
|
719
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);
|
|
720
|
+
}
|
|
721
|
+
const env = {
|
|
722
|
+
PATH: process.env.PATH ?? "",
|
|
723
|
+
HOME: process.env.HOME ?? ""
|
|
724
|
+
};
|
|
725
|
+
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
|
|
726
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
727
|
+
}
|
|
728
|
+
for (const key of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENCODE_SERVER_PASSWORD", "OPENCODE_SERVER_USERNAME"]) {
|
|
729
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
730
|
+
}
|
|
731
|
+
const args = ["run", "--session", sessionId];
|
|
732
|
+
if (this.config.model) {
|
|
733
|
+
args.push("--model", this.config.model);
|
|
734
|
+
}
|
|
735
|
+
if (this.config.agent) {
|
|
736
|
+
args.push("--agent", this.config.agent);
|
|
737
|
+
}
|
|
738
|
+
if (this.config.attachUrl) {
|
|
739
|
+
args.push("--attach", this.config.attachUrl);
|
|
740
|
+
}
|
|
741
|
+
args.push("--format", "default", "--dangerously-skip-permissions", prompt);
|
|
742
|
+
const workdir = this.resolveWorkdir(workingDirectory);
|
|
743
|
+
log5.info("Spawning opencode (resume)", { sessionId, workdir });
|
|
744
|
+
return new Promise((resolve2, reject) => {
|
|
745
|
+
const child = (0, import_node_child_process.spawn)(cliPath, args, {
|
|
746
|
+
cwd: workdir,
|
|
747
|
+
env,
|
|
748
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
749
|
+
});
|
|
750
|
+
this.currentChild = child;
|
|
751
|
+
let stdout = "";
|
|
752
|
+
let stderr = "";
|
|
753
|
+
const MAX_OUTPUT = 1e7;
|
|
754
|
+
const timer = setTimeout(() => {
|
|
755
|
+
child.kill("SIGTERM");
|
|
756
|
+
const forceTimer = setTimeout(() => {
|
|
757
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
758
|
+
}, 5e3);
|
|
759
|
+
child.on("close", () => clearTimeout(forceTimer));
|
|
760
|
+
reject(new Error(`OpenCode execution timed out after ${timeout}ms`));
|
|
761
|
+
}, timeout);
|
|
762
|
+
child.stdout?.on("data", (chunk) => {
|
|
763
|
+
stdout += chunk.toString();
|
|
764
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
765
|
+
child.kill("SIGTERM");
|
|
766
|
+
setTimeout(() => {
|
|
767
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
768
|
+
}, 5e3);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
child.stderr?.on("data", (chunk) => {
|
|
772
|
+
stderr += chunk.toString();
|
|
773
|
+
log5.debug("OpenCode stderr", { text: chunk.toString().trim() });
|
|
470
774
|
});
|
|
471
775
|
child.on("close", (code) => {
|
|
472
776
|
clearTimeout(timer);
|
|
@@ -475,21 +779,65 @@ var OpenCodeClient = class {
|
|
|
475
779
|
resolve2(stdout.trim());
|
|
476
780
|
} else {
|
|
477
781
|
const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
|
|
478
|
-
|
|
782
|
+
log5.warn("OpenCode non-zero exit", { code, stderr: errMsg });
|
|
479
783
|
reject(new Error(errMsg));
|
|
480
784
|
}
|
|
481
785
|
});
|
|
482
786
|
child.on("error", (err) => {
|
|
483
787
|
clearTimeout(timer);
|
|
484
|
-
|
|
788
|
+
log5.error("OpenCode spawn failed", { error: err.message });
|
|
485
789
|
reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
|
|
486
790
|
});
|
|
487
791
|
});
|
|
488
792
|
}
|
|
793
|
+
// ── Parser ────────────────────────────────────────────────────────────
|
|
794
|
+
/**
|
|
795
|
+
* Parse newline-delimited JSON events from OpenCode's `--format json` output.
|
|
796
|
+
* Extracts the sessionID from the first event that contains it, and
|
|
797
|
+
* accumulates text content from relevant events.
|
|
798
|
+
*/
|
|
799
|
+
parseJsonEvents(stdout) {
|
|
800
|
+
const lines = stdout.split("\n").filter((l) => l.trim());
|
|
801
|
+
let sessionId = "";
|
|
802
|
+
const textParts = [];
|
|
803
|
+
for (const line of lines) {
|
|
804
|
+
let event;
|
|
805
|
+
try {
|
|
806
|
+
event = JSON.parse(line);
|
|
807
|
+
} catch {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (!sessionId && event.sessionID) {
|
|
811
|
+
sessionId = event.sessionID;
|
|
812
|
+
}
|
|
813
|
+
if (event.type === "error") continue;
|
|
814
|
+
const text = event.text || event.content || event.message;
|
|
815
|
+
if (text) {
|
|
816
|
+
textParts.push(text);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (!sessionId) {
|
|
820
|
+
return {
|
|
821
|
+
text: textParts.join("\n").trim() || stdout.trim(),
|
|
822
|
+
sessionId: ""
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
text: textParts.join("\n").trim() || "No response from OpenCode.",
|
|
827
|
+
sessionId
|
|
828
|
+
};
|
|
829
|
+
}
|
|
489
830
|
};
|
|
490
831
|
|
|
491
832
|
// src/executors/events.ts
|
|
492
833
|
var import_uuid2 = require("uuid");
|
|
834
|
+
function extractText(message) {
|
|
835
|
+
return message.parts.filter((p) => {
|
|
836
|
+
const part = p;
|
|
837
|
+
const text = part.text;
|
|
838
|
+
return text !== void 0 && text !== null;
|
|
839
|
+
}).map((p) => p.text).join("\n");
|
|
840
|
+
}
|
|
493
841
|
function publishTask(bus, taskId, contextId) {
|
|
494
842
|
bus.publish({
|
|
495
843
|
kind: "task",
|
|
@@ -526,12 +874,13 @@ function publishFinalArtifact(bus, taskId, contextId, text) {
|
|
|
526
874
|
}
|
|
527
875
|
|
|
528
876
|
// src/executors/opencode/executor.ts
|
|
529
|
-
var
|
|
877
|
+
var log6 = logger.child("opencode:executor");
|
|
530
878
|
var OpenCodeExecutor = class {
|
|
531
|
-
constructor(config) {
|
|
879
|
+
constructor(config, sessionStore) {
|
|
532
880
|
this.client = null;
|
|
533
881
|
this.initialized = false;
|
|
534
882
|
this.config = config;
|
|
883
|
+
this.sessionStore = sessionStore;
|
|
535
884
|
}
|
|
536
885
|
async initialize() {
|
|
537
886
|
if (this.initialized) return;
|
|
@@ -545,58 +894,77 @@ var OpenCodeExecutor = class {
|
|
|
545
894
|
timeout: this.config.timeouts.prompt ?? 6e5
|
|
546
895
|
});
|
|
547
896
|
this.initialized = true;
|
|
548
|
-
|
|
897
|
+
log6.info("Executor initialized", { workdir: oc.projectDirectory, model: oc.model });
|
|
549
898
|
}
|
|
550
899
|
async shutdown() {
|
|
551
900
|
this.client = null;
|
|
552
901
|
this.initialized = false;
|
|
553
|
-
|
|
902
|
+
log6.info("Executor shut down");
|
|
554
903
|
}
|
|
555
904
|
async execute(ctx, bus) {
|
|
556
|
-
const { taskId,
|
|
905
|
+
const { taskId, userMessage } = ctx;
|
|
557
906
|
await this.initialize();
|
|
907
|
+
const reuseByContext = this.config.session?.reuseByContext ?? true;
|
|
908
|
+
let contextId = ctx.contextId;
|
|
909
|
+
if (!contextId) {
|
|
910
|
+
contextId = generateContextId();
|
|
911
|
+
log6.info("Generated new contextId", { contextId, taskId });
|
|
912
|
+
}
|
|
913
|
+
const binding = reuseByContext ? this.sessionStore.get(contextId, "opencode") : null;
|
|
914
|
+
const sessionId = binding?.sessionId;
|
|
558
915
|
try {
|
|
559
|
-
|
|
916
|
+
const existingTask = ctx.task;
|
|
917
|
+
if (!existingTask) {
|
|
560
918
|
publishTask(bus, taskId, contextId);
|
|
561
919
|
publishStatus(bus, taskId, contextId, "submitted");
|
|
562
920
|
}
|
|
563
|
-
publishStatus(bus, taskId, contextId, "working"
|
|
564
|
-
const promptText =
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
921
|
+
publishStatus(bus, taskId, contextId, "working");
|
|
922
|
+
const promptText = extractText(userMessage);
|
|
923
|
+
const userMsg = userMessage;
|
|
924
|
+
const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
|
|
925
|
+
log6.info("Sending prompt to OpenCode", {
|
|
926
|
+
taskId,
|
|
927
|
+
contextId,
|
|
928
|
+
sessionId: sessionId || "(new)",
|
|
929
|
+
len: promptText.length,
|
|
930
|
+
workingDirectory: workingDirectory || "(default)"
|
|
931
|
+
});
|
|
932
|
+
const result = await this.client.execute(promptText, sessionId, workingDirectory);
|
|
933
|
+
this.sessionStore.set({
|
|
934
|
+
provider: "opencode",
|
|
935
|
+
contextId,
|
|
936
|
+
sessionId: result.sessionId,
|
|
937
|
+
activeTaskId: void 0,
|
|
938
|
+
activeTaskState: void 0,
|
|
939
|
+
createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
940
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
941
|
+
expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
|
|
942
|
+
});
|
|
943
|
+
publishFinalArtifact(bus, taskId, contextId, result.text || "No response from OpenCode.");
|
|
569
944
|
publishStatus(bus, taskId, contextId, "completed", void 0, true);
|
|
570
945
|
bus.finished();
|
|
571
|
-
|
|
946
|
+
log6.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
|
|
572
947
|
} catch (error) {
|
|
573
948
|
const msg = error.message ?? String(error);
|
|
574
|
-
|
|
949
|
+
log6.error("Execution failed", { taskId, contextId, error: msg });
|
|
575
950
|
publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
|
|
576
951
|
bus.finished();
|
|
577
952
|
}
|
|
578
953
|
}
|
|
579
954
|
async cancelTask(taskId, bus) {
|
|
580
|
-
|
|
955
|
+
log6.info("Cancel requested for OpenCode task", { taskId });
|
|
581
956
|
this.client?.abort();
|
|
582
957
|
publishStatus(bus, taskId, "", "canceled", "OpenCode task cancelled", true);
|
|
583
958
|
bus.finished();
|
|
584
959
|
}
|
|
585
|
-
extractText(message) {
|
|
586
|
-
return message.parts.filter((p) => {
|
|
587
|
-
const part = p;
|
|
588
|
-
const text = part.text;
|
|
589
|
-
return text !== void 0 && text !== null;
|
|
590
|
-
}).map((p) => p.text).join("\n");
|
|
591
|
-
}
|
|
592
960
|
};
|
|
593
|
-
function createOpenCodeExecutor(config) {
|
|
594
|
-
return new OpenCodeExecutor(config);
|
|
961
|
+
function createOpenCodeExecutor(config, sessionStore) {
|
|
962
|
+
return new OpenCodeExecutor(config, sessionStore);
|
|
595
963
|
}
|
|
596
964
|
|
|
597
965
|
// src/executors/codex/client.ts
|
|
598
966
|
var import_node_child_process2 = require("child_process");
|
|
599
|
-
var
|
|
967
|
+
var log7 = logger.child("codex:client");
|
|
600
968
|
var CodexClient = class {
|
|
601
969
|
constructor(config) {
|
|
602
970
|
this.config = config;
|
|
@@ -614,9 +982,24 @@ var CodexClient = class {
|
|
|
614
982
|
}
|
|
615
983
|
}
|
|
616
984
|
/**
|
|
617
|
-
* Execute a prompt via the Codex CLI
|
|
985
|
+
* Execute a prompt via the Codex CLI.
|
|
986
|
+
*
|
|
987
|
+
* @param prompt - The user prompt text.
|
|
988
|
+
* @param sessionId - If provided, resumes an existing session.
|
|
989
|
+
* Otherwise creates a new session and extracts the thread_id from JSONL.
|
|
990
|
+
* @param workingDirectory - Optional per-request working directory override.
|
|
618
991
|
*/
|
|
619
|
-
async execute(prompt) {
|
|
992
|
+
async execute(prompt, sessionId, workingDirectory) {
|
|
993
|
+
if (sessionId) {
|
|
994
|
+
return this.executeResume(prompt, sessionId, workingDirectory);
|
|
995
|
+
}
|
|
996
|
+
return this.executeNew(prompt, workingDirectory);
|
|
997
|
+
}
|
|
998
|
+
resolveWorkdir(override) {
|
|
999
|
+
return override || this.config.workdir || process.cwd();
|
|
1000
|
+
}
|
|
1001
|
+
// ── New session (first turn) ──────────────────────────────────────────
|
|
1002
|
+
async executeNew(prompt, workingDirectory) {
|
|
620
1003
|
const cliPath = this.config.cliPath || "codex";
|
|
621
1004
|
const timeout = this.config.timeout ?? 3e5;
|
|
622
1005
|
const MAX_PROMPT = 1e5;
|
|
@@ -633,15 +1016,20 @@ var CodexClient = class {
|
|
|
633
1016
|
if (this.config.apiKey) {
|
|
634
1017
|
env["OPENAI_API_KEY"] = this.config.apiKey;
|
|
635
1018
|
}
|
|
636
|
-
const args = [];
|
|
1019
|
+
const args = ["exec", "--json"];
|
|
637
1020
|
if (this.config.model) {
|
|
638
1021
|
args.push("--model", this.config.model);
|
|
639
1022
|
}
|
|
640
|
-
args.push(
|
|
641
|
-
|
|
1023
|
+
args.push(
|
|
1024
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1025
|
+
"--skip-git-repo-check",
|
|
1026
|
+
prompt
|
|
1027
|
+
);
|
|
1028
|
+
const workdir = this.resolveWorkdir(workingDirectory);
|
|
1029
|
+
log7.info("Spawning codex (new session)", { cliPath, workdir, model: this.config.model });
|
|
642
1030
|
return new Promise((resolve2, reject) => {
|
|
643
1031
|
const child = (0, import_node_child_process2.spawn)(cliPath, args, {
|
|
644
|
-
cwd:
|
|
1032
|
+
cwd: workdir,
|
|
645
1033
|
env,
|
|
646
1034
|
stdio: ["ignore", "pipe", "pipe"]
|
|
647
1035
|
});
|
|
@@ -668,35 +1056,192 @@ var CodexClient = class {
|
|
|
668
1056
|
});
|
|
669
1057
|
child.stderr?.on("data", (chunk) => {
|
|
670
1058
|
stderr += chunk.toString();
|
|
671
|
-
|
|
1059
|
+
log7.debug("Codex stderr", { text: chunk.toString().trim() });
|
|
672
1060
|
});
|
|
673
1061
|
child.on("close", (code) => {
|
|
674
1062
|
clearTimeout(timer);
|
|
675
1063
|
this.currentChild = null;
|
|
676
|
-
if (code
|
|
677
|
-
|
|
678
|
-
|
|
1064
|
+
if (code !== 0) {
|
|
1065
|
+
const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
|
|
1066
|
+
log7.warn("Codex non-zero exit", { code, stderr: errMsg });
|
|
1067
|
+
reject(new Error(errMsg));
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const { text, sessionId } = this.parseJsonl(stdout);
|
|
1072
|
+
resolve2({ text, sessionId });
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
reject(new Error(`Failed to parse Codex JSONL output: ${err.message}`));
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
child.on("error", (err) => {
|
|
1078
|
+
clearTimeout(timer);
|
|
1079
|
+
log7.error("Codex spawn failed", { error: err.message });
|
|
1080
|
+
reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
// ── Resume session (subsequent turns) ─────────────────────────────────
|
|
1085
|
+
async executeResume(prompt, sessionId, workingDirectory) {
|
|
1086
|
+
const cliPath = this.config.cliPath || "codex";
|
|
1087
|
+
const timeout = this.config.timeout ?? 3e5;
|
|
1088
|
+
const MAX_PROMPT = 1e5;
|
|
1089
|
+
if (prompt.length > MAX_PROMPT) {
|
|
1090
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
1091
|
+
}
|
|
1092
|
+
const env = {
|
|
1093
|
+
PATH: process.env.PATH ?? "",
|
|
1094
|
+
HOME: process.env.HOME ?? ""
|
|
1095
|
+
};
|
|
1096
|
+
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
|
|
1097
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
1098
|
+
}
|
|
1099
|
+
if (this.config.apiKey) {
|
|
1100
|
+
env["OPENAI_API_KEY"] = this.config.apiKey;
|
|
1101
|
+
}
|
|
1102
|
+
const args = ["exec", "resume", sessionId];
|
|
1103
|
+
if (this.config.model) {
|
|
1104
|
+
args.push("--model", this.config.model);
|
|
1105
|
+
}
|
|
1106
|
+
args.push(
|
|
1107
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1108
|
+
"--skip-git-repo-check",
|
|
1109
|
+
prompt
|
|
1110
|
+
);
|
|
1111
|
+
const workdir = this.resolveWorkdir(workingDirectory);
|
|
1112
|
+
log7.info("Spawning codex (resume)", { sessionId, workdir });
|
|
1113
|
+
return new Promise((resolve2, reject) => {
|
|
1114
|
+
const child = (0, import_node_child_process2.spawn)(cliPath, args, {
|
|
1115
|
+
cwd: workdir,
|
|
1116
|
+
env,
|
|
1117
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1118
|
+
});
|
|
1119
|
+
this.currentChild = child;
|
|
1120
|
+
let stdout = "";
|
|
1121
|
+
let stderr = "";
|
|
1122
|
+
const MAX_OUTPUT = 1e7;
|
|
1123
|
+
const timer = setTimeout(() => {
|
|
1124
|
+
child.kill("SIGTERM");
|
|
1125
|
+
const forceTimer = setTimeout(() => {
|
|
1126
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1127
|
+
}, 5e3);
|
|
1128
|
+
child.on("close", () => clearTimeout(forceTimer));
|
|
1129
|
+
reject(new Error(`Codex execution timed out after ${timeout}ms`));
|
|
1130
|
+
}, timeout);
|
|
1131
|
+
child.stdout?.on("data", (chunk) => {
|
|
1132
|
+
stdout += chunk.toString();
|
|
1133
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
1134
|
+
child.kill("SIGTERM");
|
|
1135
|
+
setTimeout(() => {
|
|
1136
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
1137
|
+
}, 5e3);
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
child.stderr?.on("data", (chunk) => {
|
|
1141
|
+
stderr += chunk.toString();
|
|
1142
|
+
log7.debug("Codex stderr", { text: chunk.toString().trim() });
|
|
1143
|
+
});
|
|
1144
|
+
child.on("close", (code) => {
|
|
1145
|
+
clearTimeout(timer);
|
|
1146
|
+
this.currentChild = null;
|
|
1147
|
+
if (code !== 0) {
|
|
679
1148
|
const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
|
|
680
|
-
|
|
1149
|
+
log7.warn("Codex non-zero exit", { code, stderr: errMsg });
|
|
681
1150
|
reject(new Error(errMsg));
|
|
1151
|
+
return;
|
|
682
1152
|
}
|
|
1153
|
+
const text = this.extractResumeText(stdout);
|
|
1154
|
+
resolve2({ text, sessionId });
|
|
683
1155
|
});
|
|
684
1156
|
child.on("error", (err) => {
|
|
685
1157
|
clearTimeout(timer);
|
|
686
|
-
|
|
1158
|
+
log7.error("Codex spawn failed", { error: err.message });
|
|
687
1159
|
reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
|
|
688
1160
|
});
|
|
689
1161
|
});
|
|
690
1162
|
}
|
|
1163
|
+
// ── Parsers ───────────────────────────────────────────────────────────
|
|
1164
|
+
/**
|
|
1165
|
+
* Parse JSONL output to extract session thread_id and agent response text.
|
|
1166
|
+
*/
|
|
1167
|
+
parseJsonl(stdout) {
|
|
1168
|
+
const lines = stdout.split("\n").filter((l) => l.trim());
|
|
1169
|
+
let sessionId = "";
|
|
1170
|
+
const textParts = [];
|
|
1171
|
+
for (const line of lines) {
|
|
1172
|
+
let event;
|
|
1173
|
+
try {
|
|
1174
|
+
event = JSON.parse(line);
|
|
1175
|
+
} catch {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (event.type === "thread.started" && event.thread_id) {
|
|
1179
|
+
sessionId = event.thread_id;
|
|
1180
|
+
}
|
|
1181
|
+
if (event.type === "item.completed") {
|
|
1182
|
+
const item = event.item;
|
|
1183
|
+
if (item && item.type === "agent_message" && item.text) {
|
|
1184
|
+
textParts.push(item.text);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (!sessionId) {
|
|
1189
|
+
throw new Error("No thread_id found in Codex JSONL output");
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
text: textParts.join("\n").trim() || "No response from Codex.",
|
|
1193
|
+
sessionId
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Extract the agent response text from the resume mode output.
|
|
1198
|
+
*
|
|
1199
|
+
* The resume output includes a CLI header followed by the conversation.
|
|
1200
|
+
* We extract everything after the last "codex" marker line as the
|
|
1201
|
+
* latest agent response.
|
|
1202
|
+
*/
|
|
1203
|
+
extractResumeText(stdout) {
|
|
1204
|
+
const lines = stdout.split("\n");
|
|
1205
|
+
let responseStart = -1;
|
|
1206
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1207
|
+
if (lines[i].trim() === "codex") {
|
|
1208
|
+
responseStart = i;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (responseStart >= 0 && responseStart + 1 < lines.length) {
|
|
1212
|
+
return lines.slice(responseStart + 1).join("\n").trim();
|
|
1213
|
+
}
|
|
1214
|
+
let skipHeader = true;
|
|
1215
|
+
const resultLines = [];
|
|
1216
|
+
for (const line of lines) {
|
|
1217
|
+
const trimmed = line.trim();
|
|
1218
|
+
if (skipHeader) {
|
|
1219
|
+
if (trimmed === "--------" || trimmed === "codex") {
|
|
1220
|
+
skipHeader = false;
|
|
1221
|
+
}
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if (trimmed === "user" || trimmed === "codex") {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (trimmed.startsWith("tokens used")) {
|
|
1228
|
+
if (resultLines.length > 0) resultLines.pop();
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
resultLines.push(line);
|
|
1232
|
+
}
|
|
1233
|
+
return resultLines.join("\n").trim() || stdout.trim();
|
|
1234
|
+
}
|
|
691
1235
|
};
|
|
692
1236
|
|
|
693
1237
|
// src/executors/codex/executor.ts
|
|
694
|
-
var
|
|
1238
|
+
var log8 = logger.child("codex:executor");
|
|
695
1239
|
var CodexExecutor = class {
|
|
696
|
-
constructor(config) {
|
|
1240
|
+
constructor(config, sessionStore) {
|
|
697
1241
|
this.client = null;
|
|
698
1242
|
this.initialized = false;
|
|
699
1243
|
this.config = config;
|
|
1244
|
+
this.sessionStore = sessionStore;
|
|
700
1245
|
}
|
|
701
1246
|
async initialize() {
|
|
702
1247
|
if (this.initialized) return;
|
|
@@ -709,59 +1254,78 @@ var CodexExecutor = class {
|
|
|
709
1254
|
timeout: this.config.timeouts.prompt ?? 6e5
|
|
710
1255
|
});
|
|
711
1256
|
this.initialized = true;
|
|
712
|
-
|
|
1257
|
+
log8.info("Executor initialized", { cliPath: cx.cliPath, workdir: cx.workdir });
|
|
713
1258
|
}
|
|
714
1259
|
async shutdown() {
|
|
715
1260
|
this.client = null;
|
|
716
1261
|
this.initialized = false;
|
|
717
|
-
|
|
1262
|
+
log8.info("Executor shut down");
|
|
718
1263
|
}
|
|
719
1264
|
async execute(ctx, bus) {
|
|
720
|
-
const { taskId,
|
|
1265
|
+
const { taskId, userMessage } = ctx;
|
|
721
1266
|
await this.initialize();
|
|
1267
|
+
const reuseByContext = this.config.session?.reuseByContext ?? true;
|
|
1268
|
+
let contextId = ctx.contextId;
|
|
1269
|
+
if (!contextId) {
|
|
1270
|
+
contextId = generateContextId();
|
|
1271
|
+
log8.info("Generated new contextId", { contextId, taskId });
|
|
1272
|
+
}
|
|
1273
|
+
const binding = reuseByContext ? this.sessionStore.get(contextId, "codex") : null;
|
|
1274
|
+
const sessionId = binding?.sessionId;
|
|
722
1275
|
try {
|
|
723
|
-
|
|
1276
|
+
const existingTask = ctx.task;
|
|
1277
|
+
if (!existingTask) {
|
|
724
1278
|
publishTask(bus, taskId, contextId);
|
|
725
1279
|
publishStatus(bus, taskId, contextId, "submitted");
|
|
726
1280
|
}
|
|
727
|
-
publishStatus(bus, taskId, contextId, "working"
|
|
728
|
-
const promptText =
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
1281
|
+
publishStatus(bus, taskId, contextId, "working");
|
|
1282
|
+
const promptText = extractText(userMessage);
|
|
1283
|
+
const userMsg = userMessage;
|
|
1284
|
+
const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
|
|
1285
|
+
log8.info("Sending prompt to Codex", {
|
|
1286
|
+
taskId,
|
|
1287
|
+
contextId,
|
|
1288
|
+
sessionId: sessionId || "(new)",
|
|
1289
|
+
len: promptText.length,
|
|
1290
|
+
workingDirectory: workingDirectory || "(default)"
|
|
1291
|
+
});
|
|
1292
|
+
const result = await this.client.execute(promptText, sessionId, workingDirectory);
|
|
1293
|
+
this.sessionStore.set({
|
|
1294
|
+
provider: "codex",
|
|
1295
|
+
contextId,
|
|
1296
|
+
sessionId: result.sessionId,
|
|
1297
|
+
activeTaskId: void 0,
|
|
1298
|
+
activeTaskState: void 0,
|
|
1299
|
+
createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1300
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1301
|
+
expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
|
|
1302
|
+
});
|
|
1303
|
+
publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Codex.");
|
|
733
1304
|
publishStatus(bus, taskId, contextId, "completed", void 0, true);
|
|
734
1305
|
bus.finished();
|
|
735
|
-
|
|
1306
|
+
log8.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
|
|
736
1307
|
} catch (error) {
|
|
737
1308
|
const msg = error.message ?? String(error);
|
|
738
|
-
|
|
1309
|
+
log8.error("Execution failed", { taskId, contextId, error: msg });
|
|
739
1310
|
publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
|
|
740
1311
|
bus.finished();
|
|
741
1312
|
}
|
|
742
1313
|
}
|
|
743
1314
|
async cancelTask(taskId, bus) {
|
|
744
|
-
|
|
1315
|
+
log8.info("Cancel requested for Codex task", { taskId });
|
|
745
1316
|
this.client?.abort();
|
|
746
1317
|
publishStatus(bus, taskId, "", "canceled", "Codex task cancelled", true);
|
|
747
1318
|
bus.finished();
|
|
748
1319
|
}
|
|
749
|
-
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
750
|
-
extractText(message) {
|
|
751
|
-
return message.parts.filter((p) => {
|
|
752
|
-
const part = p;
|
|
753
|
-
const text = part.text;
|
|
754
|
-
return text !== void 0 && text !== null;
|
|
755
|
-
}).map((p) => p.text).join("\n");
|
|
756
|
-
}
|
|
757
1320
|
};
|
|
758
|
-
function createCodexExecutor(config) {
|
|
759
|
-
return new CodexExecutor(config);
|
|
1321
|
+
function createCodexExecutor(config, sessionStore) {
|
|
1322
|
+
return new CodexExecutor(config, sessionStore);
|
|
760
1323
|
}
|
|
761
1324
|
|
|
762
1325
|
// src/executors/claude/client.ts
|
|
763
1326
|
var import_node_child_process3 = require("child_process");
|
|
764
|
-
var
|
|
1327
|
+
var import_uuid3 = require("uuid");
|
|
1328
|
+
var log9 = logger.child("claude:client");
|
|
765
1329
|
var ClaudeClient = class {
|
|
766
1330
|
constructor(config) {
|
|
767
1331
|
this.config = config;
|
|
@@ -779,14 +1343,31 @@ var ClaudeClient = class {
|
|
|
779
1343
|
}
|
|
780
1344
|
}
|
|
781
1345
|
/**
|
|
782
|
-
* Execute a prompt via the Claude Code CLI
|
|
1346
|
+
* Execute a prompt via the Claude Code CLI.
|
|
1347
|
+
*
|
|
1348
|
+
* @param prompt - The user prompt text.
|
|
1349
|
+
* @param sessionId - If provided, resumes the session with `--resume`.
|
|
1350
|
+
* Otherwise creates a new session with `--session-id <new-uuid>`.
|
|
1351
|
+
* @param workingDirectory - Optional per-request working directory override.
|
|
1352
|
+
* @returns The response text and the session ID (new or existing).
|
|
783
1353
|
*/
|
|
784
|
-
async execute(prompt) {
|
|
1354
|
+
async execute(prompt, sessionId, workingDirectory) {
|
|
1355
|
+
if (sessionId) {
|
|
1356
|
+
const text2 = await this.run(["-p", "--resume", sessionId, prompt], workingDirectory);
|
|
1357
|
+
return { text: text2, sessionId };
|
|
1358
|
+
}
|
|
1359
|
+
const newSessionId = (0, import_uuid3.v4)();
|
|
1360
|
+
const text = await this.run(["-p", "--session-id", newSessionId, prompt], workingDirectory);
|
|
1361
|
+
return { text, sessionId: newSessionId };
|
|
1362
|
+
}
|
|
1363
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
1364
|
+
async run(args, workingDirectory) {
|
|
785
1365
|
const cliPath = this.config.cliPath || "claude";
|
|
786
1366
|
const timeout = this.config.timeout ?? 3e5;
|
|
787
1367
|
const MAX_PROMPT = 1e5;
|
|
788
|
-
|
|
789
|
-
|
|
1368
|
+
const promptArg = args[args.length - 1];
|
|
1369
|
+
if (promptArg && promptArg.length > MAX_PROMPT) {
|
|
1370
|
+
throw new Error(`Prompt too large (${promptArg.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
790
1371
|
}
|
|
791
1372
|
const env = {
|
|
792
1373
|
PATH: process.env.PATH ?? "",
|
|
@@ -798,15 +1379,19 @@ var ClaudeClient = class {
|
|
|
798
1379
|
if (this.config.apiKey) {
|
|
799
1380
|
env["ANTHROPIC_API_KEY"] = this.config.apiKey;
|
|
800
1381
|
}
|
|
801
|
-
const
|
|
1382
|
+
const fullArgs = [];
|
|
1383
|
+
for (const arg of args.slice(0, -1)) {
|
|
1384
|
+
fullArgs.push(arg);
|
|
1385
|
+
}
|
|
802
1386
|
if (this.config.model) {
|
|
803
|
-
|
|
1387
|
+
fullArgs.push("--model", this.config.model);
|
|
804
1388
|
}
|
|
805
|
-
|
|
806
|
-
|
|
1389
|
+
fullArgs.push(promptArg);
|
|
1390
|
+
const workdir = workingDirectory || this.config.workdir || process.cwd();
|
|
1391
|
+
log9.info("Spawning claude", { cliPath, workdir, model: this.config.model });
|
|
807
1392
|
return new Promise((resolve2, reject) => {
|
|
808
|
-
const child = (0, import_node_child_process3.spawn)(cliPath,
|
|
809
|
-
cwd:
|
|
1393
|
+
const child = (0, import_node_child_process3.spawn)(cliPath, fullArgs, {
|
|
1394
|
+
cwd: workdir,
|
|
810
1395
|
env,
|
|
811
1396
|
stdio: ["ignore", "pipe", "pipe"]
|
|
812
1397
|
});
|
|
@@ -833,7 +1418,7 @@ var ClaudeClient = class {
|
|
|
833
1418
|
});
|
|
834
1419
|
child.stderr?.on("data", (chunk) => {
|
|
835
1420
|
stderr += chunk.toString();
|
|
836
|
-
|
|
1421
|
+
log9.debug("Claude stderr", { text: chunk.toString().trim() });
|
|
837
1422
|
});
|
|
838
1423
|
child.on("close", (code) => {
|
|
839
1424
|
clearTimeout(timer);
|
|
@@ -842,13 +1427,13 @@ var ClaudeClient = class {
|
|
|
842
1427
|
resolve2(stdout.trim());
|
|
843
1428
|
} else {
|
|
844
1429
|
const errMsg = stderr.trim() || stdout.trim() || `Claude Code exited with code ${code}`;
|
|
845
|
-
|
|
1430
|
+
log9.warn("Claude non-zero exit", { code, stderr: errMsg });
|
|
846
1431
|
reject(new Error(errMsg));
|
|
847
1432
|
}
|
|
848
1433
|
});
|
|
849
1434
|
child.on("error", (err) => {
|
|
850
1435
|
clearTimeout(timer);
|
|
851
|
-
|
|
1436
|
+
log9.error("Claude spawn failed", { error: err.message });
|
|
852
1437
|
reject(new Error(`Failed to start claude: ${err.message}. Install: npm i -g @anthropic-ai/claude-code`));
|
|
853
1438
|
});
|
|
854
1439
|
});
|
|
@@ -856,12 +1441,13 @@ var ClaudeClient = class {
|
|
|
856
1441
|
};
|
|
857
1442
|
|
|
858
1443
|
// src/executors/claude/executor.ts
|
|
859
|
-
var
|
|
1444
|
+
var log10 = logger.child("claude:executor");
|
|
860
1445
|
var ClaudeExecutor = class {
|
|
861
|
-
constructor(config) {
|
|
1446
|
+
constructor(config, sessionStore) {
|
|
862
1447
|
this.client = null;
|
|
863
1448
|
this.initialized = false;
|
|
864
1449
|
this.config = config;
|
|
1450
|
+
this.sessionStore = sessionStore;
|
|
865
1451
|
}
|
|
866
1452
|
async initialize() {
|
|
867
1453
|
if (this.initialized) return;
|
|
@@ -874,53 +1460,72 @@ var ClaudeExecutor = class {
|
|
|
874
1460
|
timeout: this.config.timeouts.prompt ?? 6e5
|
|
875
1461
|
});
|
|
876
1462
|
this.initialized = true;
|
|
877
|
-
|
|
1463
|
+
log10.info("Executor initialized", { cliPath: cc.cliPath, workdir: cc.workdir });
|
|
878
1464
|
}
|
|
879
1465
|
async shutdown() {
|
|
880
1466
|
this.client = null;
|
|
881
1467
|
this.initialized = false;
|
|
882
|
-
|
|
1468
|
+
log10.info("Executor shut down");
|
|
883
1469
|
}
|
|
884
1470
|
async execute(ctx, bus) {
|
|
885
|
-
const { taskId,
|
|
1471
|
+
const { taskId, userMessage } = ctx;
|
|
886
1472
|
await this.initialize();
|
|
1473
|
+
const reuseByContext = this.config.session?.reuseByContext ?? true;
|
|
1474
|
+
let contextId = ctx.contextId;
|
|
1475
|
+
if (!contextId) {
|
|
1476
|
+
contextId = generateContextId();
|
|
1477
|
+
log10.info("Generated new contextId", { contextId, taskId });
|
|
1478
|
+
}
|
|
1479
|
+
const binding = reuseByContext ? this.sessionStore.get(contextId, "claude") : null;
|
|
1480
|
+
const sessionId = binding?.sessionId;
|
|
887
1481
|
try {
|
|
888
|
-
|
|
1482
|
+
const existingTask = ctx.task;
|
|
1483
|
+
if (!existingTask) {
|
|
889
1484
|
publishTask(bus, taskId, contextId);
|
|
890
1485
|
publishStatus(bus, taskId, contextId, "submitted");
|
|
891
1486
|
}
|
|
892
|
-
publishStatus(bus, taskId, contextId, "working"
|
|
893
|
-
const promptText =
|
|
894
|
-
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
1487
|
+
publishStatus(bus, taskId, contextId, "working");
|
|
1488
|
+
const promptText = extractText(userMessage);
|
|
1489
|
+
const userMsg = userMessage;
|
|
1490
|
+
const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
|
|
1491
|
+
log10.info("Sending prompt to Claude Code", {
|
|
1492
|
+
taskId,
|
|
1493
|
+
contextId,
|
|
1494
|
+
sessionId: sessionId || "(new)",
|
|
1495
|
+
len: promptText.length,
|
|
1496
|
+
workingDirectory: workingDirectory || "(default)"
|
|
1497
|
+
});
|
|
1498
|
+
const result = await this.client.execute(promptText, sessionId, workingDirectory);
|
|
1499
|
+
this.sessionStore.set({
|
|
1500
|
+
provider: "claude",
|
|
1501
|
+
contextId,
|
|
1502
|
+
sessionId: result.sessionId,
|
|
1503
|
+
activeTaskId: void 0,
|
|
1504
|
+
activeTaskState: void 0,
|
|
1505
|
+
createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1506
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1507
|
+
expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
|
|
1508
|
+
});
|
|
1509
|
+
publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Claude Code.");
|
|
898
1510
|
publishStatus(bus, taskId, contextId, "completed", void 0, true);
|
|
899
1511
|
bus.finished();
|
|
900
|
-
|
|
1512
|
+
log10.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
|
|
901
1513
|
} catch (error) {
|
|
902
1514
|
const msg = error.message ?? String(error);
|
|
903
|
-
|
|
1515
|
+
log10.error("Execution failed", { taskId, contextId, error: msg });
|
|
904
1516
|
publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
|
|
905
1517
|
bus.finished();
|
|
906
1518
|
}
|
|
907
1519
|
}
|
|
908
1520
|
async cancelTask(taskId, bus) {
|
|
909
|
-
|
|
1521
|
+
log10.info("Cancel requested for Claude Code task", { taskId });
|
|
910
1522
|
this.client?.abort();
|
|
911
1523
|
publishStatus(bus, taskId, "", "canceled", "Claude Code task cancelled", true);
|
|
912
1524
|
bus.finished();
|
|
913
1525
|
}
|
|
914
|
-
extractText(message) {
|
|
915
|
-
return message.parts.filter((p) => {
|
|
916
|
-
const part = p;
|
|
917
|
-
const text = part.text;
|
|
918
|
-
return text !== void 0 && text !== null;
|
|
919
|
-
}).map((p) => p.text).join("\n");
|
|
920
|
-
}
|
|
921
1526
|
};
|
|
922
|
-
function createClaudeExecutor(config) {
|
|
923
|
-
return new ClaudeExecutor(config);
|
|
1527
|
+
function createClaudeExecutor(config, sessionStore) {
|
|
1528
|
+
return new ClaudeExecutor(config, sessionStore);
|
|
924
1529
|
}
|
|
925
1530
|
// Annotate the CommonJS export names for ESM import in node:
|
|
926
1531
|
0 && (module.exports = {
|
|
@@ -928,15 +1533,18 @@ function createClaudeExecutor(config) {
|
|
|
928
1533
|
CodexExecutor,
|
|
929
1534
|
LogLevel,
|
|
930
1535
|
OpenCodeExecutor,
|
|
1536
|
+
SessionBindingStore,
|
|
931
1537
|
createA2AServer,
|
|
932
1538
|
createClaudeExecutor,
|
|
933
1539
|
createCodexExecutor,
|
|
934
1540
|
createExecutor,
|
|
935
1541
|
createOpenCodeExecutor,
|
|
1542
|
+
generateContextId,
|
|
936
1543
|
getExecutor,
|
|
937
1544
|
loadConfigFile,
|
|
938
1545
|
logger,
|
|
939
1546
|
registerExecutor,
|
|
940
|
-
resolveConfig
|
|
1547
|
+
resolveConfig,
|
|
1548
|
+
resolveSessionStorePath
|
|
941
1549
|
});
|
|
942
1550
|
//# sourceMappingURL=index.js.map
|