@axiom-lattice/cli-a2a 0.1.4 → 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.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +34 -32
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +85 -2
  4. package/__tests__/opencode-executor.test.ts +631 -31
  5. package/__tests__/session-store-path.test.ts +40 -0
  6. package/dist/{chunk-NQIDRU47.mjs → chunk-54SKDK3D.mjs} +78 -32
  7. package/dist/chunk-54SKDK3D.mjs.map +1 -0
  8. package/dist/chunk-BSNQOP2W.mjs +350 -0
  9. package/dist/chunk-BSNQOP2W.mjs.map +1 -0
  10. package/dist/chunk-DRFFQJII.mjs +195 -0
  11. package/dist/chunk-DRFFQJII.mjs.map +1 -0
  12. package/dist/chunk-URJDRXJW.mjs +378 -0
  13. package/dist/chunk-URJDRXJW.mjs.map +1 -0
  14. package/dist/{chunk-G7AGL2QA.mjs → chunk-WZB7FBSX.mjs} +23 -5
  15. package/dist/chunk-WZB7FBSX.mjs.map +1 -0
  16. package/dist/{chunk-LXL47XMZ.mjs → chunk-YGUPSPZO.mjs} +9 -1
  17. package/dist/chunk-YGUPSPZO.mjs.map +1 -0
  18. package/dist/{chunk-35NFMGMS.mjs → chunk-ZP3JNJQJ.mjs} +3 -3
  19. package/dist/chunk-ZP3JNJQJ.mjs.map +1 -0
  20. package/dist/cli.js +769 -156
  21. package/dist/cli.js.map +1 -1
  22. package/dist/cli.mjs +7 -6
  23. package/dist/cli.mjs.map +1 -1
  24. package/dist/executor-LLE5VEFJ.mjs +12 -0
  25. package/dist/executor-LO7P3KUW.mjs +12 -0
  26. package/dist/{executor-RVBGAWUF.mjs → executor-OOLGGXHS.mjs} +4 -3
  27. package/dist/{executors-QIIKBUMJ.mjs → executors-AQHOPGSR.mjs} +2 -2
  28. package/dist/index.d.mts +92 -21
  29. package/dist/index.d.ts +92 -21
  30. package/dist/index.js +731 -123
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +15 -7
  33. package/package.json +11 -4
  34. package/runtime/cli-path.cjs +3 -0
  35. package/runtime/cli-path.d.ts +1 -0
  36. package/runtime/cli-path.mjs +5 -0
  37. package/src/config/defaults.ts +3 -1
  38. package/src/config/types.ts +8 -3
  39. package/src/executors/claude/client.ts +44 -14
  40. package/src/executors/claude/executor.ts +65 -30
  41. package/src/executors/codex/client.ts +239 -16
  42. package/src/executors/codex/executor.ts +70 -37
  43. package/src/executors/events.ts +15 -1
  44. package/src/executors/index.ts +13 -6
  45. package/src/executors/opencode/client.ts +192 -18
  46. package/src/executors/opencode/executor.ts +65 -30
  47. package/src/index.ts +8 -0
  48. package/src/local-state.ts +53 -0
  49. package/src/server/agent-card.ts +1 -0
  50. package/src/server/index.ts +22 -7
  51. package/src/session-store.ts +262 -0
  52. package/dist/chunk-35NFMGMS.mjs.map +0 -1
  53. package/dist/chunk-G7AGL2QA.mjs.map +0 -1
  54. package/dist/chunk-LXL47XMZ.mjs.map +0 -1
  55. package/dist/chunk-NQIDRU47.mjs.map +0 -1
  56. package/dist/chunk-VSZ3DACI.mjs +0 -179
  57. package/dist/chunk-VSZ3DACI.mjs.map +0 -1
  58. package/dist/chunk-WNCDOYZS.mjs +0 -187
  59. package/dist/chunk-WNCDOYZS.mjs.map +0 -1
  60. package/dist/executor-OWPVDUXH.mjs +0 -11
  61. package/dist/executor-XWHWUVQ3.mjs +0 -11
  62. /package/dist/{executor-OWPVDUXH.mjs.map → executor-LLE5VEFJ.mjs.map} +0 -0
  63. /package/dist/{executor-RVBGAWUF.mjs.map → executor-LO7P3KUW.mjs.map} +0 -0
  64. /package/dist/{executor-XWHWUVQ3.mjs.map → executor-OOLGGXHS.mjs.map} +0 -0
  65. /package/dist/{executors-QIIKBUMJ.mjs.map → executors-AQHOPGSR.mjs.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/executors/opencode/client.ts","../src/executors/opencode/executor.ts"],"sourcesContent":["/**\n * OpenCode CLI client wrapper.\n *\n * Spawns the OpenCode CLI as a child process. For the first turn, uses\n * `--format json` to discover the session ID, then extracts the response\n * text. For subsequent turns, uses `--session <id>` to continue the\n * conversation with plain text output.\n */\n\nimport { spawn } from 'node:child_process';\nimport { logger } from '../../logger.js';\n\nconst log = logger.child('opencode:client');\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface OpenCodeClientConfig {\n /** Path to the opencode binary (default: \"opencode\") */\n cliPath: string;\n /** Working directory for the process */\n workdir: string;\n /** Model identifier (e.g. \"anthropic/claude-sonnet-4\") */\n model?: string;\n /** Agent preset name */\n agent?: string;\n /** Optional URL of a running OpenCode server to attach to */\n attachUrl?: string;\n /** Timeout in ms (default: 600_000 = 10 min) */\n timeout?: number;\n}\n\nexport interface OpenCodeExecuteResult {\n text: string;\n sessionId: string;\n}\n\n// ─── Client ─────────────────────────────────────────────────────────────────\n\nexport class OpenCodeClient {\n private currentChild: ReturnType<typeof spawn> | null = null;\n\n constructor(private config: OpenCodeClientConfig) {}\n\n abort(): void {\n if (this.currentChild) {\n this.currentChild.kill('SIGTERM');\n setTimeout(() => {\n if (this.currentChild?.exitCode === null) {\n this.currentChild.kill('SIGKILL');\n }\n }, 5000);\n this.currentChild = null;\n }\n }\n\n /**\n * Execute a prompt via the OpenCode CLI.\n *\n * @param prompt - The user prompt text.\n * @param sessionId - If provided, continues the session with `--session`.\n * Otherwise creates a new session using `--format json` to capture the\n * session ID.\n * @param workingDirectory - Optional per-request working directory override.\n * Falls back to config.workdir if not provided.\n */\n async execute(prompt: string, sessionId?: string, workingDirectory?: string): Promise<OpenCodeExecuteResult> {\n if (sessionId) {\n const text = await this.runResume(prompt, sessionId, workingDirectory);\n return { text, sessionId };\n }\n return this.runNew(prompt, workingDirectory);\n }\n\n private resolveWorkdir(override?: string): string {\n return override || this.config.workdir || process.cwd();\n }\n\n // ── New session (first turn) ──────────────────────────────────────────\n\n private async runNew(prompt: string, workingDirectory?: string): Promise<OpenCodeExecuteResult> {\n const cliPath = this.config.cliPath || 'opencode';\n const timeout = this.config.timeout ?? 600_000;\n const MAX_PROMPT = 100_000;\n\n if (prompt.length > MAX_PROMPT) {\n throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);\n }\n\n const env: Record<string, string> = {\n PATH: process.env.PATH ?? '',\n HOME: process.env.HOME ?? '',\n };\n for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {\n if (process.env[key]) env[key] = process.env[key];\n }\n for (const key of ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENCODE_SERVER_PASSWORD', 'OPENCODE_SERVER_USERNAME']) {\n if (process.env[key]) env[key] = process.env[key];\n }\n\n const args: string[] = ['run', '--format', 'json'];\n if (this.config.model) {\n args.push('--model', this.config.model);\n }\n if (this.config.agent) {\n args.push('--agent', this.config.agent);\n }\n if (this.config.attachUrl) {\n args.push('--attach', this.config.attachUrl);\n }\n args.push('--dangerously-skip-permissions', prompt);\n\n const workdir = this.resolveWorkdir(workingDirectory);\n log.info('Spawning opencode (new session, json)', { cliPath, workdir });\n\n return new Promise<OpenCodeExecuteResult>((resolve, reject) => {\n const child = spawn(cliPath, args, {\n cwd: workdir,\n env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n this.currentChild = child;\n\n let stdout = '';\n let stderr = '';\n const MAX_OUTPUT = 10_000_000;\n\n const timer = setTimeout(() => {\n child.kill('SIGTERM');\n const forceTimer = setTimeout(() => {\n if (child.exitCode === null) child.kill('SIGKILL');\n }, 5000);\n child.on('close', () => clearTimeout(forceTimer));\n reject(new Error(`OpenCode execution timed out after ${timeout}ms`));\n }, timeout);\n\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout += chunk.toString();\n if (stdout.length > MAX_OUTPUT) {\n child.kill('SIGTERM');\n setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 5000);\n }\n });\n\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString();\n log.debug('OpenCode stderr', { text: chunk.toString().trim() });\n });\n\n child.on('close', (code) => {\n clearTimeout(timer);\n this.currentChild = null;\n\n if (code !== 0) {\n const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;\n log.warn('OpenCode non-zero exit', { code, stderr: errMsg });\n reject(new Error(errMsg));\n return;\n }\n\n try {\n const { text, sessionId } = this.parseJsonEvents(stdout);\n resolve({ text, sessionId });\n } catch (err) {\n reject(new Error(`Failed to parse OpenCode JSON output: ${(err as Error).message}`));\n }\n });\n\n child.on('error', (err) => {\n clearTimeout(timer);\n log.error('OpenCode spawn failed', { error: err.message });\n reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));\n });\n });\n }\n\n // ── Resume session ────────────────────────────────────────────────────\n\n private async runResume(prompt: string, sessionId: string, workingDirectory?: string): Promise<string> {\n const cliPath = this.config.cliPath || 'opencode';\n const timeout = this.config.timeout ?? 600_000;\n const MAX_PROMPT = 100_000;\n\n if (prompt.length > MAX_PROMPT) {\n throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);\n }\n\n const env: Record<string, string> = {\n PATH: process.env.PATH ?? '',\n HOME: process.env.HOME ?? '',\n };\n for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {\n if (process.env[key]) env[key] = process.env[key];\n }\n for (const key of ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENCODE_SERVER_PASSWORD', 'OPENCODE_SERVER_USERNAME']) {\n if (process.env[key]) env[key] = process.env[key];\n }\n\n const args: string[] = ['run', '--session', sessionId];\n if (this.config.model) {\n args.push('--model', this.config.model);\n }\n if (this.config.agent) {\n args.push('--agent', this.config.agent);\n }\n if (this.config.attachUrl) {\n args.push('--attach', this.config.attachUrl);\n }\n args.push('--format', 'default', '--dangerously-skip-permissions', prompt);\n\n const workdir = this.resolveWorkdir(workingDirectory);\n log.info('Spawning opencode (resume)', { sessionId, workdir });\n\n return new Promise<string>((resolve, reject) => {\n const child = spawn(cliPath, args, {\n cwd: workdir,\n env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n this.currentChild = child;\n\n let stdout = '';\n let stderr = '';\n const MAX_OUTPUT = 10_000_000;\n\n const timer = setTimeout(() => {\n child.kill('SIGTERM');\n const forceTimer = setTimeout(() => {\n if (child.exitCode === null) child.kill('SIGKILL');\n }, 5000);\n child.on('close', () => clearTimeout(forceTimer));\n reject(new Error(`OpenCode execution timed out after ${timeout}ms`));\n }, timeout);\n\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout += chunk.toString();\n if (stdout.length > MAX_OUTPUT) {\n child.kill('SIGTERM');\n setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 5000);\n }\n });\n\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString();\n log.debug('OpenCode stderr', { text: chunk.toString().trim() });\n });\n\n child.on('close', (code) => {\n clearTimeout(timer);\n this.currentChild = null;\n\n if (code === 0) {\n resolve(stdout.trim());\n } else {\n const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;\n log.warn('OpenCode non-zero exit', { code, stderr: errMsg });\n reject(new Error(errMsg));\n }\n });\n\n child.on('error', (err) => {\n clearTimeout(timer);\n log.error('OpenCode spawn failed', { error: err.message });\n reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));\n });\n });\n }\n\n // ── Parser ────────────────────────────────────────────────────────────\n\n /**\n * Parse newline-delimited JSON events from OpenCode's `--format json` output.\n * Extracts the sessionID from the first event that contains it, and\n * accumulates text content from relevant events.\n */\n private parseJsonEvents(stdout: string): { text: string; sessionId: string } {\n const lines = stdout.split('\\n').filter((l) => l.trim());\n let sessionId = '';\n const textParts: string[] = [];\n\n for (const line of lines) {\n let event: Record<string, unknown>;\n try {\n event = JSON.parse(line);\n } catch {\n continue;\n }\n\n // Capture sessionID from any event that has it\n if (!sessionId && event.sessionID) {\n sessionId = event.sessionID as string;\n }\n\n // Skip error events — they don't contain user-facing text\n if (event.type === 'error') continue;\n\n // Extract text from events that have content\n // Common OpenCode event shapes: { type, text }, { type, content }, { message }\n const text = event.text as string | undefined\n || event.content as string | undefined\n || event.message as string | undefined;\n\n if (text) {\n textParts.push(text);\n }\n }\n\n if (!sessionId) {\n // Fallback: if no sessionID in JSON, the output may be plain text from\n // a non-JSON mode invocation. Return the raw text.\n return {\n text: textParts.join('\\n').trim() || stdout.trim(),\n sessionId: '',\n };\n }\n\n return {\n text: textParts.join('\\n').trim() || 'No response from OpenCode.',\n sessionId,\n };\n }\n}\n","/**\n * OpenCode A2A Executor\n *\n * Bridges the local OpenCode CLI to the A2A protocol. Spawns `opencode run`\n * as a child process for each request. Supports multi-turn conversation\n * via contextId → sessionId binding through SessionBindingStore.\n */\n\nimport type { A2AExecutor } from '../index.js';\nimport type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';\n\nimport type { AgentConfig } from '../../config/types.js';\nimport type { SessionBindingStore } from '../../session-store.js';\nimport { generateContextId } from '../../session-store.js';\nimport { logger } from '../../logger.js';\nimport { OpenCodeClient } from './client.js';\nimport {\n publishTask,\n publishStatus,\n publishFinalArtifact,\n extractText,\n} from '../events.js';\n\nconst log = logger.child('opencode:executor');\n\n// ─── Executor ───────────────────────────────────────────────────────────────\n\nexport class OpenCodeExecutor implements A2AExecutor {\n private config: Required<AgentConfig>;\n private client: OpenCodeClient | null = null;\n private initialized = false;\n private sessionStore: SessionBindingStore;\n\n constructor(config: Required<AgentConfig>, sessionStore: SessionBindingStore) {\n this.config = config;\n this.sessionStore = sessionStore;\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n const oc = this.config.opencode;\n this.client = new OpenCodeClient({\n cliPath: 'opencode',\n workdir: oc.projectDirectory || process.cwd(),\n model: oc.model || undefined,\n agent: oc.agent || undefined,\n attachUrl: oc.baseUrl || undefined,\n timeout: this.config.timeouts.prompt ?? 600_000,\n });\n\n this.initialized = true;\n log.info('Executor initialized', { workdir: oc.projectDirectory, model: oc.model });\n }\n\n async shutdown(): Promise<void> {\n this.client = null;\n this.initialized = false;\n log.info('Executor shut down');\n }\n\n async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {\n const { taskId, userMessage } = ctx;\n await this.initialize();\n\n const reuseByContext = this.config.session?.reuseByContext ?? true;\n\n let contextId = ctx.contextId;\n if (!contextId) {\n contextId = generateContextId();\n log.info('Generated new contextId', { contextId, taskId });\n }\n\n const binding = reuseByContext\n ? this.sessionStore.get(contextId, 'opencode')\n : null;\n\n const sessionId = binding?.sessionId;\n\n try {\n const existingTask = ctx.task;\n if (!existingTask) {\n publishTask(bus, taskId, contextId);\n publishStatus(bus, taskId, contextId, 'submitted');\n }\n\n publishStatus(bus, taskId, contextId, 'working');\n\n const promptText = extractText(userMessage);\n\n // Extract per-request workingDirectory from A2A message metadata\n const userMsg = userMessage as unknown as Record<string, unknown>;\n const workingDirectory = userMsg.metadata &&\n typeof userMsg.metadata === 'object'\n ? (userMsg.metadata as Record<string, unknown>).workingDirectory as string | undefined\n : undefined;\n\n log.info('Sending prompt to OpenCode', {\n taskId,\n contextId,\n sessionId: sessionId || '(new)',\n len: promptText.length,\n workingDirectory: workingDirectory || '(default)',\n });\n\n const result = await this.client!.execute(promptText, sessionId, workingDirectory);\n\n this.sessionStore.set({\n provider: 'opencode',\n contextId,\n sessionId: result.sessionId,\n activeTaskId: undefined,\n activeTaskState: undefined,\n createdAt: binding?.createdAt ?? new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1000)).toISOString(),\n });\n\n publishFinalArtifact(bus, taskId, contextId, result.text || 'No response from OpenCode.');\n publishStatus(bus, taskId, contextId, 'completed', undefined, true);\n bus.finished();\n log.info('Task completed', { taskId, contextId, sessionId: result.sessionId });\n\n } catch (error) {\n const msg = (error as Error).message ?? String(error);\n log.error('Execution failed', { taskId, contextId, error: msg });\n publishStatus(bus, taskId, contextId, 'failed', `Error: ${msg}`, true);\n bus.finished();\n }\n }\n\n async cancelTask(taskId: string, bus: ExecutionEventBus): Promise<void> {\n log.info('Cancel requested for OpenCode task', { taskId });\n this.client?.abort();\n publishStatus(bus, taskId, '', 'canceled', 'OpenCode task cancelled', true);\n bus.finished();\n }\n}\n\n// ─── Factory ────────────────────────────────────────────────────────────────\n\nexport function createOpenCodeExecutor(\n config: Required<AgentConfig>,\n sessionStore: SessionBindingStore,\n): A2AExecutor {\n return new OpenCodeExecutor(config, sessionStore);\n}\n"],"mappings":"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","names":["log"]}
@@ -0,0 +1,195 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VZEH3EPJ.mjs";
4
+
5
+ // src/session-store.ts
6
+ import { join, dirname } from "path";
7
+ import { homedir } from "os";
8
+ import { mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, existsSync } from "fs";
9
+ import { randomUUID } from "crypto";
10
+ var log = logger.child("session-store");
11
+ var DEFAULT_STORE_DIR = join(
12
+ homedir(),
13
+ "Library",
14
+ "Application Support",
15
+ "axiom-lattice",
16
+ "cli-a2a"
17
+ );
18
+ var DEFAULT_STORE_PATH = join(DEFAULT_STORE_DIR, "sessions.json");
19
+ var DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
20
+ function resolveSessionStorePath(options) {
21
+ if (options.configuredPath && options.configuredPath.length > 0) {
22
+ return options.configuredPath;
23
+ }
24
+ return join(
25
+ options.storeDir ?? DEFAULT_STORE_DIR,
26
+ `sessions-${options.provider}-${options.port}.json`
27
+ );
28
+ }
29
+ var SessionBindingStore = class {
30
+ constructor(opts) {
31
+ this.cache = /* @__PURE__ */ new Map();
32
+ this.cleanupTimer = null;
33
+ this.dirty = false;
34
+ this.flushTimeout = null;
35
+ this.storePath = opts?.storePath && opts.storePath.length > 0 ? opts.storePath : DEFAULT_STORE_PATH;
36
+ this.persist = opts?.persist ?? true;
37
+ this.loadFromFile();
38
+ this.startCleanup(opts?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS);
39
+ }
40
+ // ── Public API ────────────────────────────────────────────────────────
41
+ get(contextId, provider) {
42
+ const key = this.makeKey(contextId, provider);
43
+ const binding = this.cache.get(key);
44
+ if (!binding) return null;
45
+ if (this.isExpired(binding)) {
46
+ this.cache.delete(key);
47
+ this.markDirty();
48
+ return null;
49
+ }
50
+ return binding;
51
+ }
52
+ set(binding) {
53
+ const key = this.makeKey(binding.contextId, binding.provider);
54
+ this.cache.set(key, binding);
55
+ this.markDirty();
56
+ }
57
+ delete(contextId, provider) {
58
+ const key = this.makeKey(contextId, provider);
59
+ const existed = this.cache.delete(key);
60
+ if (existed) this.markDirty();
61
+ return existed;
62
+ }
63
+ clearActiveTask(contextId, provider) {
64
+ const binding = this.get(contextId, provider);
65
+ if (binding) {
66
+ binding.activeTaskId = void 0;
67
+ binding.activeTaskState = void 0;
68
+ binding.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
69
+ this.markDirty();
70
+ }
71
+ }
72
+ /**
73
+ * Remove all expired entries.
74
+ * Called automatically on the cleanup interval; call manually before shutdown.
75
+ */
76
+ cleanup() {
77
+ let removed = 0;
78
+ for (const [key, binding] of this.cache) {
79
+ if (this.isExpired(binding)) {
80
+ this.cache.delete(key);
81
+ removed++;
82
+ }
83
+ }
84
+ if (removed > 0) {
85
+ this.markDirty();
86
+ log.info("TTL cleanup completed", { removed, remaining: this.cache.size });
87
+ }
88
+ return removed;
89
+ }
90
+ /**
91
+ * Graceful shutdown: stop cleanup timer, flush pending writes.
92
+ */
93
+ shutdown() {
94
+ if (this.cleanupTimer) {
95
+ clearInterval(this.cleanupTimer);
96
+ this.cleanupTimer = null;
97
+ }
98
+ if (this.flushTimeout) {
99
+ clearTimeout(this.flushTimeout);
100
+ this.flushTimeout = null;
101
+ }
102
+ this.flushToFile();
103
+ log.info("Session store shut down", { bindings: this.cache.size });
104
+ }
105
+ /** For testing: return the number of cached bindings. */
106
+ get size() {
107
+ return this.cache.size;
108
+ }
109
+ // ── Internal ──────────────────────────────────────────────────────────
110
+ makeKey(contextId, provider) {
111
+ return `${provider}:${contextId}`;
112
+ }
113
+ isExpired(binding) {
114
+ if (!binding.expiresAt) return false;
115
+ return Date.now() >= new Date(binding.expiresAt).getTime();
116
+ }
117
+ startCleanup(intervalMs) {
118
+ this.cleanupTimer = setInterval(() => this.cleanup(), intervalMs);
119
+ this.cleanupTimer.unref();
120
+ }
121
+ // ── Persistence ───────────────────────────────────────────────────────
122
+ markDirty() {
123
+ if (!this.persist) return;
124
+ this.dirty = true;
125
+ if (!this.flushTimeout) {
126
+ this.flushTimeout = setTimeout(() => {
127
+ this.flushToFile();
128
+ }, 1e3);
129
+ }
130
+ }
131
+ flushToFile() {
132
+ this.flushTimeout = null;
133
+ if (!this.persist || !this.dirty) return;
134
+ this.dirty = false;
135
+ this.writeAtomically();
136
+ }
137
+ writeAtomically() {
138
+ const tmpPath = this.storePath + ".tmp";
139
+ const data = {
140
+ version: 1,
141
+ bindings: Array.from(this.cache.values())
142
+ };
143
+ try {
144
+ mkdirSync(dirname(this.storePath), { recursive: true });
145
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
146
+ renameSync(tmpPath, this.storePath);
147
+ } catch (err) {
148
+ log.error("Failed to write session store", { error: err.message });
149
+ try {
150
+ unlinkSync(tmpPath);
151
+ } catch {
152
+ }
153
+ }
154
+ }
155
+ loadFromFile() {
156
+ if (!this.persist || !existsSync(this.storePath)) return;
157
+ try {
158
+ const raw = readFileSync(this.storePath, "utf-8");
159
+ const data = JSON.parse(raw);
160
+ if (!data || !Array.isArray(data.bindings)) {
161
+ throw new Error("Invalid store format: bindings array missing");
162
+ }
163
+ let loaded = 0;
164
+ let expired = 0;
165
+ for (const b of data.bindings) {
166
+ if (!b.provider || !b.contextId || !b.sessionId) continue;
167
+ if (this.isExpired(b)) {
168
+ expired++;
169
+ continue;
170
+ }
171
+ const key = this.makeKey(b.contextId, b.provider);
172
+ this.cache.set(key, b);
173
+ loaded++;
174
+ }
175
+ log.info("Session store loaded", { path: this.storePath, loaded, expired });
176
+ } catch (err) {
177
+ log.error("Failed to load session store \u2014 starting fresh", {
178
+ path: this.storePath,
179
+ error: err.message
180
+ });
181
+ this.cache.clear();
182
+ this.writeAtomically();
183
+ }
184
+ }
185
+ };
186
+ function generateContextId() {
187
+ return randomUUID();
188
+ }
189
+
190
+ export {
191
+ resolveSessionStorePath,
192
+ SessionBindingStore,
193
+ generateContextId
194
+ };
195
+ //# sourceMappingURL=chunk-DRFFQJII.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session-store.ts"],"sourcesContent":["/**\n * SessionBindingStore\n *\n * Persists A2A contextId → CLI sessionId mappings so that multi-turn\n * conversations survive process restarts. Supports atomic file writes,\n * in-memory caching, TTL-based cleanup, and corrupted-file recovery.\n */\n\nimport { join, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport { mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport { logger } from './logger.js';\nimport type { ProviderType } from './config/types.js';\n\nconst log = logger.child('session-store');\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface SessionBinding {\n provider: ProviderType;\n contextId: string;\n sessionId: string;\n activeTaskId?: string;\n activeTaskState?: string;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\ninterface StoreData {\n version: 1;\n bindings: SessionBinding[];\n}\n\n// ─── Defaults ───────────────────────────────────────────────────────────────\n\nconst DEFAULT_STORE_DIR = join(\n homedir(),\n 'Library',\n 'Application Support',\n 'axiom-lattice',\n 'cli-a2a',\n);\nconst DEFAULT_STORE_PATH = join(DEFAULT_STORE_DIR, 'sessions.json');\nconst DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\nexport function resolveSessionStorePath(options: {\n configuredPath?: string;\n provider: ProviderType;\n port: number;\n storeDir?: string;\n}): string {\n if (options.configuredPath && options.configuredPath.length > 0) {\n return options.configuredPath;\n }\n\n return join(\n options.storeDir ?? DEFAULT_STORE_DIR,\n `sessions-${options.provider}-${options.port}.json`,\n );\n}\n\n// ─── Store ──────────────────────────────────────────────────────────────────\n\nexport class SessionBindingStore {\n private cache = new Map<string, SessionBinding>();\n private storePath: string;\n private persist: boolean;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n private dirty = false;\n private flushTimeout: ReturnType<typeof setTimeout> | null = null;\n\n constructor(opts?: {\n storePath?: string;\n persist?: boolean;\n cleanupIntervalMs?: number;\n }) {\n this.storePath = (opts?.storePath && opts.storePath.length > 0)\n ? opts.storePath\n : DEFAULT_STORE_PATH;\n this.persist = opts?.persist ?? true;\n this.loadFromFile();\n this.startCleanup(opts?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS);\n }\n\n // ── Public API ────────────────────────────────────────────────────────\n\n get(contextId: string, provider: ProviderType): SessionBinding | null {\n const key = this.makeKey(contextId, provider);\n const binding = this.cache.get(key);\n if (!binding) return null;\n\n if (this.isExpired(binding)) {\n this.cache.delete(key);\n this.markDirty();\n return null;\n }\n\n return binding;\n }\n\n set(binding: SessionBinding): void {\n const key = this.makeKey(binding.contextId, binding.provider);\n this.cache.set(key, binding);\n this.markDirty();\n }\n\n delete(contextId: string, provider: ProviderType): boolean {\n const key = this.makeKey(contextId, provider);\n const existed = this.cache.delete(key);\n if (existed) this.markDirty();\n return existed;\n }\n\n clearActiveTask(contextId: string, provider: ProviderType): void {\n const binding = this.get(contextId, provider);\n if (binding) {\n binding.activeTaskId = undefined;\n binding.activeTaskState = undefined;\n binding.updatedAt = new Date().toISOString();\n this.markDirty();\n }\n }\n\n /**\n * Remove all expired entries.\n * Called automatically on the cleanup interval; call manually before shutdown.\n */\n cleanup(): number {\n let removed = 0;\n for (const [key, binding] of this.cache) {\n if (this.isExpired(binding)) {\n this.cache.delete(key);\n removed++;\n }\n }\n if (removed > 0) {\n this.markDirty();\n log.info('TTL cleanup completed', { removed, remaining: this.cache.size });\n }\n return removed;\n }\n\n /**\n * Graceful shutdown: stop cleanup timer, flush pending writes.\n */\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n if (this.flushTimeout) {\n clearTimeout(this.flushTimeout);\n this.flushTimeout = null;\n }\n this.flushToFile();\n log.info('Session store shut down', { bindings: this.cache.size });\n }\n\n /** For testing: return the number of cached bindings. */\n get size(): number {\n return this.cache.size;\n }\n\n // ── Internal ──────────────────────────────────────────────────────────\n\n private makeKey(contextId: string, provider: ProviderType): string {\n return `${provider}:${contextId}`;\n }\n\n private isExpired(binding: SessionBinding): boolean {\n if (!binding.expiresAt) return false;\n return Date.now() >= new Date(binding.expiresAt).getTime();\n }\n\n private startCleanup(intervalMs: number): void {\n this.cleanupTimer = setInterval(() => this.cleanup(), intervalMs);\n this.cleanupTimer.unref(); // don't keep process alive\n }\n\n // ── Persistence ───────────────────────────────────────────────────────\n\n private markDirty(): void {\n if (!this.persist) return;\n this.dirty = true;\n if (!this.flushTimeout) {\n // Debounce writes by 1 second to batch rapid updates\n this.flushTimeout = setTimeout(() => {\n this.flushToFile();\n }, 1000);\n }\n }\n\n private flushToFile(): void {\n this.flushTimeout = null;\n if (!this.persist || !this.dirty) return;\n this.dirty = false;\n this.writeAtomically();\n }\n\n private writeAtomically(): void {\n const tmpPath = this.storePath + '.tmp';\n const data: StoreData = {\n version: 1,\n bindings: Array.from(this.cache.values()),\n };\n\n try {\n mkdirSync(dirname(this.storePath), { recursive: true });\n writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');\n renameSync(tmpPath, this.storePath);\n } catch (err) {\n log.error('Failed to write session store', { error: (err as Error).message });\n // Clean up temp file if it exists\n try { unlinkSync(tmpPath); } catch { /* ignore */ }\n }\n }\n\n private loadFromFile(): void {\n if (!this.persist || !existsSync(this.storePath)) return;\n\n try {\n const raw = readFileSync(this.storePath, 'utf-8');\n const data = JSON.parse(raw) as StoreData;\n\n if (!data || !Array.isArray(data.bindings)) {\n throw new Error('Invalid store format: bindings array missing');\n }\n\n let loaded = 0;\n let expired = 0;\n for (const b of data.bindings) {\n if (!b.provider || !b.contextId || !b.sessionId) continue; // skip malformed\n if (this.isExpired(b)) {\n expired++;\n continue;\n }\n const key = this.makeKey(b.contextId, b.provider);\n this.cache.set(key, b);\n loaded++;\n }\n\n log.info('Session store loaded', { path: this.storePath, loaded, expired });\n } catch (err) {\n log.error('Failed to load session store — starting fresh', {\n path: this.storePath,\n error: (err as Error).message,\n });\n this.cache.clear();\n // Write a clean empty store so the corrupted file doesn't block next start\n this.writeAtomically();\n }\n }\n}\n\n/**\n * Generate a new contextId for use when the A2A client doesn't provide one.\n */\nexport function generateContextId(): string {\n return randomUUID();\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,378 @@
1
+ import {
2
+ extractText,
3
+ publishFinalArtifact,
4
+ publishStatus,
5
+ publishTask
6
+ } from "./chunk-YGUPSPZO.mjs";
7
+ import {
8
+ generateContextId
9
+ } from "./chunk-DRFFQJII.mjs";
10
+ import {
11
+ logger
12
+ } from "./chunk-VZEH3EPJ.mjs";
13
+
14
+ // src/executors/codex/client.ts
15
+ import { spawn } from "child_process";
16
+ var log = logger.child("codex:client");
17
+ var CodexClient = class {
18
+ constructor(config) {
19
+ this.config = config;
20
+ this.currentChild = null;
21
+ }
22
+ abort() {
23
+ if (this.currentChild) {
24
+ this.currentChild.kill("SIGTERM");
25
+ setTimeout(() => {
26
+ if (this.currentChild?.exitCode === null) {
27
+ this.currentChild.kill("SIGKILL");
28
+ }
29
+ }, 5e3);
30
+ this.currentChild = null;
31
+ }
32
+ }
33
+ /**
34
+ * Execute a prompt via the Codex CLI.
35
+ *
36
+ * @param prompt - The user prompt text.
37
+ * @param sessionId - If provided, resumes an existing session.
38
+ * Otherwise creates a new session and extracts the thread_id from JSONL.
39
+ * @param workingDirectory - Optional per-request working directory override.
40
+ */
41
+ async execute(prompt, sessionId, workingDirectory) {
42
+ if (sessionId) {
43
+ return this.executeResume(prompt, sessionId, workingDirectory);
44
+ }
45
+ return this.executeNew(prompt, workingDirectory);
46
+ }
47
+ resolveWorkdir(override) {
48
+ return override || this.config.workdir || process.cwd();
49
+ }
50
+ // ── New session (first turn) ──────────────────────────────────────────
51
+ async executeNew(prompt, workingDirectory) {
52
+ const cliPath = this.config.cliPath || "codex";
53
+ const timeout = this.config.timeout ?? 3e5;
54
+ const MAX_PROMPT = 1e5;
55
+ if (prompt.length > MAX_PROMPT) {
56
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
57
+ }
58
+ const env = {
59
+ PATH: process.env.PATH ?? "",
60
+ HOME: process.env.HOME ?? ""
61
+ };
62
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
63
+ if (process.env[key]) env[key] = process.env[key];
64
+ }
65
+ if (this.config.apiKey) {
66
+ env["OPENAI_API_KEY"] = this.config.apiKey;
67
+ }
68
+ const args = ["exec", "--json"];
69
+ if (this.config.model) {
70
+ args.push("--model", this.config.model);
71
+ }
72
+ args.push(
73
+ "--dangerously-bypass-approvals-and-sandbox",
74
+ "--skip-git-repo-check",
75
+ prompt
76
+ );
77
+ const workdir = this.resolveWorkdir(workingDirectory);
78
+ log.info("Spawning codex (new session)", { cliPath, workdir, model: this.config.model });
79
+ return new Promise((resolve, reject) => {
80
+ const child = spawn(cliPath, args, {
81
+ cwd: workdir,
82
+ env,
83
+ stdio: ["ignore", "pipe", "pipe"]
84
+ });
85
+ this.currentChild = child;
86
+ let stdout = "";
87
+ let stderr = "";
88
+ const MAX_OUTPUT = 1e7;
89
+ const timer = setTimeout(() => {
90
+ child.kill("SIGTERM");
91
+ const forceTimer = setTimeout(() => {
92
+ if (child.exitCode === null) child.kill("SIGKILL");
93
+ }, 5e3);
94
+ child.on("close", () => clearTimeout(forceTimer));
95
+ reject(new Error(`Codex execution timed out after ${timeout}ms`));
96
+ }, timeout);
97
+ child.stdout?.on("data", (chunk) => {
98
+ stdout += chunk.toString();
99
+ if (stdout.length > MAX_OUTPUT) {
100
+ child.kill("SIGTERM");
101
+ setTimeout(() => {
102
+ if (child.exitCode === null) child.kill("SIGKILL");
103
+ }, 5e3);
104
+ }
105
+ });
106
+ child.stderr?.on("data", (chunk) => {
107
+ stderr += chunk.toString();
108
+ log.debug("Codex stderr", { text: chunk.toString().trim() });
109
+ });
110
+ child.on("close", (code) => {
111
+ clearTimeout(timer);
112
+ this.currentChild = null;
113
+ if (code !== 0) {
114
+ const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
115
+ log.warn("Codex non-zero exit", { code, stderr: errMsg });
116
+ reject(new Error(errMsg));
117
+ return;
118
+ }
119
+ try {
120
+ const { text, sessionId } = this.parseJsonl(stdout);
121
+ resolve({ text, sessionId });
122
+ } catch (err) {
123
+ reject(new Error(`Failed to parse Codex JSONL output: ${err.message}`));
124
+ }
125
+ });
126
+ child.on("error", (err) => {
127
+ clearTimeout(timer);
128
+ log.error("Codex spawn failed", { error: err.message });
129
+ reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
130
+ });
131
+ });
132
+ }
133
+ // ── Resume session (subsequent turns) ─────────────────────────────────
134
+ async executeResume(prompt, sessionId, workingDirectory) {
135
+ const cliPath = this.config.cliPath || "codex";
136
+ const timeout = this.config.timeout ?? 3e5;
137
+ const MAX_PROMPT = 1e5;
138
+ if (prompt.length > MAX_PROMPT) {
139
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
140
+ }
141
+ const env = {
142
+ PATH: process.env.PATH ?? "",
143
+ HOME: process.env.HOME ?? ""
144
+ };
145
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
146
+ if (process.env[key]) env[key] = process.env[key];
147
+ }
148
+ if (this.config.apiKey) {
149
+ env["OPENAI_API_KEY"] = this.config.apiKey;
150
+ }
151
+ const args = ["exec", "resume", sessionId];
152
+ if (this.config.model) {
153
+ args.push("--model", this.config.model);
154
+ }
155
+ args.push(
156
+ "--dangerously-bypass-approvals-and-sandbox",
157
+ "--skip-git-repo-check",
158
+ prompt
159
+ );
160
+ const workdir = this.resolveWorkdir(workingDirectory);
161
+ log.info("Spawning codex (resume)", { sessionId, workdir });
162
+ return new Promise((resolve, reject) => {
163
+ const child = spawn(cliPath, args, {
164
+ cwd: workdir,
165
+ env,
166
+ stdio: ["ignore", "pipe", "pipe"]
167
+ });
168
+ this.currentChild = child;
169
+ let stdout = "";
170
+ let stderr = "";
171
+ const MAX_OUTPUT = 1e7;
172
+ const timer = setTimeout(() => {
173
+ child.kill("SIGTERM");
174
+ const forceTimer = setTimeout(() => {
175
+ if (child.exitCode === null) child.kill("SIGKILL");
176
+ }, 5e3);
177
+ child.on("close", () => clearTimeout(forceTimer));
178
+ reject(new Error(`Codex execution timed out after ${timeout}ms`));
179
+ }, timeout);
180
+ child.stdout?.on("data", (chunk) => {
181
+ stdout += chunk.toString();
182
+ if (stdout.length > MAX_OUTPUT) {
183
+ child.kill("SIGTERM");
184
+ setTimeout(() => {
185
+ if (child.exitCode === null) child.kill("SIGKILL");
186
+ }, 5e3);
187
+ }
188
+ });
189
+ child.stderr?.on("data", (chunk) => {
190
+ stderr += chunk.toString();
191
+ log.debug("Codex stderr", { text: chunk.toString().trim() });
192
+ });
193
+ child.on("close", (code) => {
194
+ clearTimeout(timer);
195
+ this.currentChild = null;
196
+ if (code !== 0) {
197
+ const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
198
+ log.warn("Codex non-zero exit", { code, stderr: errMsg });
199
+ reject(new Error(errMsg));
200
+ return;
201
+ }
202
+ const text = this.extractResumeText(stdout);
203
+ resolve({ text, sessionId });
204
+ });
205
+ child.on("error", (err) => {
206
+ clearTimeout(timer);
207
+ log.error("Codex spawn failed", { error: err.message });
208
+ reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
209
+ });
210
+ });
211
+ }
212
+ // ── Parsers ───────────────────────────────────────────────────────────
213
+ /**
214
+ * Parse JSONL output to extract session thread_id and agent response text.
215
+ */
216
+ parseJsonl(stdout) {
217
+ const lines = stdout.split("\n").filter((l) => l.trim());
218
+ let sessionId = "";
219
+ const textParts = [];
220
+ for (const line of lines) {
221
+ let event;
222
+ try {
223
+ event = JSON.parse(line);
224
+ } catch {
225
+ continue;
226
+ }
227
+ if (event.type === "thread.started" && event.thread_id) {
228
+ sessionId = event.thread_id;
229
+ }
230
+ if (event.type === "item.completed") {
231
+ const item = event.item;
232
+ if (item && item.type === "agent_message" && item.text) {
233
+ textParts.push(item.text);
234
+ }
235
+ }
236
+ }
237
+ if (!sessionId) {
238
+ throw new Error("No thread_id found in Codex JSONL output");
239
+ }
240
+ return {
241
+ text: textParts.join("\n").trim() || "No response from Codex.",
242
+ sessionId
243
+ };
244
+ }
245
+ /**
246
+ * Extract the agent response text from the resume mode output.
247
+ *
248
+ * The resume output includes a CLI header followed by the conversation.
249
+ * We extract everything after the last "codex" marker line as the
250
+ * latest agent response.
251
+ */
252
+ extractResumeText(stdout) {
253
+ const lines = stdout.split("\n");
254
+ let responseStart = -1;
255
+ for (let i = 0; i < lines.length; i++) {
256
+ if (lines[i].trim() === "codex") {
257
+ responseStart = i;
258
+ }
259
+ }
260
+ if (responseStart >= 0 && responseStart + 1 < lines.length) {
261
+ return lines.slice(responseStart + 1).join("\n").trim();
262
+ }
263
+ let skipHeader = true;
264
+ const resultLines = [];
265
+ for (const line of lines) {
266
+ const trimmed = line.trim();
267
+ if (skipHeader) {
268
+ if (trimmed === "--------" || trimmed === "codex") {
269
+ skipHeader = false;
270
+ }
271
+ continue;
272
+ }
273
+ if (trimmed === "user" || trimmed === "codex") {
274
+ continue;
275
+ }
276
+ if (trimmed.startsWith("tokens used")) {
277
+ if (resultLines.length > 0) resultLines.pop();
278
+ break;
279
+ }
280
+ resultLines.push(line);
281
+ }
282
+ return resultLines.join("\n").trim() || stdout.trim();
283
+ }
284
+ };
285
+
286
+ // src/executors/codex/executor.ts
287
+ var log2 = logger.child("codex:executor");
288
+ var CodexExecutor = class {
289
+ constructor(config, sessionStore) {
290
+ this.client = null;
291
+ this.initialized = false;
292
+ this.config = config;
293
+ this.sessionStore = sessionStore;
294
+ }
295
+ async initialize() {
296
+ if (this.initialized) return;
297
+ const cx = this.config.codex;
298
+ this.client = new CodexClient({
299
+ cliPath: cx.cliPath,
300
+ workdir: cx.workdir,
301
+ model: cx.model || void 0,
302
+ apiKey: cx.apiKey || void 0,
303
+ timeout: this.config.timeouts.prompt ?? 6e5
304
+ });
305
+ this.initialized = true;
306
+ log2.info("Executor initialized", { cliPath: cx.cliPath, workdir: cx.workdir });
307
+ }
308
+ async shutdown() {
309
+ this.client = null;
310
+ this.initialized = false;
311
+ log2.info("Executor shut down");
312
+ }
313
+ async execute(ctx, bus) {
314
+ const { taskId, userMessage } = ctx;
315
+ await this.initialize();
316
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
317
+ let contextId = ctx.contextId;
318
+ if (!contextId) {
319
+ contextId = generateContextId();
320
+ log2.info("Generated new contextId", { contextId, taskId });
321
+ }
322
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "codex") : null;
323
+ const sessionId = binding?.sessionId;
324
+ try {
325
+ const existingTask = ctx.task;
326
+ if (!existingTask) {
327
+ publishTask(bus, taskId, contextId);
328
+ publishStatus(bus, taskId, contextId, "submitted");
329
+ }
330
+ publishStatus(bus, taskId, contextId, "working");
331
+ const promptText = extractText(userMessage);
332
+ const userMsg = userMessage;
333
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
334
+ log2.info("Sending prompt to Codex", {
335
+ taskId,
336
+ contextId,
337
+ sessionId: sessionId || "(new)",
338
+ len: promptText.length,
339
+ workingDirectory: workingDirectory || "(default)"
340
+ });
341
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
342
+ this.sessionStore.set({
343
+ provider: "codex",
344
+ contextId,
345
+ sessionId: result.sessionId,
346
+ activeTaskId: void 0,
347
+ activeTaskState: void 0,
348
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
349
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
350
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
351
+ });
352
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Codex.");
353
+ publishStatus(bus, taskId, contextId, "completed", void 0, true);
354
+ bus.finished();
355
+ log2.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
356
+ } catch (error) {
357
+ const msg = error.message ?? String(error);
358
+ log2.error("Execution failed", { taskId, contextId, error: msg });
359
+ publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
360
+ bus.finished();
361
+ }
362
+ }
363
+ async cancelTask(taskId, bus) {
364
+ log2.info("Cancel requested for Codex task", { taskId });
365
+ this.client?.abort();
366
+ publishStatus(bus, taskId, "", "canceled", "Codex task cancelled", true);
367
+ bus.finished();
368
+ }
369
+ };
370
+ function createCodexExecutor(config, sessionStore) {
371
+ return new CodexExecutor(config, sessionStore);
372
+ }
373
+
374
+ export {
375
+ CodexExecutor,
376
+ createCodexExecutor
377
+ };
378
+ //# sourceMappingURL=chunk-URJDRXJW.mjs.map