@agent-compose/sdk 0.8.2 → 0.8.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.
- package/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
- package/dist/agent/agent-context.d.ts +1 -1
- package/dist/agent/agent-loop.d.ts +5 -1
- package/dist/agent/desktop-open.d.ts +184 -0
- package/dist/agent/perf-sampler.d.ts +99 -0
- package/dist/agent/services-manifest.d.ts +88 -0
- package/dist/agent/services-restore.d.ts +58 -0
- package/dist/client.d.ts +164 -8
- package/dist/display.d.ts +17 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +1374 -51
- package/dist/runtimes/_cli-agent.d.ts +347 -2
- package/dist/runtimes/claude-code.d.ts +12 -0
- package/dist/runtimes/codex.d.ts +8 -0
- package/dist/runtimes/openai-desktop.js +1312 -51
- package/dist/runtimes/session-env.test.d.ts +14 -0
- package/dist/types/api-conversations.d.ts +309 -1
- package/dist/types/api-factory.d.ts +115 -10
- package/dist/types/api-runs.d.ts +21 -0
- package/dist/types/protocol.d.ts +32 -1
- package/dist/types/runtime.d.ts +120 -0
- package/package.json +1 -1
- package/src/agent/agent-context.ts +100 -11
- package/src/agent/agent-loop.ts +10 -3
- package/src/agent/desktop-open.ts +418 -0
- package/src/agent/perf-sampler.ts +202 -0
- package/src/agent/services-manifest.ts +356 -0
- package/src/agent/services-restore.ts +195 -0
- package/src/client.ts +328 -12
- package/src/display.ts +44 -1
- package/src/index.ts +63 -1
- package/src/runtimes/_cli-agent.ts +891 -35
- package/src/runtimes/claude-code.ts +187 -12
- package/src/runtimes/codex.ts +58 -1
- package/src/sandbox/providers/local.ts +16 -4
- package/src/types/api-conversations.ts +307 -3
- package/src/types/api-factory.ts +118 -10
- package/src/types/api-runs.ts +23 -0
- package/src/types/protocol.ts +30 -1
- package/src/types/runtime.ts +122 -0
|
@@ -23,13 +23,231 @@
|
|
|
23
23
|
* (see each spec's `authEnv`); `commands.run` inherits the sandbox env, so
|
|
24
24
|
* values set via `agentc secrets set` are visible to the CLI.
|
|
25
25
|
*/
|
|
26
|
-
import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider, ToolCallGateResult } from "../index.js";
|
|
26
|
+
import type { AgentMessage, ModelExecutionContract, RunnerLivenessVerdict, RuntimeOptions, SandboxProvider, ToolCallGateResult, TurnExitNotify } from "../index.js";
|
|
27
|
+
import type { GuestPerfSample } from "../agent/perf-sampler.js";
|
|
27
28
|
import type { ProcessorContext, ToolCall } from "../processors/processor.js";
|
|
28
29
|
/** Backoff between tail re-attach attempts after a mid-turn stream fault
|
|
29
30
|
* (durable detached transport). Short: the runner is alive and producing,
|
|
30
31
|
* and each retry costs one exec; the executor's evidence machinery — not
|
|
31
32
|
* this loop — bounds a truly dead sandbox. Exported for tests. */
|
|
32
33
|
export declare const TAIL_REATTACH_DELAY_MS = 2000;
|
|
34
|
+
/** Cadence of the in-guest heartbeat writer (seconds — it is a shell loop). */
|
|
35
|
+
export declare const RUNNER_HEARTBEAT_INTERVAL_SECONDS = 10;
|
|
36
|
+
/** How recently a file under the CLI's own task state dir (~/.claude/tasks)
|
|
37
|
+
* must have been touched to count as a LIVE background task in the busy
|
|
38
|
+
* sentinel (minutes — it is `find -mmin`). A running task's state/output
|
|
39
|
+
* files are written continuously, so two minutes is generous; a finished
|
|
40
|
+
* task's files go quiet and age out on their own. */
|
|
41
|
+
export declare const RUNNER_BUSY_TASK_FRESH_MINUTES = 2;
|
|
42
|
+
/** Cap on the task-id list the busy sentinel publishes — the ids are for
|
|
43
|
+
* server-side log attribution, not an inventory. */
|
|
44
|
+
export declare const RUNNER_BUSY_TASK_ID_LIMIT = 16;
|
|
45
|
+
/** The sh fragment the heartbeat subshell runs each beat to maintain the
|
|
46
|
+
* busy sentinel — the guest half of the background-work marker contract:
|
|
47
|
+
* - `<promptPath>.busy` — the COUNT of recently-active files under the
|
|
48
|
+
* CLI's task state dir (`$ac_busy` stays set in-shell for the loop's
|
|
49
|
+
* own exit decision);
|
|
50
|
+
* - `<promptPath>.tasks` — the task IDS those files belong to (first
|
|
51
|
+
* path segment under tasks/, deduped, capped).
|
|
52
|
+
* The marker's freshness stamp is the files' own mtime (each beat rewrites
|
|
53
|
+
* them; the server probe gates on `-mmin -1`). Cheap (one bounded find
|
|
54
|
+
* over a small dir), and read by the server's park path so a VM hosting
|
|
55
|
+
* live background work is never frozen mid-flight (2026-08-15 forensics:
|
|
56
|
+
* a 3-min idle park suspended a VM with three research agents mid-flight).
|
|
57
|
+
* A missing tasks dir counts 0. Exported for tests. */
|
|
58
|
+
export declare function runnerBusySentinelFragment(busyPath: string, tasksPath: string): string;
|
|
59
|
+
/** A heartbeat older than this (by the GUEST's own clock — the probe reads
|
|
60
|
+
* `date +%s%3N` in the same exec, so host clock skew is irrelevant) is
|
|
61
|
+
* stale: three missed writes. */
|
|
62
|
+
export declare const RUNNER_HEARTBEAT_STALE_MS: number;
|
|
63
|
+
/** Watchdog cadence while a tail stream is attached. Each tick is one fresh
|
|
64
|
+
* short exec; a healthy stream makes every tick a no-op. */
|
|
65
|
+
export declare const TAIL_WATCHDOG_INTERVAL_MS = 15000;
|
|
66
|
+
/** Explicit ceiling on one durable-trio probe exec. A probe that can't
|
|
67
|
+
* answer is "probe-failed" — never a verdict. */
|
|
68
|
+
export declare const TURN_PROBE_TIMEOUT_MS = 10000;
|
|
69
|
+
/** After this many stream deaths in ONE turn, stop re-attaching tails and
|
|
70
|
+
* finish the turn on durable-file polling alone (the GHA permanent-
|
|
71
|
+
* fallback pattern): a wire that killed three streams will kill the
|
|
72
|
+
* fourth, and the durable plane already delivers everything. */
|
|
73
|
+
export declare const TAIL_ABANDON_AFTER_STREAM_DEATHS = 3;
|
|
74
|
+
/** Poll cadence once streaming is abandoned — a full durable read per poll
|
|
75
|
+
* (envd HTTP, ~65ms RTT), consumed from the byte offset. Latency-tuned:
|
|
76
|
+
* this is the degraded path, correctness never depends on it being fast. */
|
|
77
|
+
export declare const DURABLE_POLL_INTERVAL_MS = 1000;
|
|
78
|
+
/** The claude CLI's stderr complaint when `--resume <id>` names a thread
|
|
79
|
+
* that does not exist on this machine (the 2026-08-18 split-brain
|
|
80
|
+
* forensic). When a nonzero exit's stderr carries it, the transport
|
|
81
|
+
* surfaces the stderr as the turn's LAST error event even though a bare
|
|
82
|
+
* result error already streamed — the consumer's classifier needs the
|
|
83
|
+
* specific complaint, not the generic `error_during_execution` token. */
|
|
84
|
+
export declare const RESUME_TARGET_MISSING_STDERR: RegExp;
|
|
85
|
+
/** One durable-trio reading, parsed from the probe exec's single line. */
|
|
86
|
+
export interface TurnProbeReading {
|
|
87
|
+
/** Bytes currently in the durable stdout file (-1: unreadable/absent). */
|
|
88
|
+
size: number;
|
|
89
|
+
/** Is the detached runner's process group leader alive (kill -0)? */
|
|
90
|
+
alive: boolean;
|
|
91
|
+
/** Last heartbeat value (epoch ms, guest clock; 0 = no heartbeat yet). */
|
|
92
|
+
hbMs: number;
|
|
93
|
+
/** The guest's own clock at probe time (epoch ms). */
|
|
94
|
+
nowMs: number;
|
|
95
|
+
/** Is the exit sentinel already in the durable file (turn FINISHED)? */
|
|
96
|
+
sentinelSeen: boolean;
|
|
97
|
+
/** Latest guest perf sample (the trailing `.perf` token field), when the
|
|
98
|
+
* guest wrote one AND it parsed clean. Absent = older guest image, no
|
|
99
|
+
* sample yet, or a garbled token — evidence-absent, never an error.
|
|
100
|
+
* Telemetry-only: no liveness verdict may ever read it. */
|
|
101
|
+
perf?: GuestPerfSample;
|
|
102
|
+
}
|
|
103
|
+
/** The probe exec: one line, six fields, always exit 0 — faults surface as
|
|
104
|
+
* parse failures, not exec failures. Reads the guest's own clock in the
|
|
105
|
+
* same exec so staleness math never involves the host clock. The sixth
|
|
106
|
+
* field is the perf token (perf-sampler.ts), `-` when absent; the char
|
|
107
|
+
* whitelist (`tr -cd`) collapses any garbled write to at most one token —
|
|
108
|
+
* it can never add whitespace and desync the field positions. */
|
|
109
|
+
export declare function turnLivenessProbeCommand(paths: {
|
|
110
|
+
outPath: string;
|
|
111
|
+
hbPath: string;
|
|
112
|
+
perfPath: string;
|
|
113
|
+
pid: number;
|
|
114
|
+
sentinel: string;
|
|
115
|
+
}): string;
|
|
116
|
+
/** Parse the probe's line. Null = unusable output (a wedged or faulted exec)
|
|
117
|
+
* — the caller treats that as "probe-failed", never as a verdict. The
|
|
118
|
+
* sixth field (perf token) is OPTIONAL both ways: a five-field line from
|
|
119
|
+
* an old guest parses fine, and a token that fails its own clamp simply
|
|
120
|
+
* leaves `perf` absent — perf can never fail a liveness reading. */
|
|
121
|
+
export declare function parseTurnProbeOutput(raw: string): TurnProbeReading | null;
|
|
122
|
+
/** The three-valued verdict from one durable reading. A FINISHED turn
|
|
123
|
+
* (sentinel durable in the file) is ALIVE — the transport's watchdog
|
|
124
|
+
* harvests it within one interval; reporting it dead is exactly the
|
|
125
|
+
* incident's ten-minute blindness. */
|
|
126
|
+
export declare function livenessVerdictFromReading(reading: TurnProbeReading | null): RunnerLivenessVerdict;
|
|
127
|
+
/** Feeder poll cadence (seconds — it is a shell `sleep`; GNU sleep accepts
|
|
128
|
+
* fractions and the durable transport only runs on the GNU guest image). */
|
|
129
|
+
export declare const INJECT_FEEDER_POLL_SECONDS = "0.3";
|
|
130
|
+
/** How much of the durable stdout tail the feeder greps for the terminal
|
|
131
|
+
* result line each poll — bounded so a long transcript never turns the
|
|
132
|
+
* poll into a full-file scan. Comfortably above the JSONL guard's line cap,
|
|
133
|
+
* so a capped result line still fits whole. */
|
|
134
|
+
export declare const INJECT_FEEDER_RESULT_TAIL_BYTES = 262144;
|
|
135
|
+
/** One in-guest delivery-ack exec: append the line, then poll the delivered
|
|
136
|
+
* counter until it covers this append (delivered), the runner dies, or the
|
|
137
|
+
* attempts run out (both: not delivered — the message stays owed). */
|
|
138
|
+
export declare const INJECT_ACK_POLL_ATTEMPTS = 30;
|
|
139
|
+
export declare const INJECT_ACK_POLL_SECONDS = "0.5";
|
|
140
|
+
export declare const INJECT_ACK_EXEC_TIMEOUT_MS = 25000;
|
|
141
|
+
/** The in-guest feeder fragment, prepended to the detached wrapper script.
|
|
142
|
+
* Runs inside the setsid session (group-kill reaps it) and keys its loop to
|
|
143
|
+
* the wrapper's own pid (`$$`), like the heartbeat subshell. Exported for
|
|
144
|
+
* tests. */
|
|
145
|
+
export declare function streamInputFeederFragment(paths: {
|
|
146
|
+
promptPath: string;
|
|
147
|
+
fifoPath: string;
|
|
148
|
+
inboxPath: string;
|
|
149
|
+
deliveredPath: string;
|
|
150
|
+
outPath: string;
|
|
151
|
+
}): string;
|
|
152
|
+
/** The one delivery-ack exec `injectUserMessage` runs: append the message
|
|
153
|
+
* line to the durable inbox, then wait for the feeder's delivered counter
|
|
154
|
+
* to cover it. Exit 0 = delivered into the CLI's stdin pre-result; 4 = the
|
|
155
|
+
* runner died first; 5 = not delivered within the ack window (feeder
|
|
156
|
+
* stopped on the result, or the guest is crawling) — both non-zero exits
|
|
157
|
+
* mean "leave the message owed". Exported for tests. */
|
|
158
|
+
export declare function injectAppendAndAckCommand(args: {
|
|
159
|
+
line: string;
|
|
160
|
+
target: number;
|
|
161
|
+
pid: number;
|
|
162
|
+
inboxPath: string;
|
|
163
|
+
deliveredPath: string;
|
|
164
|
+
}): string;
|
|
165
|
+
/** SIGTERM grace before escalation: attempts × sleep = 5s. */
|
|
166
|
+
export declare const REAP_TERM_WAIT_ATTEMPTS = 20;
|
|
167
|
+
/** Post-SIGKILL confirm window: attempts × sleep = 2s (a KILLed process only
|
|
168
|
+
* lingers as an unreaped zombie, which `kill -0` still sees — brief). */
|
|
169
|
+
export declare const REAP_KILL_WAIT_ATTEMPTS = 8;
|
|
170
|
+
export declare const REAP_POLL_SECONDS = "0.25";
|
|
171
|
+
/** The confirm exec's "still alive after SIGKILL" exit code (surfaced as a
|
|
172
|
+
* warn — the launch preflight is the backstop for whatever survived). */
|
|
173
|
+
export declare const REAP_STILL_ALIVE_EXIT = 7;
|
|
174
|
+
/** Ceiling on the whole kill-and-confirm exec (TERM 5s + KILL 2s + slack). */
|
|
175
|
+
export declare const REAP_EXEC_TIMEOUT_MS = 15000;
|
|
176
|
+
/** The one kill-and-confirm exec: TERM the group (setsid made `pid` the
|
|
177
|
+
* leader) and the leader itself, poll for exit, escalate to KILL, poll
|
|
178
|
+
* again. Exit 0 = confirmed dead; REAP_STILL_ALIVE_EXIT = it survived
|
|
179
|
+
* SIGKILL's confirm window. Exported for tests. */
|
|
180
|
+
export declare function reapKillAndConfirmCommand(pid: number): string;
|
|
181
|
+
/** Guest idle TTL for a resident with no new inbox line: hot window (180s)
|
|
182
|
+
* + margin, so the park gate's graceful `.end` normally wins and the
|
|
183
|
+
* watchdog only answers a dead server / lost record. */
|
|
184
|
+
export declare const RESIDENT_IDLE_TTL_S = 240;
|
|
185
|
+
/** Watchdog poll cadence (seconds). */
|
|
186
|
+
export declare const RESIDENT_WATCHDOG_POLL_S = 15;
|
|
187
|
+
/** Anchored prefix of the CLI's terminal result event line. Anchoring keeps
|
|
188
|
+
* the guest-side counts honest against tool output that merely CONTAINS
|
|
189
|
+
* the substring; a serialization-order change fails SAFE (parity check
|
|
190
|
+
* refuses adoption → cold launch — a latency cost, never correctness). */
|
|
191
|
+
export declare const RESIDENT_RESULT_LINE_PREFIX = "{\"type\":\"result\"";
|
|
192
|
+
/** One bounded exec printing the count of result lines at or past
|
|
193
|
+
* `fromByte` (0-based) in the durable .out. Always exits 0; stdout is the
|
|
194
|
+
* count (a missing file reads as 0). Hint-grade by design. */
|
|
195
|
+
export declare function residentResultCountCommand(outPath: string, fromByte?: number): string;
|
|
196
|
+
/** The resident feeder: `streamInputFeederFragment` with the result-break
|
|
197
|
+
* REPLACED by the end-file break — the ONLY guest-contract change the
|
|
198
|
+
* resident shape needs (verified live by the spec's probe: two messages,
|
|
199
|
+
* one process, one session file, graceful exit on `.end`). */
|
|
200
|
+
export declare function residentFeederFragment(paths: {
|
|
201
|
+
promptPath: string;
|
|
202
|
+
fifoPath: string;
|
|
203
|
+
inboxPath: string;
|
|
204
|
+
deliveredPath: string;
|
|
205
|
+
endPath: string;
|
|
206
|
+
}): string;
|
|
207
|
+
/** Sets `ac_idle` (1 = between turns: every delivered turn has its result
|
|
208
|
+
* and the inbox is fully fed). Embedded by the watchdog and the resident
|
|
209
|
+
* heartbeat gate — one idle predicate, stated once. */
|
|
210
|
+
export declare function residentIdleCheckFragment(paths: {
|
|
211
|
+
outPath: string;
|
|
212
|
+
inboxPath: string;
|
|
213
|
+
deliveredPath: string;
|
|
214
|
+
servedPath: string;
|
|
215
|
+
}): string;
|
|
216
|
+
/** Guest idle watchdog: reap the whole process group after
|
|
217
|
+
* RESIDENT_IDLE_TTL_S of CONTINUOUS idleness (any busy reading resets the
|
|
218
|
+
* clock — a long-running TURN is never reaped; the parity check reads
|
|
219
|
+
* busy mid-turn). The park gate's graceful `.end` normally wins; this is
|
|
220
|
+
* the orphan backstop for a dead server or a lost durable record.
|
|
221
|
+
*
|
|
222
|
+
* BACKGROUND TASKS ARE NOT IDLE (2026-08-19 forensics: a user's
|
|
223
|
+
* between-turns background search died to a harness teardown while its
|
|
224
|
+
* journal was still advancing). The turn-parity predicate alone reads a
|
|
225
|
+
* resident BETWEEN turns as idle even while harness background tasks it
|
|
226
|
+
* tracks are hard at work — and this watchdog's group-kill would take
|
|
227
|
+
* those children with the tree. So idle additionally requires the CLI's
|
|
228
|
+
* own task journal (`$HOME/.claude/tasks`) to be QUIET: any file touched
|
|
229
|
+
* within RUNNER_BUSY_TASK_FRESH_MINUTES resets the clock — the exact
|
|
230
|
+
* journal-freshness license the server's park gate honors
|
|
231
|
+
* (guestBusyProbeCommand). A stalled task goes stale within the window
|
|
232
|
+
* and the TTL resumes on its own; liveness alone never counts (the
|
|
233
|
+
* 2026-08-17 wake-loop damper is a SERVER rule about waking — this is a
|
|
234
|
+
* guest rule about not killing, and journal advance is required, not mere
|
|
235
|
+
* process existence). */
|
|
236
|
+
export declare function residentIdleWatchdogFragment(paths: {
|
|
237
|
+
outPath: string;
|
|
238
|
+
inboxPath: string;
|
|
239
|
+
deliveredPath: string;
|
|
240
|
+
servedPath: string;
|
|
241
|
+
}): string;
|
|
242
|
+
/** Exit-event doorbell with the turnId read from a guest FILE at push time
|
|
243
|
+
* (a resident serves many turns; each adoption rewrites `<prompt>.turnid`
|
|
244
|
+
* so a mid-turn death rings the turn actually being served). Same
|
|
245
|
+
* validation posture as `exitEventPushCommand`; an empty turnid file
|
|
246
|
+
* pushes nothing. */
|
|
247
|
+
export declare function deferredExitEventPushCommand(notify: {
|
|
248
|
+
url: string;
|
|
249
|
+
tokenEnv: string;
|
|
250
|
+
}, turnIdFile: string): string;
|
|
33
251
|
/** Provider deadline on the detached LAUNCH exec. The launch shell only
|
|
34
252
|
* truncates the durable files, forks the detached runner (stdio fully
|
|
35
253
|
* redirected — see the launch command), writes the pidfile, and echoes the
|
|
@@ -50,8 +268,50 @@ export declare const LAUNCH_PID_RECOVERY_DELAY_MS = 1000;
|
|
|
50
268
|
* call in the turn path carries an explicit timeout so no SDK default can
|
|
51
269
|
* decide a turn's fate. Exported for tests. */
|
|
52
270
|
export declare const INSTALL_PROBE_TIMEOUT_MS = 30000;
|
|
271
|
+
/** Deadline on the on-demand CLI install itself (`spec.install`). Shared by
|
|
272
|
+
* this transport's ensureInstalled and the server's CloudTurnWorkflow launch
|
|
273
|
+
* path (turn-workflow/tailer.ts), which provisions the same way before a
|
|
274
|
+
* detached launch — the session images bake only `claude`, so codex/
|
|
275
|
+
* opencode/cursor/droid MUST be installable at launch on a fresh machine. */
|
|
276
|
+
export declare const CLI_INSTALL_TIMEOUT_MS = 300000;
|
|
53
277
|
/** Single-quote a value for safe interpolation into a `sh -c` command line. */
|
|
54
278
|
export declare function shellQuote(value: string): string;
|
|
279
|
+
/** $HOME-relative path of the platform-managed session env file. The server
|
|
280
|
+
* writes it at sandbox acquire (session secrets → `export KEY='…'` lines);
|
|
281
|
+
* the detached turn launch sources it fresh EVERY turn, so a secret set or
|
|
282
|
+
* rotated between turns lands on the next turn with no VM recycle. Shared
|
|
283
|
+
* constant so the writer (server) and the reader (this runtime) can never
|
|
284
|
+
* drift. */
|
|
285
|
+
export declare const SESSION_ENV_FILE_RELPATH = ".agent-compose/session-env.sh";
|
|
286
|
+
/** The `. $HOME/<file>;` fragment sourced ahead of the CLI command, or ""
|
|
287
|
+
* when no env file is configured. Errors are swallowed — a missing or
|
|
288
|
+
* unreadable file must never fail a turn. */
|
|
289
|
+
export declare function sessionEnvSourceFragment(relPath: string | undefined): string;
|
|
290
|
+
/** The per-turn model-credential source fragment (`RuntimeOptions.
|
|
291
|
+
* credEnvFile` — an ABSOLUTE guest path), or "" when none is configured.
|
|
292
|
+
* Sourced AFTER the session env file so the turn actor's credential wins.
|
|
293
|
+
* Same swallow-errors posture: a missing file must never fail a turn. */
|
|
294
|
+
export declare function credEnvSourceFragment(absPath: string | undefined): string;
|
|
295
|
+
/** Ceiling on ONE curl attempt (seconds — it is `curl -m`). Short: the file
|
|
296
|
+
* is the truth and a slow push must not keep the wrapper alive for long. */
|
|
297
|
+
export declare const EXIT_PUSH_ATTEMPT_TIMEOUT_SECONDS = 5;
|
|
298
|
+
/** Pause before the single retry. */
|
|
299
|
+
export declare const EXIT_PUSH_RETRY_DELAY_SECONDS = 1;
|
|
300
|
+
/**
|
|
301
|
+
* The sh fragment appended to the detached wrapper AFTER the sentinel
|
|
302
|
+
* `printf` — never before it, and never in a way that can fail the wrapper:
|
|
303
|
+
* - reads the credential from the guest env at push time (`$<tokenEnv>`;
|
|
304
|
+
* the token never appears in the script text, argv-visible command
|
|
305
|
+
* lines the server logs, or any log line);
|
|
306
|
+
* - silently skips when the credential or curl is absent;
|
|
307
|
+
* - two attempts max (`-m 5`, one retry after 1s), then gives up silently
|
|
308
|
+
* — all output discarded, exit status swallowed (`|| true`).
|
|
309
|
+
* `$ac_rc` is the wrapper-local capture of the guarded pipeline's exit code
|
|
310
|
+
* (the same value the sentinel line carries). Returns "" — push disabled —
|
|
311
|
+
* for any `tokenEnv`/`turnId` that is not a plain identifier/uuid shape, so
|
|
312
|
+
* hostile config can never become shell injection.
|
|
313
|
+
*/
|
|
314
|
+
export declare function exitEventPushCommand(notify: TurnExitNotify): string;
|
|
55
315
|
/**
|
|
56
316
|
* A UNIQUE per-turn prompt path. Uniqueness is load-bearing, not cosmetic:
|
|
57
317
|
* E2B's envd cannot overwrite an existing non-root file in /tmp — it opens
|
|
@@ -129,13 +389,18 @@ export interface CliAgentSpec {
|
|
|
129
389
|
/** Build the one-shot shell command for a turn. `promptPath` is a file in the
|
|
130
390
|
* sandbox holding `promptPayload(prompt)`; `sessionId` continues a thread.
|
|
131
391
|
* `effort` is present only when the caller configured a reasoning effort
|
|
132
|
-
* AND the spec has a real knob for it (see `CliReasoningEffort`).
|
|
392
|
+
* AND the spec has a real knob for it (see `CliReasoningEffort`).
|
|
393
|
+
* `streamInput` is true when the durable transport is feeding stdin as a
|
|
394
|
+
* live message stream (see `CliAgentSpec.streamInput`): `promptPath` is
|
|
395
|
+
* then the FIFO the feeder writes, and the CLI must be invoked in its
|
|
396
|
+
* stream-input mode (claude: `--input-format stream-json`). */
|
|
133
397
|
buildCommand(args: {
|
|
134
398
|
promptPath: string;
|
|
135
399
|
sessionId?: string;
|
|
136
400
|
model?: string;
|
|
137
401
|
cwd?: string;
|
|
138
402
|
effort?: CliReasoningEffort;
|
|
403
|
+
streamInput?: boolean;
|
|
139
404
|
}): string;
|
|
140
405
|
/** Map one parsed JSONL stdout event to `AgentMessage`s. The base emits
|
|
141
406
|
* `init`/`done`/`error` lifecycle itself, so a spec maps only content +
|
|
@@ -150,6 +415,16 @@ export interface CliAgentSpec {
|
|
|
150
415
|
* resume it (codex `thread.started.thread_id`, amp `session_id`).
|
|
151
416
|
* LEGACY FALLBACK PATH (see `mapEvent`). */
|
|
152
417
|
extractSessionId(parsed: Record<string, unknown>): string | undefined;
|
|
418
|
+
/** The runtime's thread persistence admits only ONE live writer per thread
|
|
419
|
+
* (codex: an OS advisory flock per thread id, held for the holding
|
|
420
|
+
* process's whole lifetime — codex-rs thread-store writer_lock.rs). A
|
|
421
|
+
* platform that wants a SUCCESSOR turn to resume the same thread must
|
|
422
|
+
* therefore KILL the predecessor's process tree first — a merely-detached
|
|
423
|
+
* live predecessor blocks every `thread/resume` with "already has an
|
|
424
|
+
* active writer" (the 2026-08-22 superseded-codex-turn incident). Absent
|
|
425
|
+
* ⇒ concurrent resume is safe (claude-code: append-only session JSONL)
|
|
426
|
+
* and a superseded runner may be detached alive as an asset. */
|
|
427
|
+
exclusiveSessionWriter?: boolean;
|
|
153
428
|
/** ACP-mode invocation (the Zed `agent_servers` shape). When present, the
|
|
154
429
|
* runner spawns the CLI in ACP agent mode and delegates the whole wire
|
|
155
430
|
* protocol to `AcpClientPeer`; the legacy `promptPayload`/`buildCommand`/
|
|
@@ -160,6 +435,27 @@ export interface CliAgentSpec {
|
|
|
160
435
|
args: string[];
|
|
161
436
|
env?: Record<string, string>;
|
|
162
437
|
};
|
|
438
|
+
/** Mid-turn STREAM-INPUT support (the delivery half of
|
|
439
|
+
* `injectUserMessage`). When present AND the durable detached transport
|
|
440
|
+
* is in play, the CLI's stdin is fed from a FIFO by an in-guest feeder:
|
|
441
|
+
* the prompt line first, then any lines appended to the turn's durable
|
|
442
|
+
* inbox file — so a steering-capable CLI (claude: queued user input is
|
|
443
|
+
* folded into the RUNNING turn at the next tool boundary; verified live
|
|
444
|
+
* against claude 2.1.233) receives user messages while the turn runs.
|
|
445
|
+
* The feeder stops at the CLI's terminal result line (a message that
|
|
446
|
+
* races the result is NOT forwarded — it stays owed) and closes the
|
|
447
|
+
* FIFO, which is what ends the CLI process (stream-input CLIs exit on
|
|
448
|
+
* stdin EOF, not after a result). Absent → the prompt file is the whole
|
|
449
|
+
* stdin, exactly as before. */
|
|
450
|
+
streamInput?: {
|
|
451
|
+
/** Serialise the opening prompt into ONE stream-input stdin line
|
|
452
|
+
* (claude: a stream-json user message). No trailing newline — the
|
|
453
|
+
* transport owns line framing. */
|
|
454
|
+
promptLine(prompt: string): string;
|
|
455
|
+
/** Serialise one mid-turn user message into ONE stdin line. No
|
|
456
|
+
* trailing newline. */
|
|
457
|
+
messageLine(text: string): string;
|
|
458
|
+
};
|
|
163
459
|
}
|
|
164
460
|
export declare class CliAgentRunner implements ModelExecutionContract {
|
|
165
461
|
private readonly sandbox;
|
|
@@ -208,6 +504,55 @@ export declare class CliAgentRunner implements ModelExecutionContract {
|
|
|
208
504
|
* exercising the real `sendMessageAcp` even while the production default is
|
|
209
505
|
* dormant. Not a public API. */
|
|
210
506
|
private acpTransportReady;
|
|
507
|
+
/** The CURRENT turn's durable-trio reader (set once the detached runner is
|
|
508
|
+
* launched, replaced by the next launch). Null = no durable transport is
|
|
509
|
+
* active for this runner (boot phase, ACP path, single-exec providers) —
|
|
510
|
+
* `probeTurnLiveness` then answers null and the executor keeps its own
|
|
511
|
+
* fallback probe. */
|
|
512
|
+
private turnProbe;
|
|
513
|
+
/** The CURRENT turn's mid-turn injection state (durable stream-input
|
|
514
|
+
* transport only). Null = no live stream-input turn — `injectUserMessage`
|
|
515
|
+
* answers "unsupported" and the caller leaves the message owed. `chain`
|
|
516
|
+
* serializes appends so each delivery targets a deterministic line count. */
|
|
517
|
+
private turnInject;
|
|
518
|
+
/** Deliver one user message INTO the live turn — see the contract doc
|
|
519
|
+
* (types/runtime.ts). Appends a stream-input line to the turn's durable
|
|
520
|
+
* inbox and waits for the in-guest feeder's delivered-counter ack; only
|
|
521
|
+
* an acked forward (pre-result, into the CLI's stdin) reports
|
|
522
|
+
* "delivered". Guest exit 5 (ack window exhausted with the runner still
|
|
523
|
+
* alive — a CLI that is not draining stdin mid-step) reports "pending":
|
|
524
|
+
* the appended line may still be read when the current step finishes,
|
|
525
|
+
* but it was NOT seen yet. Never throws. */
|
|
526
|
+
injectUserMessage(text: string): Promise<"delivered" | "pending" | "closed" | "unsupported">;
|
|
527
|
+
/** Durable three-valued liveness for the current turn — see the contract
|
|
528
|
+
* doc (types/runtime.ts). Never throws; never blocks past the probe's
|
|
529
|
+
* explicit exec timeout. */
|
|
530
|
+
probeTurnLiveness(): Promise<RunnerLivenessVerdict | null>;
|
|
531
|
+
/** Wakes the CURRENT tail-watchdog interval early. Armed only while the
|
|
532
|
+
* watchdog's wake timer is pending; null between arms / between turns. */
|
|
533
|
+
private wakeTailWatchdog;
|
|
534
|
+
/** A doorbell that rang while no watchdog wake was armed — consumed at the
|
|
535
|
+
* next arm (immediate wake), dropped at the start of a new turn. */
|
|
536
|
+
private nudgePending;
|
|
537
|
+
/** DOORBELL, NEVER A VERDICT (contract doc: types/runtime.ts). Wake the
|
|
538
|
+
* tail watchdog NOW so its normal verification pass — durable probe, then
|
|
539
|
+
* harvest-on-sentinel / honest no-sentinel death — runs immediately
|
|
540
|
+
* instead of at the next TAIL_WATCHDOG_INTERVAL_MS tick. Carries no
|
|
541
|
+
* information: a nudge for a live mid-turn runner probes alive and is a
|
|
542
|
+
* no-op; a spurious/duplicate nudge costs at most one probe exec. No-op
|
|
543
|
+
* when no durable watchdog is armed (boot phase, ACP path, single-exec
|
|
544
|
+
* transports, abandoned-streaming polling) — those phases already carry
|
|
545
|
+
* their own bounds. */
|
|
546
|
+
nudgeTurnProbe(): void;
|
|
547
|
+
/** RESUME HANDOFF, NEVER A KILL (contract doc: types/runtime.ts). Once
|
|
548
|
+
* set, abort/early-exit unwinds the transport WITHOUT reaping the
|
|
549
|
+
* detached guest tree and WITHOUT the opportunistic durable-file cleanup
|
|
550
|
+
* — the successor turn resumes the same guest session, and the park
|
|
551
|
+
* path's busy signal keeps reading the surviving heartbeat/busy files.
|
|
552
|
+
* One-way for this runner instance (a runner is per-turn on the cloud
|
|
553
|
+
* path). */
|
|
554
|
+
private guestDetached;
|
|
555
|
+
detachGuest(): void;
|
|
211
556
|
constructor(sandbox: SandboxProvider, options: RuntimeOptions, spec: CliAgentSpec, configModel?: string | undefined, configEffort?: CliReasoningEffort | undefined);
|
|
212
557
|
/** TEST-ONLY: flip the live-ACP readiness gate on for this runner instance so
|
|
213
558
|
* the ACP lifecycle / fallback tests can exercise `sendMessageAcp` while the
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* sessions this is the gateway-minted session virtual key pointed at the
|
|
34
34
|
* token-metering gateway's Anthropic passthrough (ADR-0039).
|
|
35
35
|
*/
|
|
36
|
+
import type { AgentMessageTaskNotification } from "../index.js";
|
|
36
37
|
import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
|
|
37
38
|
/** Pinned Claude Code ACP adapter (Zed's npm shim — `claude` has no native
|
|
38
39
|
* ACP mode). Pinned EXACT, not a range: the adapter's README warns of
|
|
@@ -40,6 +41,17 @@ import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
|
|
|
40
41
|
* bridge daemon (cli/src/bridge/acp.ts) imports this same pin so both
|
|
41
42
|
* executors launch the identical adapter. Verified against 0.16.2. */
|
|
42
43
|
export declare const CLAUDE_CODE_ACP_ADAPTER = "@zed-industries/claude-code-acp@0.16.2";
|
|
44
|
+
/** Parse every `<task-notification>` block out of one user-role text blob.
|
|
45
|
+
* Pure and tolerant over untrusted harness text: a block without a task id
|
|
46
|
+
* is skipped, absent fields stay absent, everything is clamped. Exported
|
|
47
|
+
* for tests. */
|
|
48
|
+
export declare function parseTaskNotifications(text: string, timestamp: string): AgentMessageTaskNotification[];
|
|
49
|
+
/** One `system`/`task_notification` stream-json event mapped onto the same
|
|
50
|
+
* structured message the XML parse produces, or null when the event names
|
|
51
|
+
* no task id. Pure and tolerant over untrusted harness JSON: absent fields
|
|
52
|
+
* stay absent, everything is clamped; `output_file` (internal plumbing) is
|
|
53
|
+
* deliberately not forwarded. Exported for tests. */
|
|
54
|
+
export declare function parseSystemTaskNotification(p: Record<string, unknown>, timestamp: string): AgentMessageTaskNotification | null;
|
|
43
55
|
/** Claude Code's real reasoning knob is its own `--effort <level>` flag
|
|
44
56
|
* (low|medium|high|xhigh|max — verified against `claude -p --help`). The
|
|
45
57
|
* CliReasoningEffort union IS the CLI's vocabulary, so the level rides the
|
package/dist/runtimes/codex.d.ts
CHANGED
|
@@ -16,6 +16,14 @@ import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
|
|
|
16
16
|
/** True when an error-item message is a known codex advisory (log-level
|
|
17
17
|
* noise), not a real error. Exported for tests. */
|
|
18
18
|
export declare function isCodexAdvisoryNoise(text: string): boolean;
|
|
19
|
+
/** Bounded wait for a dying previous writer: attempts × sleep = 5s, matching
|
|
20
|
+
* the teardown's SIGTERM grace (sdk _cli-agent.ts REAP_TERM_WAIT_ATTEMPTS). */
|
|
21
|
+
export declare const CODEX_LOCK_WAIT_ATTEMPTS = 20;
|
|
22
|
+
export declare const CODEX_LOCK_WAIT_SECONDS = "0.25";
|
|
23
|
+
/** The sh fragment prepended to a resume launch. `waitAttempts` is a test
|
|
24
|
+
* seam (the live-holder test must not sleep 5s); production callers take
|
|
25
|
+
* the default. Exported for tests. */
|
|
26
|
+
export declare function codexWriterLockPreflight(threadId: string, waitAttempts?: number): string;
|
|
19
27
|
/** Exported for the fixture-based parity tests (ADR-0020): the legacy JSONL
|
|
20
28
|
* `mapEvent` is the golden the ACP normaliser is asserted equal to. Not part
|
|
21
29
|
* of the public runtime surface — `createCodexRuntime` stays the entry point. */
|