@khorsheed/dsh-local-agent-codex 0.1.0-rc.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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * One-shot Codex CLI subagent lifecycle: spawn `codex exec "<task>"` through
3
+ * the subprocess seam under the harness's scoped home, capture its printed
4
+ * response as the run output, and dispose to whole-tree quiescence. Mirrors
5
+ * the kimi-cli provider: every accepted run is a fresh process and a fresh
6
+ * codex session; there is no continuation across runs.
7
+ *
8
+ * The provider name `codex-local` avoids colliding with the official
9
+ * `subagent-codex` package's provider name `codex` — a composition that
10
+ * mounts both would fail loud with DUPLICATE_PROVIDER. The tool row in this
11
+ * bundle's patch takes the official model-facing name `subagent_codex`
12
+ * instead: the official preset row ships disabled, and the patch disables
13
+ * it too (a deliberate user re-enable conflicts loud, by design).
14
+ * @module @khorsheed/dsh-local-agent-codex/codex-cli-provider
15
+ */
16
+ import type { ContentBlock, TokenUsage } from '@deepseek-ai/dsh-llm';
17
+ import { type ResolvedSubagentStartRequest, type SubagentCapabilities, type SubagentProvider, type SubagentRun, type SubagentStartRequest, type SubagentStopReason } from '@deepseek-ai/dsh-subagent';
18
+ import type { Context } from '@deepseek-ai/cordis';
19
+ import type { Session } from '@deepseek-ai/dsh-session';
20
+ import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
21
+ import type { DelegationExecTarget, LocalAgentToolCalls } from '@khorsheed/dsh-local-agent/types';
22
+ import type { CodexLiveDriver } from './live-driver.ts';
23
+ /** Default POSIX grace between subprocess termination tiers. */
24
+ export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
25
+ /**
26
+ * Auth-shaped failure signatures in codex's stderr or event stream: the
27
+ * endpoint rejected the credential. Narrow on purpose — matched against
28
+ * provider-controlled error output only.
29
+ */
30
+ export declare const CODEX_AUTH_FAILURE: RegExp;
31
+ /** Codex sandbox policy values accepted by `codex exec --sandbox`. */
32
+ export type CodexSandbox = 'read-only' | 'workspace-write' | 'danger-full-access';
33
+ /**
34
+ * Member channel registration for one codex process lifetime: mint the
35
+ * per-run token and build the bridge MCP declaration as a per-process `-c`
36
+ * config override (codex takes inline TOML — spike-verified end-to-end
37
+ * against the real CLI on 2026-08-20, including the model-call leg).
38
+ * `default_tools_approval_mode="approve"` is required: codex's stable MCP
39
+ * elicitation gate auto-cancels tools lacking a readOnlyHint in
40
+ * non-interactive mode ("user cancelled MCP tool call"), and the bridge's
41
+ * member_message is a write tool. Nothing is written to the scoped home, so
42
+ * there is nothing to prune at settle; `release` only invalidates the token.
43
+ * Returns undefined when the mounted core predates the member channel
44
+ * (declare-and-degrade: the run proceeds unchanged). Token-only auth: host
45
+ * 0.1.5 removed the child pid the parentage cross-check used; the per-run
46
+ * token is the sole credential (see the package README's threat model). The
47
+ * exec driver registers per round; the live driver registers per resident
48
+ * process and releases on reclaim.
49
+ */
50
+ export interface CodexMemberRunHandle {
51
+ readonly token: string;
52
+ readonly configOverride: string;
53
+ release(): void;
54
+ }
55
+ /** Register one member run with the channel; see {@link CodexMemberRunHandle}. */
56
+ export declare function registerCodexMemberRun(ctx: Context, childSessionId: string, parentSessionId: string): CodexMemberRunHandle | undefined;
57
+ /**
58
+ * The codex build `codex --version` reports, probed through the shared
59
+ * subprocess seam inside the SCOPED home (never the user's own installation)
60
+ * and cached by the family probe against the executable's identity. Both the
61
+ * effective-settings snapshot and a settled round's read-back use it — the
62
+ * round prefers its own rollout head, which names the build that actually ran.
63
+ * @param ctx - host context carrying the subprocess seam.
64
+ * @param homeDir - the harness's scoped home.
65
+ * @returns the version, or undefined when the CLI cannot be asked.
66
+ */
67
+ export declare function codexCliVersion(ctx: Context, homeDir: string): Promise<string | undefined>;
68
+ export declare class CodexCliProvider implements SubagentProvider {
69
+ private readonly ctx;
70
+ private readonly sandbox;
71
+ private readonly live?;
72
+ private readonly model?;
73
+ private readonly overrides?;
74
+ readonly name = "codex-local";
75
+ readonly capabilities: SubagentCapabilities;
76
+ readonly inheritsParentContext = false;
77
+ /**
78
+ * @param live - the live driver, or a resolver returning the current
79
+ * generation's driver per member (the settings toggle swaps generations;
80
+ * a resolver may return undefined to steer one member's round to exec
81
+ * while a retiring generation still hosts it).
82
+ * @param model - resolver for the configured model, read PER ROUND so a
83
+ * settings-card write takes effect on the next delegation without a
84
+ * reload. Undefined (the resolver absent, or returning undefined) leaves
85
+ * the argv exactly as it was before the key existed — the scoped
86
+ * config.toml's own `model` then decides, as it always did.
87
+ * @param overrides - resolver for the member's session-level model override
88
+ * (the composer picker, owned by the model broker). It outranks BOTH the
89
+ * delegation's recorded model and the configured one: on the exec path it
90
+ * re-points the next round's `-m`, and on the live path it is the member's
91
+ * start model — a resident runtime bound to a different model is retired
92
+ * so the round respawns onto the override.
93
+ */
94
+ constructor(ctx: Context, sandbox?: CodexSandbox, live?: CodexLiveDriver | ((childSessionId: string) => CodexLiveDriver | undefined) | undefined, model?: (() => string | undefined) | undefined, overrides?: ((childSessionId: string) => string | undefined) | undefined);
95
+ /** Resolve the live driver for one round's member, if live is on for it. */
96
+ private liveDriver;
97
+ /** Per-round member-channel registration for the exec path (see {@link registerCodexMemberRun}). */
98
+ private memberRun;
99
+ start(request: ResolvedSubagentStartRequest): Promise<SubagentRun>;
100
+ /** Fresh round: record the child session, spawn `codex exec`, append after settle. */
101
+ private startCodexFresh;
102
+ /** Resume round: continue the recorded thread inside the existing child session. */
103
+ private startCodexResume;
104
+ }
105
+ /** Fully resolved inputs for one Codex CLI run. */
106
+ export interface CodexCliRunSpec {
107
+ /** Parent Session workspace; also the codex process cwd. */
108
+ readonly cwd: string;
109
+ /** Explicit environment layered after the shared credential scrub. */
110
+ readonly env: Readonly<NodeJS.ProcessEnv>;
111
+ /**
112
+ * Run the CLI inside this container instead of on the host: the argv below
113
+ * is wrapped in `docker exec` and {@link env} is forwarded through NAME-only
114
+ * `-e` flags. Absent means the host spawn, unchanged.
115
+ */
116
+ readonly exec?: DelegationExecTarget | undefined;
117
+ /** Resolved endpoint label for diagnostics; absent means the CLI default. */
118
+ readonly endpointLabel?: string | undefined;
119
+ /** Sandbox policy passed to `codex exec --sandbox`. */
120
+ readonly sandbox: CodexSandbox;
121
+ /**
122
+ * Model this round runs with, passed as `codex exec -m <model>`. Absent
123
+ * means no `-m` on the argv at all: the scoped `config.toml`'s own `model`
124
+ * (or, with none, codex's built-in default) decides, exactly as before the
125
+ * key existed.
126
+ */
127
+ readonly model?: string | undefined;
128
+ readonly effort?: string | undefined;
129
+ /** Subprocess termination grace passed to the shared process-tree owner. */
130
+ readonly disposeGraceMs: number;
131
+ /** Shared subprocess service spawn operation. */
132
+ readonly spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
133
+ /** Diagnostic sink for a post-publication error flattened into a result. */
134
+ readonly onError?: (error: Error, stopReason: SubagentStopReason) => void;
135
+ /**
136
+ * Called when the settled failure is auth-shaped (the endpoint rejected the
137
+ * credential — a 401 a presence probe cannot see). The provider wires this
138
+ * to the family registry's auth-failure mark.
139
+ */
140
+ readonly onAuthFailure?: ((detail: string) => void) | undefined;
141
+ /**
142
+ * Called when the round SETTLED COMPLETED — the CLI reached its endpoint and
143
+ * produced an answer, which is the only evidence a host has that the scoped
144
+ * credential is live. The provider wires this to the family registry's
145
+ * auth-success mark (the `verified` credential grade).
146
+ */
147
+ readonly onAuthSuccess?: (() => void) | undefined;
148
+ /**
149
+ * The codex build the executable reports, resolved lazily and cached by the
150
+ * family probe. Only consulted when the round's own rollout head named none.
151
+ */
152
+ readonly cliVersion?: (() => Promise<string | undefined>) | undefined;
153
+ /**
154
+ * Member channel: the bridge MCP declaration for this run as one `-c`
155
+ * inline-TOML config override (per-process; nothing lands in the scoped
156
+ * config.toml). Absent on a core that predates the member channel — the
157
+ * argv is then exactly the pre-channel shape.
158
+ */
159
+ readonly member?: {
160
+ readonly configOverride: string;
161
+ } | undefined;
162
+ /** dsh subagent session recording this delegation; its response is appended after settle. */
163
+ readonly childSession?: Session | undefined;
164
+ /** Host context carrying session persistence. */
165
+ readonly ctx?: Context | undefined;
166
+ /**
167
+ * Resume round: continue the thread named by `cliSessionId` with
168
+ * `codex exec --json resume <thread_id>` instead of a fresh exec, appending
169
+ * this round into the same child session under the given turn number.
170
+ */
171
+ readonly resume?: {
172
+ readonly cliSessionId: string;
173
+ readonly turn: number;
174
+ } | undefined;
175
+ /**
176
+ * Called after the run settles with the thread id parsed from the NDJSON
177
+ * stream (absent when none was reported). The fresh path uses it to record
178
+ * the delegation so a later resume round can continue the thread.
179
+ */
180
+ readonly onThreadId?: ((threadId: string | undefined) => void) | undefined;
181
+ /**
182
+ * Called once per settled round, after the output stream has been fully
183
+ * parsed and mirrored: the round's observed model identifier and token
184
+ * usage, each absent when the stream (and the rollout fallback) yielded
185
+ * none. The provider wires this to the registry's observation channel
186
+ * (`recordRoundSettled` — the delegation record's `observedModel` merge and
187
+ * the `settled` run-progress event). Fires for fresh and resume rounds
188
+ * alike, on every terminal state.
189
+ */
190
+ readonly onRoundSettled?: ((round: {
191
+ readonly observedModel?: string;
192
+ readonly usage?: TokenUsage;
193
+ readonly toolCalls?: LocalAgentToolCalls;
194
+ }) => void) | undefined;
195
+ }
196
+ /**
197
+ * Validate and join the one-shot task before crossing the process boundary.
198
+ * @param prompt - task content accepted from the shared subagent service.
199
+ * @returns the joined non-empty text.
200
+ */
201
+ export declare function textTask(prompt: readonly ContentBlock[]): string;
202
+ /** One ordered transcript line from a `codex exec --json` event stream. */
203
+ export type CodexTranscriptLine =
204
+ /**
205
+ * Reasoning or reply text. `itemId` (live app-server rounds only) pairs the
206
+ * line with its delta stream, so the completed item folds at the step its
207
+ * snapshots reserved.
208
+ */
209
+ {
210
+ kind: 'think';
211
+ text: string;
212
+ itemId?: string;
213
+ } | {
214
+ kind: 'text';
215
+ text: string;
216
+ itemId?: string;
217
+ }
218
+ /**
219
+ * Tool activity: one call with its (possibly absent) result. `id` is the
220
+ * stream item's id when present, else a synthesized position-based id —
221
+ * stable within a run either way, so the child session's
222
+ * `tool/call`/`tool/result` events pair by it.
223
+ */
224
+ | {
225
+ kind: 'tool';
226
+ id: string;
227
+ name: string;
228
+ args?: string;
229
+ result?: string;
230
+ };
231
+ /** Mutable fold state shared by the batch parse and the incremental parser. */
232
+ interface CodexStreamFoldState {
233
+ readonly lines: CodexTranscriptLine[];
234
+ text: string | undefined;
235
+ usage: TokenUsage | undefined;
236
+ threadId: string | undefined;
237
+ /**
238
+ * The model identifier the stream itself named (a `model` field on
239
+ * `thread.started` or `turn.completed`), when the CLI emits one. codex
240
+ * 0.144.0's exec stream carries none — the model then comes from the
241
+ * rollout's turn_context at settle — but the fold accepts it so a CLI that
242
+ * starts naming the model in the stream needs no parser change.
243
+ */
244
+ model: string | undefined;
245
+ /** Whether the stream's terminal `turn.completed` event was folded. */
246
+ completed: boolean;
247
+ /**
248
+ * Tool calls this stream made, keyed by codex's OWN item type
249
+ * (`command_execution`, `web_search_call`) — not the display names the
250
+ * transcript lines carry (`Bash`, `WebSearch`), because the accounting
251
+ * reports what the CLI called the thing. Counted in the same branches that
252
+ * already fold the item, so no event is parsed twice and none is parsed
253
+ * that was not parsed before. `function_call_output` is a RESULT and never
254
+ * counts: it merges into the call that preceded it.
255
+ */
256
+ readonly toolCalls: Map<string, number>;
257
+ }
258
+ /**
259
+ * Fold a tool-call counter into the settled shape: `{count, byName}` with the
260
+ * per-name values summing to `count`. An empty map yields undefined — a
261
+ * stream that named no tool call reports nothing rather than a zero it did
262
+ * not observe.
263
+ * @param counts - the fold's per-name tally.
264
+ * @returns the settled accounting, or undefined when nothing was counted.
265
+ */
266
+ export declare function toolCallsOf(counts: ReadonlyMap<string, number>): LocalAgentToolCalls | undefined;
267
+ /**
268
+ * Incremental `codex exec --json` parser: feed stdout chunks as they arrive
269
+ * and the folded transcript accumulates line by line. The last line is
270
+ * volatile until the terminal `turn.completed` — a `function_call_output`
271
+ * merges into a pending tool line — so live consumers hold it back (see the
272
+ * run's live mirror).
273
+ */
274
+ export declare class CodexStreamParser implements CodexStreamFoldState {
275
+ private buffer;
276
+ readonly lines: CodexTranscriptLine[];
277
+ text: string | undefined;
278
+ usage: TokenUsage | undefined;
279
+ threadId: string | undefined;
280
+ model: string | undefined;
281
+ completed: boolean;
282
+ readonly toolCalls: Map<string, number>;
283
+ /** Fold every complete NDJSON line in the chunk; the tail stays buffered. */
284
+ push(chunk: string): void;
285
+ }
286
+ /**
287
+ * Parse a `codex exec --json` NDJSON event stream into an ordered transcript
288
+ * (thinking, agent text, tool/command activity in event order), plus the final
289
+ * answer text, token usage, the thread id a later resume round continues, and
290
+ * the model identifier the stream named (usually absent — codex 0.144.0 does
291
+ * not put one on the wire; the settle path then reads the rollout's
292
+ * turn_context). Item types covered: `reasoning` (thinking), `agent_message`
293
+ * (reply text), `command_execution` (shell command with aggregated output),
294
+ * `web_search_call`, and `function_call_output`. The last `agent_message`
295
+ * wins as the run output (the final answer), and `turn.completed` carries the
296
+ * turn's usage. Malformed lines are skipped.
297
+ * @param stream - the collected stdout NDJSON text.
298
+ * @returns the ordered transcript, final answer text, usage, thread id, and
299
+ * any stream-named model.
300
+ */
301
+ export declare function parseCodexJsonStream(stream: string): {
302
+ lines: readonly CodexTranscriptLine[];
303
+ text?: string;
304
+ usage?: TokenUsage;
305
+ threadId?: string;
306
+ model?: string;
307
+ toolCalls?: LocalAgentToolCalls;
308
+ };
309
+ /**
310
+ * Start the real `codex exec` child and publish its run. The codex
311
+ * reply arrives as an NDJSON event stream on stdout (`--json`): the final
312
+ * `agent_message` item is the run output and the `turn.completed` usage is
313
+ * the token accounting. stderr is piped for diagnostics and never folded
314
+ * into the run output. A resume round spawns `codex exec --json resume
315
+ * <thread_id>` instead, continuing the same thread. The run id is the child
316
+ * session id for a session-backed run.
317
+ * @param request - resolved shared subagent request.
318
+ * @param spec - workspace, environment, process service, and diagnostic policy.
319
+ * @returns the published run after the child starts.
320
+ */
321
+ export declare function startCodexCliRun(request: SubagentStartRequest, spec: CodexCliRunSpec): Promise<SubagentRun>;
322
+ /** One assistant-role message event, attributed to the codex route. */
323
+ export declare function codexAssistantEvent(blocks: readonly ContentBlock[]): import("@deepseek-ai/dsh-llm").AssistantMessage;
324
+ /**
325
+ * Fold one transcript line into the child session as one assistant step,
326
+ * wrapped in the step/start–step/end boundary pair the live conversation
327
+ * assembler requires: without a boundary the step never enters the location
328
+ * index, the line's assistant message resolves to a turn-level location, and
329
+ * the real-time view renders nothing (only a full rebuild, which mints step
330
+ * drafts from the explicit coordinates, recovers it). Tool cards escape that
331
+ * fate only because their definition does not consult the step location.
332
+ * @returns whether the line folded (false when the options skipped it).
333
+ */
334
+ export declare function appendCodexTranscriptLine(childSession: Session, turn: number, step: number, line: CodexTranscriptLine, usage: TokenUsage | undefined): boolean;
335
+ /** The delta-progress text for one transcript line. */
336
+ export declare function codexLineText(line: CodexTranscriptLine): string;
337
+ /**
338
+ * Persist the child session's mirrored events. Thin wrapper over the core's
339
+ * {@link persistChildSession}: a live session syncs through the core's cached
340
+ * per-child write handle (host 0.1.5's live write-behind never checkpoints a
341
+ * mirror session — it has no agent loop — so production child logs stayed
342
+ * header-only until the explicit sync), a standalone one through a one-shot
343
+ * handle flow. Kept under the historical name for the live driver.
344
+ */
345
+ export declare function persistIfStandalone(ctx: Context, childSession: Session): Promise<void>;
346
+ export {};
347
+ //# sourceMappingURL=codex-cli-provider.d.ts.map