@defrex/autobuild 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexAgentRunner (SPEC §9): AgentRunner over the locally installed OpenAI
|
|
3
|
+
* Codex CLI. Phase turns use Codex's JSONL exec protocol and native thread
|
|
4
|
+
* resumption; non-phase judgments run as isolated, tool-free ephemeral turns.
|
|
5
|
+
*/
|
|
6
|
+
import type { StreamPart } from '../../store/streams/types'
|
|
7
|
+
import { spawnCliStream } from './cli-stream'
|
|
8
|
+
import {
|
|
9
|
+
agentInvocation,
|
|
10
|
+
type AgentContinueOpts,
|
|
11
|
+
type AgentRunner,
|
|
12
|
+
type AgentSessionHandle,
|
|
13
|
+
type AgentStartOpts,
|
|
14
|
+
type AgentTurnFailure,
|
|
15
|
+
type AgentTurnResult,
|
|
16
|
+
type SessionStreamEmitter,
|
|
17
|
+
type Transcript,
|
|
18
|
+
} from '../types'
|
|
19
|
+
import type { OneShotCompletion, OneShotCompletionInput, OneShotCompletionResult } from './one-shot'
|
|
20
|
+
import { classifyProviderError, configurationFailure, credentialFailure } from './provider-error'
|
|
21
|
+
import {
|
|
22
|
+
abortPart,
|
|
23
|
+
errorPart,
|
|
24
|
+
finishPart,
|
|
25
|
+
finishStepPart,
|
|
26
|
+
promptPart,
|
|
27
|
+
reasoningDeltaPart,
|
|
28
|
+
reasoningEndPart,
|
|
29
|
+
reasoningStartPart,
|
|
30
|
+
startPart,
|
|
31
|
+
startStepPart,
|
|
32
|
+
textDeltaPart,
|
|
33
|
+
textEndPart,
|
|
34
|
+
textStartPart,
|
|
35
|
+
toolInputPart,
|
|
36
|
+
toolOutputPart,
|
|
37
|
+
} from './stream-parts'
|
|
38
|
+
import type { RuntimeUsabilityInput, RuntimeUsabilityResult } from './runtime'
|
|
39
|
+
import { sessionEnv } from './session-env'
|
|
40
|
+
|
|
41
|
+
export interface CodexCliInvocation {
|
|
42
|
+
/** Arguments after the `codex` executable. */
|
|
43
|
+
args: string[]
|
|
44
|
+
cwd: string
|
|
45
|
+
env: Record<string, string>
|
|
46
|
+
signal?: AbortSignal
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CodexCliResult {
|
|
50
|
+
stdout: string
|
|
51
|
+
stderr: string
|
|
52
|
+
exitCode: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Injectable direct-process boundary used by deterministic adapter tests. */
|
|
56
|
+
export type CodexCliRunFn = (invocation: CodexCliInvocation) => Promise<CodexCliResult>
|
|
57
|
+
|
|
58
|
+
/** A streaming CLI turn: decoded JSONL lines as they arrive, plus the
|
|
59
|
+
* completed result. The consumer accumulates the lines it needs. */
|
|
60
|
+
export interface CodexCliStreamHandle {
|
|
61
|
+
lines: AsyncIterable<string>
|
|
62
|
+
result: Promise<CodexCliResult>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Injectable streaming boundary: production spawns the same argv; tests
|
|
66
|
+
* script line-at-a-time output. */
|
|
67
|
+
export type CodexCliStreamFn = (invocation: CodexCliInvocation) => CodexCliStreamHandle
|
|
68
|
+
|
|
69
|
+
const runCodexCli: CodexCliRunFn = async (invocation) => {
|
|
70
|
+
const proc = Bun.spawn(['codex', ...invocation.args], {
|
|
71
|
+
cwd: invocation.cwd,
|
|
72
|
+
env: invocation.env,
|
|
73
|
+
stdin: 'ignore',
|
|
74
|
+
stdout: 'pipe',
|
|
75
|
+
stderr: 'pipe',
|
|
76
|
+
...(invocation.signal !== undefined ? { signal: invocation.signal } : {}),
|
|
77
|
+
})
|
|
78
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
79
|
+
new Response(proc.stdout).text(),
|
|
80
|
+
new Response(proc.stderr).text(),
|
|
81
|
+
proc.exited,
|
|
82
|
+
])
|
|
83
|
+
return { stdout, stderr, exitCode }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const runCodexCliStream: CodexCliStreamFn = (invocation) => spawnCliStream('codex', invocation)
|
|
87
|
+
|
|
88
|
+
/** Verify both the local executable and Codex login for init suggestions. */
|
|
89
|
+
export async function isCodexRuntimeUsable(
|
|
90
|
+
input: RuntimeUsabilityInput,
|
|
91
|
+
runCli: CodexCliRunFn = runCodexCli,
|
|
92
|
+
): Promise<RuntimeUsabilityResult> {
|
|
93
|
+
const env = Object.fromEntries(
|
|
94
|
+
Object.entries(input.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
95
|
+
)
|
|
96
|
+
try {
|
|
97
|
+
const result = await runCli({ args: ['login', 'status'], cwd: input.cwd, env })
|
|
98
|
+
return result.exitCode === 0
|
|
99
|
+
? { usable: true, reason: 'Codex CLI is installed and logged in' }
|
|
100
|
+
: {
|
|
101
|
+
usable: false,
|
|
102
|
+
reason: result.stderr.trim() || result.stdout.trim() || 'Codex is not logged in',
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
usable: false,
|
|
107
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface JsonRecord {
|
|
113
|
+
[key: string]: unknown
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface ParsedCodexOutput {
|
|
117
|
+
events: JsonRecord[]
|
|
118
|
+
malformedLines: string[]
|
|
119
|
+
threadId?: string
|
|
120
|
+
assistantText: string[]
|
|
121
|
+
usage: { inputTokens: number; outputTokens: number }
|
|
122
|
+
completed: boolean
|
|
123
|
+
failureMessages: string[]
|
|
124
|
+
statuses: number[]
|
|
125
|
+
codes: Array<string | number>
|
|
126
|
+
toolItems: string[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface CodexTurn {
|
|
130
|
+
text: string
|
|
131
|
+
usage: { inputTokens: number; outputTokens: number }
|
|
132
|
+
failure?: AgentTurnFailure
|
|
133
|
+
cli: CodexCliResult
|
|
134
|
+
events: JsonRecord[]
|
|
135
|
+
malformedLines: string[]
|
|
136
|
+
threadId?: string
|
|
137
|
+
toolItems: string[]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface TurnRecord {
|
|
141
|
+
turn: number
|
|
142
|
+
prompt: string
|
|
143
|
+
text: string
|
|
144
|
+
usage: { inputTokens: number; outputTokens: number }
|
|
145
|
+
failure?: AgentTurnFailure
|
|
146
|
+
cli: CodexCliResult
|
|
147
|
+
events: JsonRecord[]
|
|
148
|
+
malformedLines: string[]
|
|
149
|
+
threadId?: string
|
|
150
|
+
toolItems: string[]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface SessionState {
|
|
154
|
+
opts: AgentStartOpts
|
|
155
|
+
model?: string
|
|
156
|
+
/** Absent when start failed before Codex emitted `thread.started`. */
|
|
157
|
+
nativeThreadId?: string
|
|
158
|
+
turns: TurnRecord[]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const CODEX_JSON_ARG = '--json'
|
|
162
|
+
const CODEX_MODEL_ARG = '--model'
|
|
163
|
+
const CODEX_MODEL_ALIAS = '-m'
|
|
164
|
+
|
|
165
|
+
/** Structural separator before Codex's positional prompt. */
|
|
166
|
+
export const CODEX_PROMPT_BOUNDARY = '--'
|
|
167
|
+
|
|
168
|
+
/** Options that select the model or the JSONL protocol parsed below. */
|
|
169
|
+
export const CODEX_OWNED_ARGS = [CODEX_JSON_ARG, CODEX_MODEL_ARG, CODEX_MODEL_ALIAS] as const
|
|
170
|
+
|
|
171
|
+
const MISSING_CLI_MESSAGE =
|
|
172
|
+
'codex runtime: Codex CLI executable "codex" was not found. ' +
|
|
173
|
+
'Install the Codex CLI (https://developers.openai.com/codex/cli), run `codex login`, ' +
|
|
174
|
+
'and complete authentication before running Autobuild.'
|
|
175
|
+
|
|
176
|
+
const SHELL_ENV_INHERIT = 'shell_environment_policy.inherit=all'
|
|
177
|
+
|
|
178
|
+
/** Feature gates disabled for tool-free one-shot judgments. The isolated
|
|
179
|
+
* invocation also ignores user config/rules and runs in a read-only sandbox;
|
|
180
|
+
* emitted item kinds are still checked fail-closed for future Codex versions. */
|
|
181
|
+
const ONE_SHOT_DISABLED_FEATURES = [
|
|
182
|
+
'shell_tool',
|
|
183
|
+
'unified_exec',
|
|
184
|
+
'standalone_web_search',
|
|
185
|
+
'apps',
|
|
186
|
+
'plugins',
|
|
187
|
+
'multi_agent',
|
|
188
|
+
'multi_agent_v2',
|
|
189
|
+
'image_generation',
|
|
190
|
+
'computer_use',
|
|
191
|
+
'browser_use',
|
|
192
|
+
'browser_use_external',
|
|
193
|
+
'browser_use_full_cdp_access',
|
|
194
|
+
] as const
|
|
195
|
+
|
|
196
|
+
export class CodexAgentRunner implements AgentRunner, OneShotCompletion {
|
|
197
|
+
readonly name = 'codex'
|
|
198
|
+
|
|
199
|
+
private readonly runCli: CodexCliRunFn
|
|
200
|
+
private readonly runCliStream: CodexCliStreamFn | undefined
|
|
201
|
+
private readonly createSessionId: () => string
|
|
202
|
+
private readonly sessions = new Map<string, SessionState>()
|
|
203
|
+
|
|
204
|
+
constructor(
|
|
205
|
+
opts: {
|
|
206
|
+
runCli?: CodexCliRunFn
|
|
207
|
+
runCliStream?: CodexCliStreamFn
|
|
208
|
+
createSessionId?: () => string
|
|
209
|
+
} = {},
|
|
210
|
+
) {
|
|
211
|
+
this.runCli = opts.runCli ?? runCodexCli
|
|
212
|
+
// A test injecting only the buffered boundary takes the buffered
|
|
213
|
+
// translation path for streaming turns (degraded latency, same content);
|
|
214
|
+
// production gets the live streaming boundary.
|
|
215
|
+
this.runCliStream =
|
|
216
|
+
opts.runCliStream ?? (opts.runCli !== undefined ? undefined : runCodexCliStream)
|
|
217
|
+
this.createSessionId = opts.createSessionId ?? (() => crypto.randomUUID())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async complete(input: OneShotCompletionInput): Promise<OneShotCompletionResult> {
|
|
221
|
+
const args = [
|
|
222
|
+
'exec',
|
|
223
|
+
CODEX_JSON_ARG,
|
|
224
|
+
'--ephemeral',
|
|
225
|
+
'--ignore-user-config',
|
|
226
|
+
'--ignore-rules',
|
|
227
|
+
'--sandbox',
|
|
228
|
+
'read-only',
|
|
229
|
+
// The legacy web_search feature gates emit deprecation warning items in
|
|
230
|
+
// current Codex releases. This supported config value disables search
|
|
231
|
+
// without making a healthy one-shot look like tool activity.
|
|
232
|
+
'-c',
|
|
233
|
+
'web_search="disabled"',
|
|
234
|
+
]
|
|
235
|
+
for (const feature of ONE_SHOT_DISABLED_FEATURES) args.push('--disable', feature)
|
|
236
|
+
if (input.model !== undefined) args.push(CODEX_MODEL_ARG, input.model)
|
|
237
|
+
args.push(...(input.args ?? []), CODEX_PROMPT_BOUNDARY, input.prompt)
|
|
238
|
+
|
|
239
|
+
const turn = await this.runPrompt({
|
|
240
|
+
args,
|
|
241
|
+
cwd: input.cwd,
|
|
242
|
+
env: sessionEnv(input.env),
|
|
243
|
+
...(input.signal !== undefined ? { signal: input.signal } : {}),
|
|
244
|
+
})
|
|
245
|
+
if (turn.failure !== undefined) throw new Error(turn.failure.message)
|
|
246
|
+
if (turn.toolItems.length > 0) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`codex runtime: tool-free completion emitted tool item(s): ${turn.toolItems.join(', ')}`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
return { text: turn.text }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async start(
|
|
255
|
+
opts: AgentStartOpts,
|
|
256
|
+
): Promise<{ session: AgentSessionHandle; result: AgentTurnResult }> {
|
|
257
|
+
// Codex Agent Skills use `$name`, unlike Claude/Pi's slash invocation.
|
|
258
|
+
const prompt = `$${opts.skill} ${agentInvocation(opts)}`
|
|
259
|
+
const executed = await this.runTurn(prompt, opts)
|
|
260
|
+
const turn: CodexTurn =
|
|
261
|
+
executed.failure === undefined && executed.threadId === undefined
|
|
262
|
+
? {
|
|
263
|
+
...executed,
|
|
264
|
+
failure: classifyProviderError(
|
|
265
|
+
`${this.name} runtime: Codex CLI stream ended without a thread.started event`,
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
: executed
|
|
269
|
+
const id = turn.threadId ?? this.createSessionId()
|
|
270
|
+
const session: AgentSessionHandle = {
|
|
271
|
+
id,
|
|
272
|
+
runner: this.name,
|
|
273
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
274
|
+
}
|
|
275
|
+
this.sessions.set(id, {
|
|
276
|
+
opts,
|
|
277
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
278
|
+
...(turn.threadId !== undefined ? { nativeThreadId: turn.threadId } : {}),
|
|
279
|
+
turns: [this.turnRecord(1, prompt, turn)],
|
|
280
|
+
})
|
|
281
|
+
return { session, result: this.toResult(turn) }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async continue(
|
|
285
|
+
session: AgentSessionHandle,
|
|
286
|
+
message: string,
|
|
287
|
+
opts?: AgentContinueOpts,
|
|
288
|
+
): Promise<AgentTurnResult> {
|
|
289
|
+
const state = this.liveState(session, 'continue')
|
|
290
|
+
if (state.nativeThreadId === undefined) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`${this.name}: cannot continue session "${session.id}" because Codex start failed before thread.started`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
// §10/D8: a continued turn gets this round's env. §9: it also streams
|
|
296
|
+
// through THIS turn's emitter only — the start turn's emitter (if any)
|
|
297
|
+
// belongs to its own bracket and never receives this turn's parts,
|
|
298
|
+
// matching the pi adapter's per-turn forwarding.
|
|
299
|
+
const turnOpts: AgentStartOpts = {
|
|
300
|
+
...(opts?.env !== undefined
|
|
301
|
+
? { ...state.opts, env: { ...state.opts.env, ...opts.env } }
|
|
302
|
+
: state.opts),
|
|
303
|
+
stream: opts?.stream,
|
|
304
|
+
}
|
|
305
|
+
const turn = await this.runTurn(message, turnOpts, state.nativeThreadId, opts?.signal)
|
|
306
|
+
state.turns.push(this.turnRecord(state.turns.length + 1, message, turn))
|
|
307
|
+
return this.toResult(turn)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async end(session: AgentSessionHandle): Promise<Transcript> {
|
|
311
|
+
const state = this.liveState(session, 'end')
|
|
312
|
+
this.sessions.delete(session.id)
|
|
313
|
+
|
|
314
|
+
const usage = { inputTokens: 0, outputTokens: 0, turns: 0 }
|
|
315
|
+
for (const turn of state.turns) {
|
|
316
|
+
usage.inputTokens += turn.usage.inputTokens
|
|
317
|
+
usage.outputTokens += turn.usage.outputTokens
|
|
318
|
+
usage.turns += 1
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
content: JSON.stringify(
|
|
323
|
+
{
|
|
324
|
+
session: session.id,
|
|
325
|
+
...(state.nativeThreadId !== undefined ? { nativeThreadId: state.nativeThreadId } : {}),
|
|
326
|
+
skill: state.opts.skill,
|
|
327
|
+
invocation: agentInvocation(state.opts),
|
|
328
|
+
...(state.opts.buildSlug !== undefined ? { buildSlug: state.opts.buildSlug } : {}),
|
|
329
|
+
turns: state.turns,
|
|
330
|
+
},
|
|
331
|
+
null,
|
|
332
|
+
2,
|
|
333
|
+
),
|
|
334
|
+
metadata: {
|
|
335
|
+
runner: this.name,
|
|
336
|
+
...(state.model !== undefined ? { model: state.model } : {}),
|
|
337
|
+
usage,
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private liveState(session: AgentSessionHandle, op: 'continue' | 'end'): SessionState {
|
|
343
|
+
const state = this.sessions.get(session.id)
|
|
344
|
+
if (state === undefined) {
|
|
345
|
+
throw new Error(`${this.name}: ${op} on unknown session "${session.id}"`)
|
|
346
|
+
}
|
|
347
|
+
return state
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private async runTurn(
|
|
351
|
+
prompt: string,
|
|
352
|
+
opts: AgentStartOpts,
|
|
353
|
+
resume?: string,
|
|
354
|
+
signal: AbortSignal | undefined = opts.signal,
|
|
355
|
+
): Promise<CodexTurn> {
|
|
356
|
+
const args = ['exec']
|
|
357
|
+
if (resume !== undefined) args.push('resume')
|
|
358
|
+
args.push(
|
|
359
|
+
CODEX_JSON_ARG,
|
|
360
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
361
|
+
'--config',
|
|
362
|
+
SHELL_ENV_INHERIT,
|
|
363
|
+
)
|
|
364
|
+
if (opts.model !== undefined) args.push(CODEX_MODEL_ARG, opts.model)
|
|
365
|
+
args.push(...(opts.args ?? []))
|
|
366
|
+
if (resume !== undefined) args.push(resume)
|
|
367
|
+
args.push(CODEX_PROMPT_BOUNDARY, prompt)
|
|
368
|
+
const invocation: CodexCliInvocation = {
|
|
369
|
+
args,
|
|
370
|
+
cwd: opts.workspacePath,
|
|
371
|
+
env: sessionEnv(opts.env),
|
|
372
|
+
...(signal !== undefined ? { signal } : {}),
|
|
373
|
+
}
|
|
374
|
+
if (opts.stream === undefined) return this.runPrompt(invocation)
|
|
375
|
+
|
|
376
|
+
// Streaming turn: translate while the CLI runs. Without an injected
|
|
377
|
+
// streaming boundary (offline fakes), the buffered path still translates
|
|
378
|
+
// after completion — degraded latency, same content.
|
|
379
|
+
const emitter = opts.stream
|
|
380
|
+
emitter.append([promptPart(prompt), startPart(crypto.randomUUID())])
|
|
381
|
+
let turn: CodexTurn
|
|
382
|
+
if (this.runCliStream !== undefined) {
|
|
383
|
+
turn = await this.runStreamingPrompt(invocation, emitter)
|
|
384
|
+
} else {
|
|
385
|
+
turn = await this.runPrompt(invocation)
|
|
386
|
+
translateBufferedCodexTurn(turn, (parts) => emitter.append(parts))
|
|
387
|
+
}
|
|
388
|
+
this.emitTurnEnd(turn, emitter, signal)
|
|
389
|
+
return turn
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Completed-turn, failed-turn, and cancelled-turn closing parts. */
|
|
393
|
+
private emitTurnEnd(
|
|
394
|
+
turn: CodexTurn,
|
|
395
|
+
emitter: SessionStreamEmitter,
|
|
396
|
+
signal: AbortSignal | undefined,
|
|
397
|
+
): void {
|
|
398
|
+
if (turn.failure === undefined) {
|
|
399
|
+
emitter.append([finishPart()])
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
if (signal?.aborted) {
|
|
403
|
+
const reason = signal.reason
|
|
404
|
+
emitter.append([
|
|
405
|
+
abortPart(reason instanceof Error ? reason.message : 'codex runtime: turn aborted'),
|
|
406
|
+
])
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
emitter.append([errorPart(turn.failure.message)])
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Live translation path: consume JSONL lines as they arrive while the
|
|
413
|
+
* same records feed the ordinary accumulators. */
|
|
414
|
+
private async runStreamingPrompt(
|
|
415
|
+
invocation: CodexCliInvocation,
|
|
416
|
+
emitter: SessionStreamEmitter,
|
|
417
|
+
): Promise<CodexTurn> {
|
|
418
|
+
let handle: CodexCliStreamHandle
|
|
419
|
+
try {
|
|
420
|
+
handle = this.runCliStream!(invocation)
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return launchFailure(error)
|
|
423
|
+
}
|
|
424
|
+
const acc = new CodexOutputAccumulator()
|
|
425
|
+
const translator = createCodexStreamTranslator((parts) => emitter.append(parts))
|
|
426
|
+
try {
|
|
427
|
+
for await (const line of handle.lines) {
|
|
428
|
+
let value: unknown
|
|
429
|
+
try {
|
|
430
|
+
value = JSON.parse(line)
|
|
431
|
+
} catch {
|
|
432
|
+
acc.push(line)
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
if (!isRecord(value)) {
|
|
436
|
+
acc.push(line)
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
acc.pushRecord(value)
|
|
440
|
+
translator.onEvent(value)
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
// The result promise below still carries the CLI's exit and stderr.
|
|
444
|
+
}
|
|
445
|
+
const cli = await handle.result
|
|
446
|
+
return this.finishTurn(cli, acc.snapshot())
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private async runPrompt(invocation: CodexCliInvocation): Promise<CodexTurn> {
|
|
450
|
+
let cli: CodexCliResult
|
|
451
|
+
try {
|
|
452
|
+
cli = await this.runCli(invocation)
|
|
453
|
+
} catch (error) {
|
|
454
|
+
return launchFailure(error)
|
|
455
|
+
}
|
|
456
|
+
return this.finishTurn(cli, parseCodexOutput(cli.stdout))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Shared post-processing of a completed CLI run. */
|
|
460
|
+
private finishTurn(cli: CodexCliResult, parsed: ParsedCodexOutput): CodexTurn {
|
|
461
|
+
const text = parsed.assistantText.join('\n')
|
|
462
|
+
let failureMessage: string | undefined
|
|
463
|
+
let loggedOut = false
|
|
464
|
+
if (cli.exitCode !== 0 || parsed.failureMessages.length > 0) {
|
|
465
|
+
const provider = parsed.failureMessages.find((message) => message.length > 0)
|
|
466
|
+
const stderr = nonempty(cli.stderr)
|
|
467
|
+
const detail = provider ?? stderr
|
|
468
|
+
loggedOut = detail !== undefined && looksLoggedOut(detail)
|
|
469
|
+
if (loggedOut) {
|
|
470
|
+
failureMessage =
|
|
471
|
+
`codex runtime: Codex CLI executable "codex" is not authenticated. ` +
|
|
472
|
+
`Run \`codex login\` and complete authentication. ${detail}`
|
|
473
|
+
} else {
|
|
474
|
+
failureMessage =
|
|
475
|
+
detail ??
|
|
476
|
+
`${this.name} runtime: Codex CLI exited with code ${cli.exitCode} without error text`
|
|
477
|
+
}
|
|
478
|
+
} else if (parsed.malformedLines.length > 0) {
|
|
479
|
+
failureMessage = `${this.name} runtime: Codex CLI emitted malformed JSONL output`
|
|
480
|
+
} else if (!parsed.completed) {
|
|
481
|
+
failureMessage = `${this.name} runtime: Codex CLI stream ended without a turn.completed event`
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
text,
|
|
486
|
+
usage: parsed.usage,
|
|
487
|
+
...(failureMessage !== undefined
|
|
488
|
+
? {
|
|
489
|
+
failure: loggedOut
|
|
490
|
+
? credentialFailure(failureMessage)
|
|
491
|
+
: classifyProviderError(failureMessage, {
|
|
492
|
+
status: parsed.statuses[0],
|
|
493
|
+
codes: parsed.codes,
|
|
494
|
+
}),
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
|
+
cli,
|
|
498
|
+
events: parsed.events,
|
|
499
|
+
malformedLines: parsed.malformedLines,
|
|
500
|
+
...(parsed.threadId !== undefined ? { threadId: parsed.threadId } : {}),
|
|
501
|
+
toolItems: parsed.toolItems,
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private turnRecord(turnNumber: number, prompt: string, turn: CodexTurn): TurnRecord {
|
|
506
|
+
return {
|
|
507
|
+
turn: turnNumber,
|
|
508
|
+
prompt,
|
|
509
|
+
text: turn.text,
|
|
510
|
+
usage: turn.usage,
|
|
511
|
+
...(turn.failure !== undefined ? { failure: turn.failure } : {}),
|
|
512
|
+
cli: turn.cli,
|
|
513
|
+
events: turn.events,
|
|
514
|
+
malformedLines: turn.malformedLines,
|
|
515
|
+
...(turn.threadId !== undefined ? { threadId: turn.threadId } : {}),
|
|
516
|
+
toolItems: turn.toolItems,
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private toResult(turn: CodexTurn): AgentTurnResult {
|
|
521
|
+
const base = { text: turn.text, usage: { ...turn.usage, turns: 1 } }
|
|
522
|
+
return turn.failure === undefined
|
|
523
|
+
? { kind: 'completed', ...base }
|
|
524
|
+
: { kind: 'failed', ...base, failure: turn.failure }
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseCodexOutput(stdout: string): ParsedCodexOutput {
|
|
529
|
+
const acc = new CodexOutputAccumulator()
|
|
530
|
+
for (const line of stdout.split(/\r?\n/)) acc.push(line)
|
|
531
|
+
return acc.snapshot()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Incremental accumulator over Codex's JSONL exec protocol, shared by the
|
|
536
|
+
* buffered parse and the streaming path (which also feeds the stream
|
|
537
|
+
* translator below). Collects the same shape `parseCodexOutput` always
|
|
538
|
+
* returned so outcome classification is byte-identical in both paths.
|
|
539
|
+
*/
|
|
540
|
+
class CodexOutputAccumulator {
|
|
541
|
+
readonly events: JsonRecord[] = []
|
|
542
|
+
readonly malformedLines: string[] = []
|
|
543
|
+
readonly assistantText: string[] = []
|
|
544
|
+
readonly failureMessages: string[] = []
|
|
545
|
+
readonly statuses: number[] = []
|
|
546
|
+
readonly codes: Array<string | number> = []
|
|
547
|
+
readonly toolItems: string[] = []
|
|
548
|
+
usage = { inputTokens: 0, outputTokens: 0 }
|
|
549
|
+
completed = false
|
|
550
|
+
threadId: string | undefined
|
|
551
|
+
|
|
552
|
+
push(line: string): void {
|
|
553
|
+
if (line.trim() === '') return
|
|
554
|
+
let value: unknown
|
|
555
|
+
try {
|
|
556
|
+
value = JSON.parse(line)
|
|
557
|
+
} catch {
|
|
558
|
+
this.malformedLines.push(line)
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
if (!isRecord(value)) {
|
|
562
|
+
this.malformedLines.push(line)
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
this.pushRecord(value)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
pushRecord(value: JsonRecord): void {
|
|
569
|
+
this.events.push(value)
|
|
570
|
+
|
|
571
|
+
if (value.type === 'thread.started') {
|
|
572
|
+
const id = stringField(value, 'thread_id') ?? stringField(value, 'threadId')
|
|
573
|
+
if (id !== undefined && id.length > 0) this.threadId = id
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (value.type === 'item.completed' || value.type === 'item.started') {
|
|
577
|
+
const item = isRecord(value.item) ? value.item : undefined
|
|
578
|
+
const itemType = stringField(item, 'type')
|
|
579
|
+
if (value.type === 'item.completed' && itemType === 'agent_message') {
|
|
580
|
+
const text = itemText(item)
|
|
581
|
+
if (text !== undefined) this.assistantText.push(text)
|
|
582
|
+
}
|
|
583
|
+
// Codex represents non-fatal warnings/deprecation notices as `error`
|
|
584
|
+
// thread items. They are transcript evidence, not executed tools. Every
|
|
585
|
+
// other unknown item remains fail-closed so newly added capabilities
|
|
586
|
+
// cannot silently weaken one-shot isolation.
|
|
587
|
+
if (
|
|
588
|
+
itemType !== undefined &&
|
|
589
|
+
itemType !== 'agent_message' &&
|
|
590
|
+
itemType !== 'reasoning' &&
|
|
591
|
+
itemType !== 'error'
|
|
592
|
+
) {
|
|
593
|
+
this.toolItems.push(itemType)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (value.type === 'turn.completed') {
|
|
598
|
+
this.completed = true
|
|
599
|
+
const usage = isRecord(value.usage) ? value.usage : undefined
|
|
600
|
+
this.usage = {
|
|
601
|
+
inputTokens: tokenCount(usage?.input_tokens ?? usage?.inputTokens),
|
|
602
|
+
outputTokens: tokenCount(usage?.output_tokens ?? usage?.outputTokens),
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (value.type === 'turn.failed' || value.type === 'error') {
|
|
607
|
+
const nested = isRecord(value.error) ? value.error : undefined
|
|
608
|
+
const message =
|
|
609
|
+
stringField(nested, 'message') ??
|
|
610
|
+
stringField(value, 'message') ??
|
|
611
|
+
stringField(value, 'error')
|
|
612
|
+
if (message !== undefined) this.failureMessages.push(message)
|
|
613
|
+
collectHints(value, this)
|
|
614
|
+
if (nested !== undefined) collectHints(nested, this)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
snapshot(): ParsedCodexOutput {
|
|
619
|
+
return {
|
|
620
|
+
events: [...this.events],
|
|
621
|
+
malformedLines: [...this.malformedLines],
|
|
622
|
+
...(this.threadId !== undefined ? { threadId: this.threadId } : {}),
|
|
623
|
+
assistantText: [...this.assistantText],
|
|
624
|
+
usage: { ...this.usage },
|
|
625
|
+
completed: this.completed,
|
|
626
|
+
failureMessages: [...this.failureMessages],
|
|
627
|
+
statuses: [...this.statuses],
|
|
628
|
+
codes: [...this.codes],
|
|
629
|
+
toolItems: [...this.toolItems],
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function itemText(item: JsonRecord | undefined): string | undefined {
|
|
635
|
+
const direct = stringField(item, 'text')
|
|
636
|
+
if (direct !== undefined) return direct
|
|
637
|
+
const content = item?.content
|
|
638
|
+
if (!Array.isArray(content)) return undefined
|
|
639
|
+
const parts: string[] = []
|
|
640
|
+
for (const block of content) {
|
|
641
|
+
if (!isRecord(block)) continue
|
|
642
|
+
const text = stringField(block, 'text')
|
|
643
|
+
if (text !== undefined) parts.push(text)
|
|
644
|
+
}
|
|
645
|
+
return parts.length > 0 ? parts.join('') : undefined
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function collectHints(value: JsonRecord, acc: CodexOutputAccumulator): void {
|
|
649
|
+
for (const key of ['status', 'status_code', 'statusCode']) {
|
|
650
|
+
const status = value[key]
|
|
651
|
+
if (typeof status === 'number' && Number.isFinite(status)) acc.statuses.push(status)
|
|
652
|
+
}
|
|
653
|
+
for (const key of ['code', 'type', 'category']) {
|
|
654
|
+
const code = value[key]
|
|
655
|
+
if (typeof code === 'string' || typeof code === 'number') acc.codes.push(code)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function tokenCount(value: unknown): number {
|
|
660
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
664
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function stringField(value: JsonRecord | undefined, key: string): string | undefined {
|
|
668
|
+
const field = value?.[key]
|
|
669
|
+
return typeof field === 'string' ? field : undefined
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function nonempty(value: string | undefined): string | undefined {
|
|
673
|
+
return value !== undefined && value.length > 0 ? value : undefined
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function looksLoggedOut(message: string): boolean {
|
|
677
|
+
return /not logged in|not authenticated|codex login|authentication required/i.test(message)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isEnoent(error: unknown): boolean {
|
|
681
|
+
return isRecord(error) && error.code === 'ENOENT'
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function errorText(error: unknown): string {
|
|
685
|
+
return error instanceof Error ? error.message : String(error)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** A CLI that never ran: missing executable vs. a failed launch. */
|
|
689
|
+
function launchFailure(error: unknown): CodexTurn {
|
|
690
|
+
const missing = isEnoent(error)
|
|
691
|
+
const message = missing
|
|
692
|
+
? MISSING_CLI_MESSAGE
|
|
693
|
+
: `codex runtime: failed to launch Codex CLI executable "codex": ${errorText(error)}`
|
|
694
|
+
return {
|
|
695
|
+
text: '',
|
|
696
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
697
|
+
failure: missing ? configurationFailure(message) : classifyProviderError(message),
|
|
698
|
+
cli: { stdout: '', stderr: errorText(error), exitCode: -1 },
|
|
699
|
+
events: [],
|
|
700
|
+
malformedLines: [],
|
|
701
|
+
toolItems: [],
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Live stream translation (SPEC §9): turn.started → start-step; tool-shaped
|
|
707
|
+
* item.started → tool-input-available; item.completed → tool output, whole
|
|
708
|
+
* agent-message text, or reasoning parts; item.updated → text deltas when it
|
|
709
|
+
* carries incremental text. Unrecognized records produce no parts.
|
|
710
|
+
*
|
|
711
|
+
* Items without an `id` still correlate across started/updated/completed:
|
|
712
|
+
* each item kind's events arrive sequentially, so a stable per-kind key is
|
|
713
|
+
* assigned when the item is first seen and reused until the item completes.
|
|
714
|
+
*/
|
|
715
|
+
function createCodexStreamTranslator(emit: (parts: StreamPart[]) => void): {
|
|
716
|
+
onEvent(event: JsonRecord): void
|
|
717
|
+
} {
|
|
718
|
+
const openText = new Set<string>()
|
|
719
|
+
const keys = createItemKeyTracker()
|
|
720
|
+
const itemKey = keys.keyFor
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
onEvent(event) {
|
|
724
|
+
switch (event.type) {
|
|
725
|
+
case 'turn.started':
|
|
726
|
+
emit([startStepPart()])
|
|
727
|
+
break
|
|
728
|
+
case 'item.started': {
|
|
729
|
+
const item = isRecord(event.item) ? event.item : undefined
|
|
730
|
+
const itemType = stringField(item, 'type')
|
|
731
|
+
// agent_message/reasoning items stream at item.updated/item.completed;
|
|
732
|
+
// a bare item.started opens nothing yet (the first delta does).
|
|
733
|
+
if (itemType === undefined || itemType === 'agent_message' || itemType === 'reasoning') {
|
|
734
|
+
break
|
|
735
|
+
}
|
|
736
|
+
emit([toolInputPart(itemKey(item, itemType), itemType, itemCommand(item) ?? item)])
|
|
737
|
+
break
|
|
738
|
+
}
|
|
739
|
+
case 'item.updated': {
|
|
740
|
+
const item = isRecord(event.item) ? event.item : undefined
|
|
741
|
+
if (item === undefined) break
|
|
742
|
+
if (stringField(item, 'type') !== 'agent_message') break
|
|
743
|
+
const delta = itemText(item)
|
|
744
|
+
if (delta === undefined || delta.length === 0) break
|
|
745
|
+
const id = itemKey(item, 'agent_message')
|
|
746
|
+
if (!openText.has(id)) emit([textStartPart(id)])
|
|
747
|
+
openText.add(id)
|
|
748
|
+
emit([textDeltaPart(id, delta)])
|
|
749
|
+
break
|
|
750
|
+
}
|
|
751
|
+
case 'item.completed': {
|
|
752
|
+
const item = isRecord(event.item) ? event.item : undefined
|
|
753
|
+
const itemType = stringField(item, 'type')
|
|
754
|
+
if (itemType === 'agent_message') {
|
|
755
|
+
const id = itemKey(item, itemType)
|
|
756
|
+
const text = itemText(item)
|
|
757
|
+
if (openText.has(id)) {
|
|
758
|
+
emit([textEndPart(id)])
|
|
759
|
+
openText.delete(id)
|
|
760
|
+
} else if (text !== undefined && text.length > 0) {
|
|
761
|
+
emit([textStartPart(id), textDeltaPart(id, text), textEndPart(id)])
|
|
762
|
+
}
|
|
763
|
+
keys.complete(item, itemType)
|
|
764
|
+
break
|
|
765
|
+
}
|
|
766
|
+
if (itemType === 'reasoning') {
|
|
767
|
+
const id = itemKey(item, itemType)
|
|
768
|
+
const text = itemText(item)
|
|
769
|
+
if (text !== undefined && text.length > 0) {
|
|
770
|
+
emit([reasoningStartPart(id), reasoningDeltaPart(id, text), reasoningEndPart(id)])
|
|
771
|
+
}
|
|
772
|
+
keys.complete(item, itemType)
|
|
773
|
+
break
|
|
774
|
+
}
|
|
775
|
+
if (itemType === undefined) break
|
|
776
|
+
emit([toolOutputPart(itemKey(item, itemType), itemCommand(item) ?? item)])
|
|
777
|
+
keys.complete(item, itemType)
|
|
778
|
+
break
|
|
779
|
+
}
|
|
780
|
+
case 'turn.completed':
|
|
781
|
+
keys.clear()
|
|
782
|
+
emit([finishStepPart()])
|
|
783
|
+
break
|
|
784
|
+
default:
|
|
785
|
+
break
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Codex items carry their human-readable payload in `command`/`input`. */
|
|
792
|
+
function itemCommand(item: JsonRecord | undefined): unknown {
|
|
793
|
+
const command = item?.command
|
|
794
|
+
if (typeof command === 'string') return command
|
|
795
|
+
const input = item?.input
|
|
796
|
+
if (input !== undefined) return input
|
|
797
|
+
return undefined
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Stable per-item keys (SPEC §9 translation): items carrying an `id` key by
|
|
802
|
+
* it; items without one key by kind and occurrence — each kind's events
|
|
803
|
+
* arrive sequentially, so the slot assigned when the item is first seen is
|
|
804
|
+
* reused until it completes, and a per-turn reset frees the counters.
|
|
805
|
+
*/
|
|
806
|
+
function createItemKeyTracker(): {
|
|
807
|
+
keyFor(item: JsonRecord | undefined, itemType: string | undefined): string
|
|
808
|
+
complete(item: JsonRecord | undefined, itemType: string | undefined): void
|
|
809
|
+
clear(): void
|
|
810
|
+
} {
|
|
811
|
+
const counters = new Map<string, number>()
|
|
812
|
+
const lastIdless = new Map<string, string>()
|
|
813
|
+
return {
|
|
814
|
+
keyFor(item, itemType) {
|
|
815
|
+
const id = stringField(item, 'id')
|
|
816
|
+
if (id !== undefined && id.length > 0) return id
|
|
817
|
+
const kind = itemType ?? 'item'
|
|
818
|
+
const known = lastIdless.get(kind)
|
|
819
|
+
if (known !== undefined) return known
|
|
820
|
+
const n = (counters.get(kind) ?? 0) + 1
|
|
821
|
+
counters.set(kind, n)
|
|
822
|
+
const key = `cx-${kind}-${n}`
|
|
823
|
+
lastIdless.set(kind, key)
|
|
824
|
+
return key
|
|
825
|
+
},
|
|
826
|
+
/** An id-less item's kind slot frees up when the item completes. */
|
|
827
|
+
complete(item, itemType) {
|
|
828
|
+
if (stringField(item, 'id') === undefined) lastIdless.delete(itemType ?? 'item')
|
|
829
|
+
},
|
|
830
|
+
clear() {
|
|
831
|
+
lastIdless.clear()
|
|
832
|
+
},
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Buffered-path translation (degraded latency, same content): walk the
|
|
838
|
+
* completed turn's events and emit whole messages — start-step/finish-step
|
|
839
|
+
* around the turn, text/reasoning parts from items, and tool input/output
|
|
840
|
+
* from non-message items.
|
|
841
|
+
*/
|
|
842
|
+
function translateBufferedCodexTurn(turn: CodexTurn, emit: (parts: StreamPart[]) => void): void {
|
|
843
|
+
const keys = createItemKeyTracker()
|
|
844
|
+
emit([startStepPart()])
|
|
845
|
+
for (const event of turn.events) {
|
|
846
|
+
if (event.type !== 'item.completed' && event.type !== 'item.started') continue
|
|
847
|
+
const item = isRecord(event.item) ? event.item : undefined
|
|
848
|
+
const itemType = stringField(item, 'type')
|
|
849
|
+
if (itemType === 'agent_message' && event.type === 'item.completed') {
|
|
850
|
+
const text = itemText(item)
|
|
851
|
+
if (text !== undefined && text.length > 0) {
|
|
852
|
+
const id = keys.keyFor(item, itemType)
|
|
853
|
+
emit([textStartPart(id), textDeltaPart(id, text), textEndPart(id)])
|
|
854
|
+
}
|
|
855
|
+
keys.complete(item, itemType)
|
|
856
|
+
} else if (itemType === 'reasoning' && event.type === 'item.completed') {
|
|
857
|
+
const text = itemText(item)
|
|
858
|
+
if (text !== undefined && text.length > 0) {
|
|
859
|
+
const id = keys.keyFor(item, itemType)
|
|
860
|
+
emit([reasoningStartPart(id), reasoningDeltaPart(id, text), reasoningEndPart(id)])
|
|
861
|
+
}
|
|
862
|
+
keys.complete(item, itemType)
|
|
863
|
+
} else if (itemType !== undefined && itemType !== 'agent_message' && itemType !== 'reasoning') {
|
|
864
|
+
// Mirror the live mapping: input at item.started, output at completed.
|
|
865
|
+
const id = keys.keyFor(item, itemType)
|
|
866
|
+
if (event.type === 'item.started')
|
|
867
|
+
emit([toolInputPart(id, itemType, itemCommand(item) ?? item)])
|
|
868
|
+
else emit([toolOutputPart(id, itemCommand(item) ?? item)])
|
|
869
|
+
if (event.type === 'item.completed') keys.complete(item, itemType)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
emit([finishStepPart()])
|
|
873
|
+
}
|