@twelvehart/orcats 0.0.0-seed.0 → 0.2.3

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 (275) hide show
  1. package/LICENSE +48 -0
  2. package/NOTICE +5 -0
  3. package/README.md +399 -0
  4. package/bin/orcats +4 -0
  5. package/dist/backends/acp-client.d.ts +74 -0
  6. package/dist/backends/acp-client.d.ts.map +1 -0
  7. package/dist/backends/acp-run.d.ts +31 -0
  8. package/dist/backends/acp-run.d.ts.map +1 -0
  9. package/dist/backends/claude-run.d.ts +35 -0
  10. package/dist/backends/claude-run.d.ts.map +1 -0
  11. package/dist/backends/claude-stream-json.d.ts +15 -0
  12. package/dist/backends/claude-stream-json.d.ts.map +1 -0
  13. package/dist/backends/codex-jsonl.d.ts +34 -0
  14. package/dist/backends/codex-jsonl.d.ts.map +1 -0
  15. package/dist/backends/codex-run.d.ts +40 -0
  16. package/dist/backends/codex-run.d.ts.map +1 -0
  17. package/dist/backends/codex.d.ts +5 -0
  18. package/dist/backends/codex.d.ts.map +1 -0
  19. package/dist/backends/conversation-config.d.ts +12 -0
  20. package/dist/backends/conversation-config.d.ts.map +1 -0
  21. package/dist/backends/index.d.ts +14 -0
  22. package/dist/backends/index.d.ts.map +1 -0
  23. package/dist/backends/opencode-run.d.ts +48 -0
  24. package/dist/backends/opencode-run.d.ts.map +1 -0
  25. package/dist/backends/opencode-sse.d.ts +35 -0
  26. package/dist/backends/opencode-sse.d.ts.map +1 -0
  27. package/dist/backends/pi-rpc.d.ts +21 -0
  28. package/dist/backends/pi-rpc.d.ts.map +1 -0
  29. package/dist/backends/pi-run.d.ts +21 -0
  30. package/dist/backends/pi-run.d.ts.map +1 -0
  31. package/dist/backends/select.d.ts +17 -0
  32. package/dist/backends/select.d.ts.map +1 -0
  33. package/dist/backends/subprocess-run.d.ts +68 -0
  34. package/dist/backends/subprocess-run.d.ts.map +1 -0
  35. package/dist/backends/types.d.ts +17 -0
  36. package/dist/backends/types.d.ts.map +1 -0
  37. package/dist/backends/unsupported.d.ts +9 -0
  38. package/dist/backends/unsupported.d.ts.map +1 -0
  39. package/dist/baseline/index.d.ts +56 -0
  40. package/dist/baseline/index.d.ts.map +1 -0
  41. package/dist/cli/args.d.ts +22 -0
  42. package/dist/cli/args.d.ts.map +1 -0
  43. package/dist/cli/embedded.d.ts +7 -0
  44. package/dist/cli/embedded.d.ts.map +1 -0
  45. package/dist/cli/main.d.ts +6 -0
  46. package/dist/cli/main.d.ts.map +1 -0
  47. package/dist/cli/version.d.ts +2 -0
  48. package/dist/cli/version.d.ts.map +1 -0
  49. package/dist/conversation/ask-user.d.ts +17 -0
  50. package/dist/conversation/ask-user.d.ts.map +1 -0
  51. package/dist/conversation/conversation.d.ts +46 -0
  52. package/dist/conversation/conversation.d.ts.map +1 -0
  53. package/dist/conversation/harness.d.ts +14 -0
  54. package/dist/conversation/harness.d.ts.map +1 -0
  55. package/dist/conversation/index.d.ts +5 -0
  56. package/dist/conversation/index.d.ts.map +1 -0
  57. package/dist/conversation/queue.d.ts +15 -0
  58. package/dist/conversation/queue.d.ts.map +1 -0
  59. package/dist/flow/accessors.d.ts +13 -0
  60. package/dist/flow/accessors.d.ts.map +1 -0
  61. package/dist/flow/approvals.d.ts +6 -0
  62. package/dist/flow/approvals.d.ts.map +1 -0
  63. package/dist/flow/args.d.ts +4 -0
  64. package/dist/flow/args.d.ts.map +1 -0
  65. package/dist/flow/context.d.ts +36 -0
  66. package/dist/flow/context.d.ts.map +1 -0
  67. package/dist/flow/index.d.ts +5 -0
  68. package/dist/flow/index.d.ts.map +1 -0
  69. package/dist/index.d.ts +16 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/loop/builder/index.d.ts +17 -0
  72. package/dist/loop/builder/index.d.ts.map +1 -0
  73. package/dist/loop/builder/types.d.ts +97 -0
  74. package/dist/loop/builder/types.d.ts.map +1 -0
  75. package/dist/loop/context/compaction.d.ts +38 -0
  76. package/dist/loop/context/compaction.d.ts.map +1 -0
  77. package/dist/loop/context/index.d.ts +4 -0
  78. package/dist/loop/context/index.d.ts.map +1 -0
  79. package/dist/loop/context/offload.d.ts +34 -0
  80. package/dist/loop/context/offload.d.ts.map +1 -0
  81. package/dist/loop/context/types.d.ts +13 -0
  82. package/dist/loop/context/types.d.ts.map +1 -0
  83. package/dist/loop/engine/bridge.d.ts +21 -0
  84. package/dist/loop/engine/bridge.d.ts.map +1 -0
  85. package/dist/loop/engine/index.d.ts +73 -0
  86. package/dist/loop/engine/index.d.ts.map +1 -0
  87. package/dist/loop/execution.d.ts +68 -0
  88. package/dist/loop/execution.d.ts.map +1 -0
  89. package/dist/loop/fanout.d.ts +148 -0
  90. package/dist/loop/fanout.d.ts.map +1 -0
  91. package/dist/loop/firing.d.ts +54 -0
  92. package/dist/loop/firing.d.ts.map +1 -0
  93. package/dist/loop/graph/analyze.d.ts +8 -0
  94. package/dist/loop/graph/analyze.d.ts.map +1 -0
  95. package/dist/loop/graph/index.d.ts +3 -0
  96. package/dist/loop/graph/index.d.ts.map +1 -0
  97. package/dist/loop/graph/types.d.ts +35 -0
  98. package/dist/loop/graph/types.d.ts.map +1 -0
  99. package/dist/loop/index.d.ts +7 -0
  100. package/dist/loop/index.d.ts.map +1 -0
  101. package/dist/loop/io/index.d.ts +4 -0
  102. package/dist/loop/io/index.d.ts.map +1 -0
  103. package/dist/loop/io/linear.d.ts +142 -0
  104. package/dist/loop/io/linear.d.ts.map +1 -0
  105. package/dist/loop/io/sink.d.ts +47 -0
  106. package/dist/loop/io/sink.d.ts.map +1 -0
  107. package/dist/loop/io/source.d.ts +64 -0
  108. package/dist/loop/io/source.d.ts.map +1 -0
  109. package/dist/loop/serve.d.ts +97 -0
  110. package/dist/loop/serve.d.ts.map +1 -0
  111. package/dist/loop/state/index.d.ts +5 -0
  112. package/dist/loop/state/index.d.ts.map +1 -0
  113. package/dist/loop/state/manifest.d.ts +36 -0
  114. package/dist/loop/state/manifest.d.ts.map +1 -0
  115. package/dist/loop/state/port.d.ts +33 -0
  116. package/dist/loop/state/port.d.ts.map +1 -0
  117. package/dist/loop/state/snapshot.d.ts +15 -0
  118. package/dist/loop/state/snapshot.d.ts.map +1 -0
  119. package/dist/loop/state/sqlite.d.ts +31 -0
  120. package/dist/loop/state/sqlite.d.ts.map +1 -0
  121. package/dist/loop/termination-contract.d.ts +28 -0
  122. package/dist/loop/termination-contract.d.ts.map +1 -0
  123. package/dist/loop/termination.d.ts +33 -0
  124. package/dist/loop/termination.d.ts.map +1 -0
  125. package/dist/model/backend-config.d.ts +30 -0
  126. package/dist/model/backend-config.d.ts.map +1 -0
  127. package/dist/model/brand.d.ts +9 -0
  128. package/dist/model/brand.d.ts.map +1 -0
  129. package/dist/model/errors.d.ts +11 -0
  130. package/dist/model/errors.d.ts.map +1 -0
  131. package/dist/model/index.d.ts +8 -0
  132. package/dist/model/index.d.ts.map +1 -0
  133. package/dist/model/json-schema.d.ts +4 -0
  134. package/dist/model/json-schema.d.ts.map +1 -0
  135. package/dist/model/result.d.ts +3 -0
  136. package/dist/model/result.d.ts.map +1 -0
  137. package/dist/model/schemas.d.ts +435 -0
  138. package/dist/model/schemas.d.ts.map +1 -0
  139. package/dist/model/structured.d.ts +9 -0
  140. package/dist/model/structured.d.ts.map +1 -0
  141. package/dist/monitor/index.d.ts +139 -0
  142. package/dist/monitor/index.d.ts.map +1 -0
  143. package/dist/plan/index.d.ts +3 -0
  144. package/dist/plan/index.d.ts.map +1 -0
  145. package/dist/plan/persistent.d.ts +23 -0
  146. package/dist/plan/persistent.d.ts.map +1 -0
  147. package/dist/plan/verdict.d.ts +9 -0
  148. package/dist/plan/verdict.d.ts.map +1 -0
  149. package/dist/review/index.d.ts +4 -0
  150. package/dist/review/index.d.ts.map +1 -0
  151. package/dist/review/loop.d.ts +117 -0
  152. package/dist/review/loop.d.ts.map +1 -0
  153. package/dist/review/reviewers.d.ts +13 -0
  154. package/dist/review/reviewers.d.ts.map +1 -0
  155. package/dist/review/tool.d.ts +20 -0
  156. package/dist/review/tool.d.ts.map +1 -0
  157. package/dist/run-output/index.d.ts +112 -0
  158. package/dist/run-output/index.d.ts.map +1 -0
  159. package/dist/runner/index.d.ts +2 -0
  160. package/dist/runner/index.d.ts.map +1 -0
  161. package/dist/runner/typecheck.d.ts +23 -0
  162. package/dist/runner/typecheck.d.ts.map +1 -0
  163. package/dist/test-utils/fakes.d.ts +76 -0
  164. package/dist/test-utils/fakes.d.ts.map +1 -0
  165. package/dist/test-utils/fixtures.d.ts +4 -0
  166. package/dist/test-utils/fixtures.d.ts.map +1 -0
  167. package/dist/test-utils/index.d.ts +4 -0
  168. package/dist/test-utils/index.d.ts.map +1 -0
  169. package/dist/test-utils/tier2.d.ts +8 -0
  170. package/dist/test-utils/tier2.d.ts.map +1 -0
  171. package/dist/tools/fs.d.ts +11 -0
  172. package/dist/tools/fs.d.ts.map +1 -0
  173. package/dist/tools/git.d.ts +10 -0
  174. package/dist/tools/git.d.ts.map +1 -0
  175. package/dist/tools/github.d.ts +13 -0
  176. package/dist/tools/github.d.ts.map +1 -0
  177. package/dist/tools/index.d.ts +7 -0
  178. package/dist/tools/index.d.ts.map +1 -0
  179. package/dist/tools/linear.d.ts +117 -0
  180. package/dist/tools/linear.d.ts.map +1 -0
  181. package/dist/tools/process.d.ts +40 -0
  182. package/dist/tools/process.d.ts.map +1 -0
  183. package/dist/tools/terminal.d.ts +23 -0
  184. package/dist/tools/terminal.d.ts.map +1 -0
  185. package/package.json +89 -9
  186. package/src/backends/acp-client.ts +324 -0
  187. package/src/backends/acp-run.ts +636 -0
  188. package/src/backends/claude-run.ts +226 -0
  189. package/src/backends/claude-stream-json.ts +273 -0
  190. package/src/backends/codex-jsonl.ts +339 -0
  191. package/src/backends/codex-run.ts +177 -0
  192. package/src/backends/codex.ts +84 -0
  193. package/src/backends/conversation-config.ts +26 -0
  194. package/src/backends/index.ts +13 -0
  195. package/src/backends/opencode-run.ts +432 -0
  196. package/src/backends/opencode-sse.ts +360 -0
  197. package/src/backends/pi-rpc.ts +242 -0
  198. package/src/backends/pi-run.ts +154 -0
  199. package/src/backends/select.ts +77 -0
  200. package/src/backends/subprocess-run.ts +305 -0
  201. package/src/backends/types.ts +29 -0
  202. package/src/backends/unsupported.ts +48 -0
  203. package/src/baseline/index.ts +437 -0
  204. package/src/cli/args.ts +109 -0
  205. package/src/cli/embedded.ts +206 -0
  206. package/src/cli/main.ts +332 -0
  207. package/src/cli/version.ts +1 -0
  208. package/src/conversation/ask-user.ts +105 -0
  209. package/src/conversation/conversation.ts +109 -0
  210. package/src/conversation/harness.ts +39 -0
  211. package/src/conversation/index.ts +4 -0
  212. package/src/conversation/queue.ts +86 -0
  213. package/src/flow/accessors.ts +43 -0
  214. package/src/flow/approvals.ts +10 -0
  215. package/src/flow/args.ts +26 -0
  216. package/src/flow/context.ts +106 -0
  217. package/src/flow/index.ts +4 -0
  218. package/src/index.ts +17 -0
  219. package/src/loop/builder/index.ts +346 -0
  220. package/src/loop/builder/types.ts +131 -0
  221. package/src/loop/context/compaction.ts +175 -0
  222. package/src/loop/context/index.ts +3 -0
  223. package/src/loop/context/offload.ts +79 -0
  224. package/src/loop/context/types.ts +17 -0
  225. package/src/loop/engine/bridge.ts +61 -0
  226. package/src/loop/engine/index.ts +166 -0
  227. package/src/loop/execution.ts +453 -0
  228. package/src/loop/fanout.ts +444 -0
  229. package/src/loop/firing.ts +188 -0
  230. package/src/loop/graph/analyze.ts +128 -0
  231. package/src/loop/graph/index.ts +2 -0
  232. package/src/loop/graph/types.ts +45 -0
  233. package/src/loop/index.ts +11 -0
  234. package/src/loop/io/index.ts +3 -0
  235. package/src/loop/io/linear.ts +714 -0
  236. package/src/loop/io/sink.ts +170 -0
  237. package/src/loop/io/source.ts +255 -0
  238. package/src/loop/serve.ts +307 -0
  239. package/src/loop/state/index.ts +4 -0
  240. package/src/loop/state/manifest.ts +56 -0
  241. package/src/loop/state/port.ts +43 -0
  242. package/src/loop/state/snapshot.ts +129 -0
  243. package/src/loop/state/sqlite.ts +265 -0
  244. package/src/loop/termination-contract.ts +89 -0
  245. package/src/loop/termination.ts +156 -0
  246. package/src/model/backend-config.ts +34 -0
  247. package/src/model/brand.ts +11 -0
  248. package/src/model/errors.ts +28 -0
  249. package/src/model/index.ts +7 -0
  250. package/src/model/json-schema.ts +14 -0
  251. package/src/model/result.ts +28 -0
  252. package/src/model/schemas.ts +218 -0
  253. package/src/model/structured.ts +26 -0
  254. package/src/monitor/index.ts +367 -0
  255. package/src/plan/index.ts +2 -0
  256. package/src/plan/persistent.ts +98 -0
  257. package/src/plan/verdict.ts +3 -0
  258. package/src/review/index.ts +3 -0
  259. package/src/review/loop.ts +365 -0
  260. package/src/review/reviewers.ts +92 -0
  261. package/src/review/tool.ts +41 -0
  262. package/src/run-output/index.ts +319 -0
  263. package/src/runner/index.ts +1 -0
  264. package/src/runner/typecheck.ts +72 -0
  265. package/src/test-utils/fakes.ts +260 -0
  266. package/src/test-utils/fixtures.ts +33 -0
  267. package/src/test-utils/index.ts +3 -0
  268. package/src/test-utils/tier2.ts +12 -0
  269. package/src/tools/fs.ts +44 -0
  270. package/src/tools/git.ts +34 -0
  271. package/src/tools/github.ts +25 -0
  272. package/src/tools/index.ts +6 -0
  273. package/src/tools/linear.ts +428 -0
  274. package/src/tools/process.ts +156 -0
  275. package/src/tools/terminal.ts +116 -0
@@ -0,0 +1,339 @@
1
+ import type { z } from "zod";
2
+ import {
3
+ collectConversation,
4
+ StreamConversation,
5
+ type ConversationCapture
6
+ } from "../conversation/index.ts";
7
+ import {
8
+ backendFailed,
9
+ parseStructuredOutput,
10
+ sessionId,
11
+ structuredOutputValidationFailed,
12
+ unsupportedFeature,
13
+ type BackendApprovalPolicy,
14
+ type BackendSandboxMode,
15
+ type RuntimeError,
16
+ type Usage
17
+ } from "../model/index.ts";
18
+
19
+ interface CodexLine {
20
+ readonly type?: string;
21
+ readonly thread_id?: string;
22
+ readonly usage?: {
23
+ readonly input_tokens?: number;
24
+ readonly output_tokens?: number;
25
+ readonly reasoning_output_tokens?: number;
26
+ };
27
+ readonly item?: {
28
+ readonly id?: string;
29
+ readonly type?: string;
30
+ readonly text?: string;
31
+ readonly name?: string;
32
+ readonly server?: string;
33
+ readonly tool?: string;
34
+ readonly command?: unknown;
35
+ readonly path?: string;
36
+ readonly changes?: unknown;
37
+ readonly output?: unknown;
38
+ readonly arguments?: unknown;
39
+ readonly result?: {
40
+ readonly content?: ReadonlyArray<{ readonly text?: string }>;
41
+ } | null;
42
+ readonly error?: string | null;
43
+ };
44
+ }
45
+
46
+ export type CodexParseResult = ConversationCapture<"codex">;
47
+
48
+ export interface CodexExecArgs {
49
+ readonly model?: string;
50
+ readonly approvalPolicy?: BackendApprovalPolicy;
51
+ readonly sandbox?: BackendSandboxMode;
52
+ readonly readOnly?: boolean;
53
+ readonly ignoreUserConfig?: boolean;
54
+ readonly outputSchemaPath?: string;
55
+ readonly resumeSessionId?: string;
56
+ readonly mcpServerUrl?: string;
57
+ readonly prompt?: string;
58
+ }
59
+
60
+ export function codexExecJsonlArgs(args: CodexExecArgs = {}): readonly string[] {
61
+ const sandbox = args.sandbox ?? (args.readOnly ? "read-only" : undefined);
62
+ const promptArgs = args.prompt === undefined ? [] : [args.prompt];
63
+ const commonArgs = [
64
+ "--json",
65
+ ...(args.ignoreUserConfig ? ["--ignore-user-config"] : []),
66
+ ...(args.model ? ["--model", args.model] : []),
67
+ ...(args.approvalPolicy ? ["-c", `approval_policy="${args.approvalPolicy}"`] : []),
68
+ ...(args.mcpServerUrl ? ["-c", `mcp_servers.orcats.url=${JSON.stringify(args.mcpServerUrl)}`] : []),
69
+ ...(args.outputSchemaPath ? ["--output-schema", args.outputSchemaPath] : [])
70
+ ];
71
+
72
+ if (args.resumeSessionId) {
73
+ return [
74
+ "exec",
75
+ "resume",
76
+ ...commonArgs,
77
+ // `codex exec resume` rejects the exec-only `--sandbox` flag; express the
78
+ // sandbox as the equivalent config override instead (same form as
79
+ // `approval_policy` above).
80
+ ...(sandbox ? ["-c", `sandbox_mode="${sandbox}"`] : []),
81
+ args.resumeSessionId,
82
+ ...promptArgs
83
+ ];
84
+ }
85
+
86
+ return [
87
+ "exec",
88
+ ...commonArgs,
89
+ ...(sandbox ? ["--sandbox", sandbox] : []),
90
+ ...promptArgs
91
+ ];
92
+ }
93
+
94
+ export interface CodexJsonlOptions<Output = unknown> {
95
+ readonly schema?: z.ZodType<Output>;
96
+ /** Interactive conversations surface the agent's `ask_user` call as a
97
+ * `user_question` event for the renderer. The answer is NOT routed here — Codex
98
+ * obtains it from the Orcats MCP HTTP bridge and re-emits it on the matching
99
+ * `item.completed`, which the tool-result branch turns into a `tool_result`.
100
+ * Autonomous conversations leave this false and reject `ask_user` explicitly. */
101
+ readonly interactive?: boolean;
102
+ }
103
+
104
+ export async function collectCodexJsonl<Output = unknown>(
105
+ lines: readonly string[],
106
+ options: CodexJsonlOptions<Output> = {}
107
+ ): Promise<CodexParseResult> {
108
+ return collectConversation({
109
+ backend: "codex",
110
+ consume: async (conversation) => {
111
+ await consumeCodexJsonl(lines, conversation, options);
112
+ }
113
+ });
114
+ }
115
+
116
+ export async function consumeCodexJsonl<Output = unknown>(
117
+ lines: readonly string[],
118
+ conversation: StreamConversation<"codex">,
119
+ options: CodexJsonlOptions<Output> = {}
120
+ ): Promise<void> {
121
+ const consumer = createCodexJsonlConsumer(conversation, options);
122
+ for (const raw of lines) {
123
+ await consumer.consume(raw);
124
+ }
125
+ consumer.finish();
126
+ }
127
+
128
+ export interface CodexJsonlConsumer {
129
+ readonly signal: AbortSignal;
130
+ consume(raw: string): Promise<void>;
131
+ finish(): void;
132
+ }
133
+
134
+ export function createCodexJsonlConsumer<Output = unknown>(
135
+ conversation: StreamConversation<"codex">,
136
+ options: CodexJsonlOptions<Output> = {}
137
+ ): CodexJsonlConsumer {
138
+ let threadId = "";
139
+ let output = "";
140
+ let lastAgentText = "";
141
+ const controller = new AbortController();
142
+
143
+ return {
144
+ get signal() {
145
+ return controller.signal;
146
+ },
147
+
148
+ async consume(raw: string): Promise<void> {
149
+ if (controller.signal.aborted || raw.trim() === "") {
150
+ return;
151
+ }
152
+
153
+ let line: CodexLine;
154
+ try {
155
+ line = JSON.parse(raw) as CodexLine;
156
+ } catch (error) {
157
+ controller.abort();
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ conversation.fail(backendFailed("codex", `invalid codex JSONL: ${message}`));
160
+ return;
161
+ }
162
+
163
+ if (line.type === "thread.started") {
164
+ threadId = line.thread_id ?? threadId;
165
+ return;
166
+ }
167
+
168
+ if (line.type === "item.started" && isToolItem(line.item)) {
169
+ if (line.item.server === "orcats" && line.item.tool === "ask_user") {
170
+ if (options.interactive) {
171
+ // Surface the question for the renderer only. The answer travels
172
+ // Codex <-> Orcats MCP HTTP bridge and arrives on the matching
173
+ // `item.completed`, which the tool-result branch emits below.
174
+ const question = askUserQuestion(line.item.arguments);
175
+ await conversation.emit({ type: "user_question", question });
176
+ return;
177
+ }
178
+ controller.abort();
179
+ conversation.fail(
180
+ unsupportedFeature(
181
+ "codex ask_user",
182
+ "Codex ask_user MCP bridge requires an explicit interactive conversation"
183
+ )
184
+ );
185
+ return;
186
+ }
187
+ await conversation.emit({
188
+ type: "assistant_tool_call",
189
+ id: line.item.id ?? "",
190
+ name: toolName(line.item),
191
+ input: toolInput(line.item)
192
+ });
193
+ return;
194
+ }
195
+
196
+ if (line.type === "item.completed" && isToolItem(line.item)) {
197
+ await conversation.emit({
198
+ type: "tool_result",
199
+ toolCallId: line.item.id ?? "",
200
+ output: toolOutput(line.item),
201
+ ...(line.item.error ? { isError: true } : {})
202
+ });
203
+ if (line.item.error) {
204
+ controller.abort();
205
+ conversation.fail(backendFailed("codex", line.item.error));
206
+ }
207
+ return;
208
+ }
209
+
210
+ if (line.type === "item.completed" && line.item?.type === "agent_message") {
211
+ const text = line.item.text ?? "";
212
+ output += text;
213
+ lastAgentText = text;
214
+ await conversation.emit({ type: "assistant_text_delta", text });
215
+ return;
216
+ }
217
+
218
+ if (line.type === "turn.completed") {
219
+ await conversation.emit({ type: "assistant_turn_end" });
220
+ // Codex emits one agent_message per step (progress updates + final answer).
221
+ // Only the last message is the structured result; using `output` (all steps
222
+ // concatenated) produces invalid JSON when the schema is set.
223
+ const structured = parseCodexStructuredOutput(options.schema, lastAgentText || output);
224
+ if (structured.type === "failed") {
225
+ controller.abort();
226
+ conversation.fail(structured.error);
227
+ return;
228
+ }
229
+
230
+ controller.abort();
231
+ conversation.succeed({
232
+ backend: "codex",
233
+ sessionId: sessionId("codex", threadId),
234
+ output,
235
+ ...(structured.value === undefined ? {} : { structured: structured.value }),
236
+ usage: normalizeCodexUsage(line.usage)
237
+ });
238
+ }
239
+ },
240
+
241
+ finish(): void {
242
+ if (controller.signal.aborted) {
243
+ return;
244
+ }
245
+ controller.abort();
246
+ conversation.fail(backendFailed("codex", "codex stream ended before turn.completed"));
247
+ }
248
+ };
249
+ }
250
+
251
+ function parseCodexStructuredOutput<Output>(
252
+ schema: z.ZodType<Output> | undefined,
253
+ output: string
254
+ ):
255
+ | { readonly type: "success"; readonly value?: Output }
256
+ | { readonly type: "failed"; readonly error: RuntimeError } {
257
+ if (!schema) {
258
+ return { type: "success" };
259
+ }
260
+
261
+ let raw: unknown;
262
+ try {
263
+ raw = JSON.parse(output) as unknown;
264
+ } catch {
265
+ return {
266
+ type: "failed",
267
+ error: structuredOutputValidationFailed({
268
+ raw: output,
269
+ issues: ["Codex structured output was not valid JSON"]
270
+ })
271
+ };
272
+ }
273
+
274
+ const parsed = parseStructuredOutput(schema, raw);
275
+ if (parsed.isErr()) {
276
+ return { type: "failed", error: parsed.error };
277
+ }
278
+
279
+ return { type: "success", value: parsed.value.value };
280
+ }
281
+
282
+ function isToolItem(item: CodexLine["item"]): item is NonNullable<CodexLine["item"]> {
283
+ return (
284
+ item?.type === "mcp_tool_call" ||
285
+ item?.type === "command_execution" ||
286
+ item?.type === "file_change" ||
287
+ item?.type === "tool_call"
288
+ );
289
+ }
290
+
291
+ function toolName(item: NonNullable<CodexLine["item"]>): string {
292
+ if (item.type === "mcp_tool_call") {
293
+ return `${item.server ?? ""}.${item.tool ?? ""}`;
294
+ }
295
+ return item.name ?? item.tool ?? item.type ?? "tool_call";
296
+ }
297
+
298
+ function toolInput(item: NonNullable<CodexLine["item"]>): unknown {
299
+ if (item.arguments !== undefined) {
300
+ return item.arguments;
301
+ }
302
+ if (item.command !== undefined) {
303
+ return { command: item.command };
304
+ }
305
+ if (item.path !== undefined || item.changes !== undefined) {
306
+ return {
307
+ ...(item.path === undefined ? {} : { path: item.path }),
308
+ ...(item.changes === undefined ? {} : { changes: item.changes })
309
+ };
310
+ }
311
+ return {};
312
+ }
313
+
314
+ function toolOutput(item: NonNullable<CodexLine["item"]>): unknown {
315
+ if (item.output !== undefined) {
316
+ return item.output;
317
+ }
318
+ const content = item.result?.content?.map((part) => part.text ?? "").join("") ?? "";
319
+ if (content.length > 0) {
320
+ return content;
321
+ }
322
+ return item.error ?? "";
323
+ }
324
+
325
+ function askUserQuestion(input: unknown): string {
326
+ if (typeof input === "object" && input !== null && "question" in input) {
327
+ const question = (input as { readonly question?: unknown }).question;
328
+ return typeof question === "string" ? question : "";
329
+ }
330
+ return "";
331
+ }
332
+
333
+ function normalizeCodexUsage(usage: CodexLine["usage"]): Usage {
334
+ return {
335
+ input: usage?.input_tokens ?? 0,
336
+ output: usage?.output_tokens ?? 0,
337
+ ...(usage?.reasoning_output_tokens ? { reasoning: usage.reasoning_output_tokens } : {})
338
+ };
339
+ }
@@ -0,0 +1,177 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { z } from "zod";
5
+ import {
6
+ codexExecJsonlArgs,
7
+ createCodexJsonlConsumer,
8
+ type CodexExecArgs
9
+ } from "./codex-jsonl.ts";
10
+ import { composeBackendPrompt } from "./conversation-config.ts";
11
+ import {
12
+ errorMessage,
13
+ runSubprocessConversation,
14
+ type SubprocessProcess,
15
+ type SubprocessSpawner
16
+ } from "./subprocess-run.ts";
17
+ import type { AutonomousRequest } from "./types.ts";
18
+ import {
19
+ createAskUserMcpServer,
20
+ type AskUserMcpServer,
21
+ type AskUserMcpServerOptions,
22
+ type AskUserResponder,
23
+ type StreamConversation
24
+ } from "../conversation/index.ts";
25
+ import {
26
+ backendFailed,
27
+ jsonSchemaFromZod,
28
+ type BackendApprovalPolicy,
29
+ type BackendConfig,
30
+ type BackendSandboxMode
31
+ } from "../model/index.ts";
32
+
33
+ export interface CodexBackendOptions
34
+ extends Omit<CodexExecArgs, "prompt" | "outputSchemaPath" | "resumeSessionId" | "mcpServerUrl"> {
35
+ readonly command?: string;
36
+ readonly cwd?: string;
37
+ readonly env?: NodeJS.ProcessEnv;
38
+ readonly capacity?: number;
39
+ readonly spawnProcess?: CodexProcessSpawner;
40
+ readonly config?: BackendConfig<"codex">;
41
+ readonly askUser?: AskUserResponder;
42
+ readonly createAskUserServer?: (options: AskUserMcpServerOptions) => AskUserMcpServer;
43
+ readonly inactivityTimeoutMs?: number;
44
+ readonly wallClockTimeoutMs?: number;
45
+ }
46
+
47
+ /** Codex's subprocess handle is the shared {@link SubprocessProcess}; the alias
48
+ * is kept for the public API (tests and flow code import `CodexProcess`). */
49
+ export type CodexProcess = SubprocessProcess;
50
+
51
+ export type CodexProcessSpawner = SubprocessSpawner;
52
+
53
+ export interface ResolvedCodexConfig<Output> {
54
+ model?: string;
55
+ systemPrompt?: string;
56
+ approvalPolicy?: BackendApprovalPolicy;
57
+ sandbox?: BackendSandboxMode;
58
+ readOnly?: boolean;
59
+ ignoreUserConfig?: boolean;
60
+ selfManagedGit?: boolean;
61
+ retryAttempts?: number;
62
+ schema?: z.ZodType<Output>;
63
+ outputSchemaPath?: string;
64
+ resumeSessionId?: string;
65
+ interactive?: boolean;
66
+ mcpServerUrl?: string;
67
+ }
68
+
69
+ export async function runCodexConversation<Output>(
70
+ request: AutonomousRequest<Output, "codex">,
71
+ options: CodexBackendOptions,
72
+ conversation: StreamConversation<"codex">,
73
+ setProcess: (process: CodexProcess) => void
74
+ ): Promise<void> {
75
+ const command = options.command ?? "codex";
76
+ const config = resolveCodexConfig(request, options);
77
+ let schemaDir: string | undefined;
78
+ let askUserServer: AskUserMcpServer | undefined;
79
+
80
+ try {
81
+ if (config.interactive) {
82
+ askUserServer = (options.createAskUserServer ?? createAskUserMcpServer)({
83
+ responder:
84
+ options.askUser ??
85
+ (() => {
86
+ throw new Error("interactive ask_user responder is not configured");
87
+ })
88
+ });
89
+ config.mcpServerUrl = askUserServer.url;
90
+ }
91
+
92
+ if (config.schema) {
93
+ schemaDir = await mkdtemp(join(tmpdir(), "orca-codex-schema-"));
94
+ config.outputSchemaPath = join(schemaDir, "output-schema.json");
95
+ await writeFile(
96
+ config.outputSchemaPath,
97
+ `${JSON.stringify(jsonSchemaFromZod(config.schema), null, 2)}\n`
98
+ );
99
+ }
100
+
101
+ const args = codexExecJsonlArgs({
102
+ prompt: composeBackendPrompt(request.prompt, config),
103
+ ...(config.model === undefined ? {} : { model: config.model }),
104
+ ...(config.approvalPolicy === undefined ? {} : { approvalPolicy: config.approvalPolicy }),
105
+ ...(config.sandbox === undefined ? {} : { sandbox: config.sandbox }),
106
+ ...(config.readOnly === undefined ? {} : { readOnly: config.readOnly }),
107
+ ...(config.ignoreUserConfig === undefined ? {} : { ignoreUserConfig: config.ignoreUserConfig }),
108
+ ...(config.outputSchemaPath === undefined ? {} : { outputSchemaPath: config.outputSchemaPath }),
109
+ ...(config.resumeSessionId === undefined ? {} : { resumeSessionId: config.resumeSessionId }),
110
+ ...(config.mcpServerUrl === undefined ? {} : { mcpServerUrl: config.mcpServerUrl })
111
+ });
112
+
113
+ await runSubprocessConversation({
114
+ backend: "codex",
115
+ command,
116
+ args,
117
+ conversation,
118
+ setProcess,
119
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
120
+ ...(options.env === undefined ? {} : { env: options.env }),
121
+ ...(options.spawnProcess === undefined ? {} : { spawnProcess: options.spawnProcess }),
122
+ ...(options.inactivityTimeoutMs === undefined ? {} : { inactivityTimeoutMs: options.inactivityTimeoutMs }),
123
+ ...(options.wallClockTimeoutMs === undefined ? {} : { wallClockTimeoutMs: options.wallClockTimeoutMs }),
124
+ createConsumer: () =>
125
+ createCodexJsonlConsumer(conversation, {
126
+ ...(config.schema === undefined ? {} : { schema: config.schema }),
127
+ // The answer is routed by the MCP HTTP bridge (askUserServer), not the
128
+ // consumer; the consumer only surfaces the question for the renderer.
129
+ ...(config.interactive ? { interactive: true } : {})
130
+ })
131
+ });
132
+ } catch (error) {
133
+ if (!conversation.signal.aborted) {
134
+ conversation.fail(backendFailed("codex", errorMessage(error)));
135
+ }
136
+ } finally {
137
+ await askUserServer?.close();
138
+ if (schemaDir) {
139
+ await rm(schemaDir, { recursive: true, force: true });
140
+ }
141
+ }
142
+ }
143
+
144
+ export function resolveCodexConfig<Output>(
145
+ request: AutonomousRequest<Output, "codex">,
146
+ options: CodexBackendOptions
147
+ ): ResolvedCodexConfig<Output> {
148
+ const optionConfig = options.config;
149
+ const requestConfig = request.config;
150
+ const config: ResolvedCodexConfig<Output> = {};
151
+ const model = requestConfig?.model ?? optionConfig?.model ?? options.model;
152
+ if (model !== undefined) config.model = model;
153
+ const systemPrompt = requestConfig?.systemPrompt ?? optionConfig?.systemPrompt;
154
+ if (systemPrompt !== undefined) config.systemPrompt = systemPrompt;
155
+ const approvalPolicy =
156
+ requestConfig?.approvalPolicy ?? optionConfig?.approvalPolicy ?? options.approvalPolicy;
157
+ if (approvalPolicy !== undefined) config.approvalPolicy = approvalPolicy;
158
+ const sandbox = requestConfig?.sandbox ?? optionConfig?.sandbox ?? options.sandbox;
159
+ if (sandbox !== undefined) config.sandbox = sandbox;
160
+ const readOnly = requestConfig?.readOnly ?? optionConfig?.readOnly ?? options.readOnly;
161
+ if (readOnly !== undefined) config.readOnly = readOnly;
162
+ if (options.ignoreUserConfig !== undefined) config.ignoreUserConfig = options.ignoreUserConfig;
163
+ const selfManagedGit = requestConfig?.selfManagedGit ?? optionConfig?.selfManagedGit;
164
+ if (selfManagedGit !== undefined) config.selfManagedGit = selfManagedGit;
165
+ const retryAttempts = requestConfig?.retry?.attempts ?? optionConfig?.retry?.attempts;
166
+ if (retryAttempts !== undefined) config.retryAttempts = retryAttempts;
167
+ const schema =
168
+ requestConfig?.structuredOutput?.schema ??
169
+ request.schema ??
170
+ (optionConfig?.structuredOutput?.schema as z.ZodType<Output> | undefined);
171
+ if (schema !== undefined) config.schema = schema;
172
+ if (requestConfig?.resumeSessionId !== undefined)
173
+ config.resumeSessionId = String(requestConfig.resumeSessionId);
174
+ const interactive = requestConfig?.interactive ?? optionConfig?.interactive;
175
+ if (interactive !== undefined) config.interactive = interactive;
176
+ return config;
177
+ }
@@ -0,0 +1,84 @@
1
+ import type { AutonomousRequest, LlmBackend } from "./types.ts";
2
+ import {
3
+ codexAcpCommand,
4
+ experimentalAcpBackendEnabled,
5
+ runAcpConversation
6
+ } from "./acp-run.ts";
7
+ import {
8
+ resolveCodexConfig,
9
+ runCodexConversation,
10
+ type CodexBackendOptions,
11
+ type CodexProcess
12
+ } from "./codex-run.ts";
13
+ import { StreamConversation } from "../conversation/index.ts";
14
+
15
+ export type {
16
+ CodexBackendOptions,
17
+ CodexProcess,
18
+ CodexProcessSpawner
19
+ } from "./codex-run.ts";
20
+
21
+ export function codex(options: CodexBackendOptions = {}): LlmBackend<"codex"> {
22
+ return {
23
+ tag: "codex",
24
+ autonomous<Output = unknown>(request: AutonomousRequest<Output, "codex">) {
25
+ let child: CodexProcess | undefined;
26
+ let cancelAcp: (() => Promise<void>) | undefined;
27
+ const useAcp = experimentalAcpBackendEnabled("codex");
28
+ const conversation = new StreamConversation({
29
+ backend: "codex",
30
+ capacity: options.capacity ?? 256,
31
+ canAskUser: request.config?.interactive ?? options.config?.interactive ?? false,
32
+ onCancel: async () => {
33
+ if (useAcp && cancelAcp) {
34
+ await cancelAcp();
35
+ return;
36
+ }
37
+ child?.kill("SIGTERM");
38
+ }
39
+ });
40
+
41
+ queueMicrotask(() => {
42
+ if (conversation.signal.aborted) {
43
+ return;
44
+ }
45
+ if (useAcp) {
46
+ const acp = codexAcpCommand();
47
+ const config = resolveCodexConfig(request, options);
48
+ void runAcpConversation(
49
+ request,
50
+ {
51
+ backend: "codex",
52
+ command: acp.command,
53
+ args: acp.args,
54
+ config,
55
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
56
+ ...(options.env === undefined ? {} : { env: options.env }),
57
+ ...(options.wallClockTimeoutMs === undefined ? {} : { requestTimeoutMs: options.wallClockTimeoutMs }),
58
+ ...(options.inactivityTimeoutMs === undefined ? {} : { inactivityTimeoutMs: options.inactivityTimeoutMs }),
59
+ setProcess: (process) => {
60
+ child = process;
61
+ if (conversation.signal.aborted) {
62
+ process.kill("SIGTERM");
63
+ }
64
+ },
65
+ setCancel: (cancel) => {
66
+ cancelAcp = cancel;
67
+ }
68
+ },
69
+ conversation
70
+ );
71
+ return;
72
+ }
73
+ void runCodexConversation(request, options, conversation, (process) => {
74
+ child = process;
75
+ if (conversation.signal.aborted) {
76
+ process.kill("SIGTERM");
77
+ }
78
+ });
79
+ });
80
+
81
+ return conversation;
82
+ }
83
+ };
84
+ }
@@ -0,0 +1,26 @@
1
+ import type { z } from "zod";
2
+
3
+ export interface SharedBackendConfig<Output> {
4
+ model?: string;
5
+ systemPrompt?: string;
6
+ readOnly?: boolean;
7
+ selfManagedGit?: boolean;
8
+ retryAttempts?: number;
9
+ schema?: z.ZodType<Output>;
10
+ resumeSessionId?: string;
11
+ }
12
+
13
+ export function composeBackendPrompt(prompt: string, config: SharedBackendConfig<unknown>): string {
14
+ return [
15
+ config.systemPrompt ? `System instructions:\n${config.systemPrompt}` : "",
16
+ config.selfManagedGit === false
17
+ ? "Git policy: Orca is the parent runtime. Do not create commits, branches, pushes, or pull requests; leave repository mutation to the parent workflow."
18
+ : "",
19
+ config.retryAttempts === undefined
20
+ ? ""
21
+ : `Retry policy: maximum attempts ${String(config.retryAttempts)}.`,
22
+ prompt
23
+ ]
24
+ .filter((part) => part.length > 0)
25
+ .join("\n\n");
26
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./claude-run.ts";
2
+ export * from "./conversation-config.ts";
3
+ export * from "./claude-stream-json.ts";
4
+ export * from "./codex.ts";
5
+ export * from "./codex-jsonl.ts";
6
+ export * from "./opencode-run.ts";
7
+ export * from "./opencode-sse.ts";
8
+ export * from "./pi-rpc.ts";
9
+ export * from "./pi-run.ts";
10
+ export * from "./select.ts";
11
+ export * from "./subprocess-run.ts";
12
+ export * from "./types.ts";
13
+ export * from "./unsupported.ts";