@gobing-ai/ts-ai-runner 0.4.67 → 0.4.69

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/README.md CHANGED
@@ -314,12 +314,15 @@ interactive session (ADR-047).
314
314
  3. else → fresh open.
315
315
 
316
316
  **Capability query (R2):** consult `getAgentSessionCapability(agent)` rather than inventing per-agent
317
- argv. It reports `supportsResumeById` and `supportsSessionDir`.
317
+ argv. It reports `supportsResumeById`, `supportsSessionDir`, `supportsPersistentStdin`,
318
+ `supportsStructuredOutput`, the `verifiedAgainst` CLI version the row was probed on, and a `note`
319
+ naming the reason for any `false` (including CLI flags that exist but are not shim-wired yet).
320
+ Uninstalled CLIs carry `verifiedAgainst: 'unverified …'` — never a fabricated version.
318
321
 
319
322
  ```ts
320
323
  import { getAgentSessionCapability, getAgentShim } from '@gobing-ai/ts-ai-runner';
321
324
 
322
- const cap = getAgentSessionCapability('omp'); // { supportsResumeById: true, supportsSessionDir: true }
325
+ const cap = getAgentSessionCapability('omp'); // { supportsResumeById: true, supportsSessionDir: true, supportsPersistentStdin: true, … }
323
326
  const { args } = getAgentShim('omp').getPromptCommand({
324
327
  input: '',
325
328
  sessionDir: '.spur/run/r1/agent-sessions/omp',
@@ -328,21 +331,31 @@ const { args } = getAgentShim('omp').getPromptCommand({
328
331
  // args: ['-p', '', '--session-dir', '.spur/run/r1/...', '-r', 'abc123', '--mode', 'text']
329
332
  ```
330
333
 
331
- **Shim / capability matrix:**
332
-
333
- | Agent | `supportsResumeById` | `supportsSessionDir` | sessionDir flag | sessionId resume flag | Legacy continue |
334
- | ----- | -------------------- | -------------------- | --------------- | --------------------- | --------------- |
335
- | omp | ✓ | ✓ | `--session-dir <dir>` | `-r <id>` | `-c` |
336
- | pi | ✓ | ✓ | `--session-dir <dir>` | `-r <id>` | `-c` |
337
- | claude | ✓ | ✗ (ignored) | — | `--resume <id>` | `--continue` |
338
- | codex | ✗ | ✗ (ignored) | — | degrade → fresh `exec` | `exec resume --last` |
339
- | agy | ✓ | ✗ (ignored) | — | `--conversation <id>` | `--continue` |
340
- | grok | ✓ | ✗ (ignored) | — | `--resume <id>` | `-c` |
341
- | deepseek | ✗ | ✗ (ignored) | — | degrade → fresh headless one-shot | degrade → fresh headless one-shot |
334
+ **Shim / capability matrix** (probed 2026-09-18; `verifiedAgainst` per row in `getAgentSessionCapability`):
335
+
336
+ | Agent | resumeById | sessionDir | sessionId resume flag | persistentStdin | structuredOutput |
337
+ | ----- | ---------- | ---------- | --------------------- | --------------- | ---------------- |
338
+ | omp | ✓ | ✓ | `-r <id>` | ✓ (`--mode rpc`) | ✓ (`--mode json`) |
339
+ | pi | ✓ | ✓ | `-r <id>` | ✓ (`--mode rpc`) | ✓ (`--mode json`) |
340
+ | claude | ✓ | ✗ (ignored) | `--resume <id>` | ✓ (`--input-format stream-json`; not shim-wired) | ✓ (`--output-format json`) |
341
+ | codex | ✓ (0.154.0) | ✗ (ignored) | `exec resume <id> <prompt>` | ✗ | ✓ (`--json`) |
342
+ | agy | ✓ | ✗ (ignored) | `--conversation <id>` | ✗ | ✗ (unverified) |
343
+ | grok | ✓ | ✗ (ignored) | `--resume <id>` | ✗ | ✓ (`--output-format json`) |
344
+ | gemini | ✗ (`-r` = latest/index) | ✗ | degrade → fresh `-p` | ✗ | ✓ (`-o json`) |
345
+ | opencode | ✗ (CLI `-s <id>` unwired) | ✗ | degrade → fresh `run` | ✗ | ✓ (`--format json`) |
346
+ | openclaw | ✗ (CLI `--session-id` unwired) | ✗ | degrade → fresh `agent` | ✗ | ✓ (CLI `--json`; unwired) |
347
+ | hermes | ✗ (unverified) | ✗ | degrade → fresh `chat` | ✗ | ✗ (unverified) |
348
+ | deepseek | ✗ | ✗ | degrade → fresh headless one-shot | ✗ | ✗ |
349
+
350
+ **Claude session discovery:** claude has no session-dir flag and does not accept a caller-chosen
351
+ session id at open; the headless run's session id is discovered from the process output after the
352
+ run (the `discoverSessionId` contract consumers implement). The capability row encodes exactly
353
+ that: `supportsResumeById: true` (resume later via `--resume <id>`), `supportsSessionDir: false`.
342
354
 
343
355
  **Degrade rule:** when `sessionDir`/`sessionId` is set and the agent lacks resume-by-id or a
344
356
  session-store flag, the shim opens **fresh in isolation** (or plain fresh) — never bare global
345
- continue/last. codex, which has no one-shot resume-by-id, always degrades to a fresh `exec`.
357
+ continue/last. codex pins via non-interactive `exec resume <id> <prompt>` (verified 0.154.0); a
358
+ sessionDir-only request still degrades to a fresh `exec` (no session-dir flag).
346
359
 
347
360
  **Durable vs ephemeral (R4):** for omp/pi, setting `sessionDir` implies a durable session — the shim
348
361
  omits `--no-session` so a session file is written and discoverable. The legacy fresh path (no session
@@ -54,7 +54,21 @@ export interface PromptOptions {
54
54
  type: string;
55
55
  purpose?: string;
56
56
  }>;
57
+ /**
58
+ * Request the agent's persistent-stdin dispatch: argv starts a long-lived
59
+ * process that reads framed prompts from stdin (one per line) instead of
60
+ * consuming a one-shot `-p <input>` and exiting. Only shims exposing a
61
+ * `persistentStdinProtocol` accept this — callers must consult
62
+ * {@link getAgentSessionCapability}.`supportsPersistentStdin` first. In
63
+ * persistent mode the shim omits `input` from argv entirely; the caller
64
+ * writes the identity preamble / prompts via the shim's `frame` (see
65
+ * `TeamAgentProcess` `stdinFramer`). rpc/stream-json modes supersede
66
+ * `mode` (the structured-output flag rides inside the protocol).
67
+ */
68
+ persistentStdin?: boolean;
57
69
  }
70
+ /** Frames one user prompt as a single stdin line for a persistent-stdin dispatch. */
71
+ export type StdinFrame = (input: string) => string;
58
72
  /** Deprecation metadata attached to a retired or superseded agent shim. */
59
73
  export interface AgentDeprecation {
60
74
  /** Release date (ISO) when the id was marked deprecated. */
@@ -82,6 +96,15 @@ export interface AgentShim {
82
96
  getPromptCommand(options: PromptOptions): ShimCommand;
83
97
  /** Build an auth-status command, or null when unsupported. */
84
98
  getAuthCommand(): ShimCommand | null;
99
+ /**
100
+ * Persistent-stdin framing contract; present iff the shim wires a
101
+ * stdin-listener dispatch (mirrors `supportsPersistentStdin`). Callers
102
+ * frame every prompt (including the identity preamble) through it before
103
+ * writing to the process stdin.
104
+ */
105
+ readonly persistentStdinProtocol?: {
106
+ readonly frame: StdinFrame;
107
+ };
85
108
  }
86
109
  /** All bundled agent shims keyed by canonical agent name. */
87
110
  export declare const AGENT_SHIMS: Readonly<Record<AgentName, AgentShim>>;
@@ -91,6 +114,14 @@ export interface AgentSessionCapability {
91
114
  readonly supportsResumeById: boolean;
92
115
  /** Can isolate session storage into a caller-supplied directory. */
93
116
  readonly supportsSessionDir: boolean;
117
+ /** Multi-turn conversation over stdin in one process (e.g. `--input-format stream-json`, `--mode rpc`). */
118
+ readonly supportsPersistentStdin: boolean;
119
+ /** Structured machine-readable output mode (json / stream-json). */
120
+ readonly supportsStructuredOutput: boolean;
121
+ /** Agent CLI version this row was last verified against; `'unverified …'` when the CLI was absent. */
122
+ readonly verifiedAgainst: string;
123
+ /** Recorded reason for any `false` — a CLI gap, or a CLI-supported flag the shim argv does not wire yet. */
124
+ readonly note?: string;
94
125
  }
95
126
  /** Query a bundled agent's session-affinity capability by canonical name. */
96
127
  export declare function getAgentSessionCapability(agent: AgentName): AgentSessionCapability;
@@ -1 +1 @@
1
- {"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/agents/shims.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/agents/shims.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,8 @@
1
1
  import { getLogger } from '@gobing-ai/ts-infra';
2
+ /** pi-family rpc dialect (pi docs/rpc.md): one JSON object per line. */
3
+ const piRpcFrame = (input) => `${JSON.stringify({ type: 'prompt', message: input })}\n`;
4
+ /** claude stream-json input dialect: one user-message JSONL envelope per line. */
5
+ const claudeStreamJsonFrame = (input) => `${JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: input }] } })}\n`;
2
6
  const claudeShim = {
3
7
  name: 'claude',
4
8
  command: 'claude',
@@ -8,7 +12,22 @@ const claudeShim = {
8
12
  getPromptCommand: (options) => {
9
13
  // Headless dispatch cannot answer edit approval prompts. Keep the grant
10
14
  // scoped to file edits; shell and broader tools remain gated.
11
- const args = ['-p', options.input ?? '', '--permission-mode', 'acceptEdits', '--allowedTools', 'Write', 'Edit'];
15
+ // Persistent stdin rides the print path: `--input-format` only works
16
+ // with --print, and stream-json input keeps the process alive reading
17
+ // JSONL envelopes until stdin closes (verified --help, CLI 2.1.274).
18
+ const persistent = options.persistentStdin === true;
19
+ const args = persistent
20
+ ? [
21
+ '-p',
22
+ '--input-format',
23
+ 'stream-json',
24
+ '--permission-mode',
25
+ 'acceptEdits',
26
+ '--allowedTools',
27
+ 'Write',
28
+ 'Edit',
29
+ ]
30
+ : ['-p', options.input ?? '', '--permission-mode', 'acceptEdits', '--allowedTools', 'Write', 'Edit'];
12
31
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
13
32
  if (hasSession) {
14
33
  // Session/pin path — never emit --continue. Claude has no session-dir
@@ -21,10 +40,13 @@ const claudeShim = {
21
40
  }
22
41
  if (options.model !== undefined)
23
42
  args.push('--model', options.model);
24
- args.push('--output-format', options.mode ?? 'text');
43
+ // stream-json output is part of the persistent protocol (the framing
44
+ // assumes JSONL stdout); one-shot keeps the caller-requested mode.
45
+ args.push('--output-format', persistent ? 'stream-json' : (options.mode ?? 'text'));
25
46
  return { command: 'claude', args };
26
47
  },
27
48
  getAuthCommand: () => ({ command: 'claude', args: ['auth', 'status'] }),
49
+ persistentStdinProtocol: { frame: claudeStreamJsonFrame },
28
50
  };
29
51
  const codexShim = {
30
52
  name: 'codex',
@@ -34,12 +56,19 @@ const codexShim = {
34
56
  getVersionCommand: () => ({ command: 'codex', args: ['--version'] }),
35
57
  getPromptCommand: (options) => {
36
58
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
37
- // Session/pin path degrades to a fresh `exec` — codex has no reliable
38
- // one-shot resume-by-id, so never `exec resume --last` (no global last).
39
59
  if (options.continue === true && !hasSession && options.input !== undefined) {
40
60
  throw new Error('Codex resume mode does not accept a new prompt');
41
61
  }
42
- const args = options.continue === true && !hasSession ? ['exec', 'resume', '--last'] : ['exec', options.input ?? ''];
62
+ // Non-interactive resume-by-id (verified codex-cli 0.154.0): `exec resume
63
+ // <id> <prompt>` continues the recorded thread headlessly — the former
64
+ // interactive-picker-only gap is closed (feature B8 R3). sessionDir-only
65
+ // still degrades to fresh exec (no session-dir flag); bare `continue`
66
+ // stays `resume --last` (input omitted — resume-last rejects a prompt).
67
+ const args = hasSession && options.sessionId !== undefined
68
+ ? ['exec', 'resume', options.sessionId, options.input ?? '']
69
+ : options.continue === true && !hasSession
70
+ ? ['exec', 'resume', '--last']
71
+ : ['exec', options.input ?? ''];
43
72
  if (options.model !== undefined)
44
73
  args.push('-m', options.model);
45
74
  if ((options.mode ?? 'text') === 'json')
@@ -75,11 +104,16 @@ const piShim = {
75
104
  getPromptCommand: (options) => {
76
105
  const args = [];
77
106
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
107
+ const persistent = options.persistentStdin === true;
78
108
  // Session/pin path: durable — never ephemeral --no-session, never global -c.
79
- if (!hasSession && options.continue !== true)
109
+ if (!hasSession && !persistent && options.continue !== true)
80
110
  args.push('--no-session');
81
- args.push('-p', options.input ?? '');
82
- if (!hasSession && options.continue === true)
111
+ // Persistent stdin: no -p positional (prompts arrive framed over stdin);
112
+ // rpc mode is the long-lived listener (verified headless probe + pi
113
+ // docs/rpc.md). It supersedes any text/json output mode.
114
+ if (!persistent)
115
+ args.push('-p', options.input ?? '');
116
+ if (!hasSession && !persistent && options.continue === true)
83
117
  args.push('-c');
84
118
  if (options.sessionDir !== undefined)
85
119
  args.push('--session-dir', options.sessionDir);
@@ -87,10 +121,11 @@ const piShim = {
87
121
  args.push('-r', options.sessionId);
88
122
  if (options.model !== undefined)
89
123
  args.push('--model', options.model);
90
- args.push('--mode', options.mode ?? 'text');
124
+ args.push('--mode', persistent ? 'rpc' : (options.mode ?? 'text'));
91
125
  return { command: 'pi', args };
92
126
  },
93
127
  getAuthCommand: () => ({ command: 'pi', args: ['--list-models'] }),
128
+ persistentStdinProtocol: { frame: piRpcFrame },
94
129
  };
95
130
  const opencodeShim = {
96
131
  name: 'opencode',
@@ -199,11 +234,15 @@ const ompShim = {
199
234
  getPromptCommand: (options) => {
200
235
  const args = [];
201
236
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
237
+ const persistent = options.persistentStdin === true;
202
238
  // Session/pin path: durable — never ephemeral --no-session, never global -c.
203
- if (!hasSession && options.continue !== true)
239
+ if (!hasSession && !persistent && options.continue !== true)
204
240
  args.push('--no-session');
205
- args.push('-p', options.input ?? '');
206
- if (!hasSession && options.continue === true)
241
+ // Persistent stdin mirrors pi (omp is a pi fork; same rpc dialect) —
242
+ // verified `--mode rpc` in `omp --help` at the pi-compatible surface.
243
+ if (!persistent)
244
+ args.push('-p', options.input ?? '');
245
+ if (!hasSession && !persistent && options.continue === true)
207
246
  args.push('-c');
208
247
  if (options.sessionDir !== undefined)
209
248
  args.push('--session-dir', options.sessionDir);
@@ -211,10 +250,11 @@ const ompShim = {
211
250
  args.push('-r', options.sessionId);
212
251
  if (options.model !== undefined)
213
252
  args.push('--model', options.model);
214
- args.push('--mode', options.mode ?? 'text');
253
+ args.push('--mode', persistent ? 'rpc' : (options.mode ?? 'text'));
215
254
  return { command: 'omp', args };
216
255
  },
217
256
  getAuthCommand: () => ({ command: 'omp', args: ['--list-models'] }),
257
+ persistentStdinProtocol: { frame: piRpcFrame },
218
258
  };
219
259
  /**
220
260
  * Grok Build CLI (`grok`) — xAI coding agent. Headless one-shot via `-p`/`--single`;
@@ -295,25 +335,109 @@ export const AGENT_SHIMS = {
295
335
  * `supportsResumeById` decides resume-by-id vs fresh-degrade, and
296
336
  * `supportsSessionDir` decides whether `sessionDir` is honored (ADR-047 R2).
297
337
  *
298
- * Agents not in the six-agent affinity matrix default to no resume-by-id and no
299
- * session-dir — they get the isolated-fresh / no-resume degrade.
338
+ * Each row records what the agent CLI verifiably supports at the version named
339
+ * in `verifiedAgainst` (probed via `<agent> --help` / documented flags; feature
340
+ * B8). Every `false` carries a `note` naming the gap — an unexplained `false`
341
+ * is a defect — and a CLI-supported flag the shim argv does not wire yet is
342
+ * called out in the note instead of being silently degraded or guessed `true`.
343
+ * CLIs that were absent on the probing host carry `verifiedAgainst:
344
+ * 'unverified …'`, never a fabricated version.
300
345
  */
301
346
  const AGENT_SESSION_CAPABILITY = {
302
- omp: { supportsResumeById: true, supportsSessionDir: true },
303
- pi: { supportsResumeById: true, supportsSessionDir: true },
304
- claude: { supportsResumeById: true, supportsSessionDir: false },
305
- // codex resume is interactive-only (`exec resume` picker); no one-shot
306
- // resume-by-id and no session-dir — degrades to fresh exec.
307
- codex: { supportsResumeById: false, supportsSessionDir: false },
308
- 'antigravity-cli': { supportsResumeById: true, supportsSessionDir: false },
309
- grok: { supportsResumeById: true, supportsSessionDir: false },
310
- // Non-matrix agents: conservative default (isolated-fresh / no-resume).
311
- gemini: { supportsResumeById: false, supportsSessionDir: false },
312
- opencode: { supportsResumeById: false, supportsSessionDir: false },
313
- openclaw: { supportsResumeById: false, supportsSessionDir: false },
314
- hermes: { supportsResumeById: false, supportsSessionDir: false },
347
+ omp: {
348
+ supportsResumeById: true,
349
+ supportsSessionDir: true,
350
+ supportsPersistentStdin: true,
351
+ supportsStructuredOutput: true,
352
+ verifiedAgainst: '18.2.3',
353
+ },
354
+ pi: {
355
+ supportsResumeById: true,
356
+ supportsSessionDir: true,
357
+ supportsPersistentStdin: true,
358
+ supportsStructuredOutput: true,
359
+ verifiedAgainst: '0.85.1',
360
+ },
361
+ claude: {
362
+ supportsResumeById: true,
363
+ supportsSessionDir: false,
364
+ supportsPersistentStdin: true,
365
+ supportsStructuredOutput: true,
366
+ verifiedAgainst: '2.1.274',
367
+ // Persistent stdin is shim-wired: `-p --input-format stream-json --output-format
368
+ // stream-json` keeps the process alive reading JSONL envelopes (verified --help;
369
+ // --input-format only works with --print).
370
+ note: 'no session-dir flag — sessionDir is ignored (best-effort isolate)',
371
+ },
372
+ codex: {
373
+ supportsResumeById: true,
374
+ supportsSessionDir: false,
375
+ supportsPersistentStdin: false,
376
+ supportsStructuredOutput: true,
377
+ verifiedAgainst: '0.154.0',
378
+ // R3 branch 2: `exec resume <id> <prompt>` is the working non-interactive
379
+ // resume (verified 0.154.0) — wired in getPromptCommand below.
380
+ note: 'no session-dir flag — sessionDir is ignored; `exec` carries one prompt arg (stdin `-` is one-shot), so no multi-turn stdin',
381
+ },
382
+ 'antigravity-cli': {
383
+ supportsResumeById: true,
384
+ supportsSessionDir: false,
385
+ supportsPersistentStdin: false,
386
+ supportsStructuredOutput: false,
387
+ verifiedAgainst: 'unverified (CLI not installed)',
388
+ // resume-by-id is provenance from the shim's own `--conversation <id>`
389
+ // argv, not a guess; agy itself was absent at verification (2026-09-18).
390
+ note: 'resume-by-id per the shim’s documented --conversation <id> argv; agy absent at verification (2026-09-18) — no session-dir, stdin, or structured-output flags documented',
391
+ },
392
+ grok: {
393
+ supportsResumeById: true,
394
+ supportsSessionDir: false,
395
+ supportsPersistentStdin: false,
396
+ supportsStructuredOutput: true,
397
+ verifiedAgainst: '1.0.34',
398
+ note: 'no session-dir flag — sessionDir is ignored (best-effort isolate); no multi-turn stdin input mode',
399
+ },
400
+ gemini: {
401
+ supportsResumeById: false,
402
+ supportsSessionDir: false,
403
+ supportsPersistentStdin: false,
404
+ supportsStructuredOutput: true,
405
+ verifiedAgainst: '0.46.0',
406
+ note: '`-r/--resume` accepts `latest` or a `--list-sessions` index, not a session id; no session-dir flag; no stdin input mode',
407
+ },
408
+ opencode: {
409
+ supportsResumeById: false,
410
+ supportsSessionDir: false,
411
+ supportsPersistentStdin: false,
412
+ supportsStructuredOutput: true,
413
+ verifiedAgainst: '1.17.15',
414
+ note: 'CLI `run -s/--session <id>` supports resume-by-id but the shim argv does not wire it yet (follow-up before declaring true); no session-dir flag; no stdin input mode',
415
+ },
416
+ openclaw: {
417
+ supportsResumeById: false,
418
+ supportsSessionDir: false,
419
+ supportsPersistentStdin: false,
420
+ supportsStructuredOutput: true,
421
+ verifiedAgainst: '2026.6.11',
422
+ note: 'CLI `agent --session-id <id>` and `--json` are not yet shim-wired (follow-up before declaring resume-by-id true); gateway turns have no persistent stdin',
423
+ },
424
+ hermes: {
425
+ supportsResumeById: false,
426
+ supportsSessionDir: false,
427
+ supportsPersistentStdin: false,
428
+ supportsStructuredOutput: false,
429
+ verifiedAgainst: 'unverified (CLI not installed)',
430
+ note: 'CLI absent at verification (2026-09-18) — conservative defaults; `chat --continue` resumes the last session only, no session-dir/stdin/structured-output flags documented',
431
+ },
315
432
  // dsh headless has no resume/session flags — fresh-dispatch degrade.
316
- deepseek: { supportsResumeById: false, supportsSessionDir: false },
433
+ deepseek: {
434
+ supportsResumeById: false,
435
+ supportsSessionDir: false,
436
+ supportsPersistentStdin: false,
437
+ supportsStructuredOutput: false,
438
+ verifiedAgainst: '0.1.5-rc.1',
439
+ note: 'headless app takes only the task positional and -h at 0.1.5-rc.1 — no resume/session/mode flags; fresh-dispatch degrade',
440
+ },
317
441
  };
318
442
  /** Query a bundled agent's session-affinity capability by canonical name. */
319
443
  export function getAgentSessionCapability(agent) {
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
2
2
  import { type EventBus, type Logger } from '@gobing-ai/ts-infra';
3
3
  import { type ProcessExecutor } from '@gobing-ai/ts-runtime';
4
4
  import type { AgentSpec } from './agent-spec';
5
+ import type { StdinFrame } from './agents/shims';
5
6
  import type { AgentEvents } from './events';
6
7
  import { type QuotaAttribution } from './quota';
7
8
  /** Options for spawning a team agent subprocess. */
@@ -16,6 +17,11 @@ export interface AgentProcessOptions {
16
17
  events?: EventBus<AgentEvents>;
17
18
  /** Optional exact quota attribution; absent fields stay absent on observations — never inferred. */
18
19
  quotaContext?: QuotaAttribution;
20
+ /**
21
+ * Frames each sent prompt for the agent's persistent-stdin dialect (from
22
+ * the shim's `persistentStdinProtocol.frame`); absent = raw text + newline.
23
+ */
24
+ stdinFramer?: StdinFrame;
19
25
  }
20
26
  type ProcessStatus = 'running' | 'stopped' | 'errored';
21
27
  /**
@@ -38,6 +44,7 @@ export declare class TeamAgentProcess {
38
44
  private readonly quotaProducer;
39
45
  private readonly spec;
40
46
  private readonly quotaContext;
47
+ private readonly stdinFramer;
41
48
  constructor(options: AgentProcessOptions);
42
49
  start(): Promise<void>;
43
50
  stop(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"team-agent-process.d.ts","sourceRoot":"","sources":["../src/team-agent-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,QAAQ,EAAa,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAoC,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAIH,KAAK,gBAAgB,EAExB,MAAM,SAAS,CAAC;AAEjB,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sHAAsH;IACtH,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/B,oGAAoG;IACpG,YAAY,CAAC,EAAE,gBAAgB,CAAC;CACnC;AAED,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvD;;;GAGG;AACH,qBAAa,gBAAgB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqC;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,kGAAkG;IAClG,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;gBAEhD,OAAO,EAAE,mBAAmB;IAYlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BrB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC;IAerD,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAOvD,SAAS,IAAI,aAAa;IAI1B,MAAM,IAAI,MAAM,GAAG,IAAI;IAIvB,WAAW,IAAI,MAAM,GAAG,IAAI;YAId,IAAI;IAkBlB,sHAAsH;IACtH,OAAO,CAAC,gBAAgB;IAIxB,qGAAqG;IACrG,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,IAAI;CAOf"}
1
+ {"version":3,"file":"team-agent-process.d.ts","sourceRoot":"","sources":["../src/team-agent-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,QAAQ,EAAa,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAoC,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAIH,KAAK,gBAAgB,EAExB,MAAM,SAAS,CAAC;AAEjB,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sHAAsH;IACtH,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/B,oGAAoG;IACpG,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,UAAU,CAAC;CAC5B;AAED,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvD;;;GAGG;AACH,qBAAa,gBAAgB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqC;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,kGAAkG;IAClG,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;gBAEzC,OAAO,EAAE,mBAAmB;IAalC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BrB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC;IAerD,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAOvD,SAAS,IAAI,aAAa;IAI1B,MAAM,IAAI,MAAM,GAAG,IAAI;IAIvB,WAAW,IAAI,MAAM,GAAG,IAAI;YAId,IAAI;IAkBlB,sHAAsH;IACtH,OAAO,CAAC,gBAAgB;IAIxB,qGAAqG;IACrG,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,IAAI;CAOf"}
@@ -22,6 +22,7 @@ export class TeamAgentProcess {
22
22
  quotaProducer;
23
23
  spec;
24
24
  quotaContext;
25
+ stdinFramer;
25
26
  constructor(options) {
26
27
  this.agentId = options.spec.id;
27
28
  this.command = options.command;
@@ -32,6 +33,7 @@ export class TeamAgentProcess {
32
33
  this.quotaProducer = new QuotaObservationProducer(options.events);
33
34
  this.spec = options.spec;
34
35
  this.quotaContext = options.quotaContext;
36
+ this.stdinFramer = options.stdinFramer;
35
37
  }
36
38
  async start() {
37
39
  if (this.status === 'running')
@@ -95,7 +97,7 @@ export class TeamAgentProcess {
95
97
  return { ok: false };
96
98
  }
97
99
  try {
98
- this.subprocess.writeStdin(`${message}\n`);
100
+ this.subprocess.writeStdin(this.stdinFramer !== undefined ? this.stdinFramer(message) : `${message}\n`);
99
101
  return { ok: true };
100
102
  }
101
103
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-ai-runner",
3
- "version": "0.4.67",
3
+ "version": "0.4.69",
4
4
  "description": "@gobing-ai/ts-ai-runner — Coding-agent shims, detection, doctor checks, and prompt execution.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,12 +47,12 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-ai-runner-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-infra": "^0.4.67",
51
- "@gobing-ai/ts-runtime": "^0.4.67"
50
+ "@gobing-ai/ts-infra": "^0.4.69",
51
+ "@gobing-ai/ts-runtime": "^0.4.69"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/bun": "1.3.14",
55
- "@gobing-ai/ts-db": "^0.4.67"
55
+ "@gobing-ai/ts-db": "^0.4.69"
56
56
  },
57
57
  "publishConfig": {
58
58
  "access": "public"
@@ -66,8 +66,30 @@ export interface PromptOptions {
66
66
  taskId?: string;
67
67
  /** Peer agents included in the identity preamble. */
68
68
  peers?: Array<{ id: string; type: string; purpose?: string }>;
69
+ /**
70
+ * Request the agent's persistent-stdin dispatch: argv starts a long-lived
71
+ * process that reads framed prompts from stdin (one per line) instead of
72
+ * consuming a one-shot `-p <input>` and exiting. Only shims exposing a
73
+ * `persistentStdinProtocol` accept this — callers must consult
74
+ * {@link getAgentSessionCapability}.`supportsPersistentStdin` first. In
75
+ * persistent mode the shim omits `input` from argv entirely; the caller
76
+ * writes the identity preamble / prompts via the shim's `frame` (see
77
+ * `TeamAgentProcess` `stdinFramer`). rpc/stream-json modes supersede
78
+ * `mode` (the structured-output flag rides inside the protocol).
79
+ */
80
+ persistentStdin?: boolean;
69
81
  }
70
82
 
83
+ /** Frames one user prompt as a single stdin line for a persistent-stdin dispatch. */
84
+ export type StdinFrame = (input: string) => string;
85
+
86
+ /** pi-family rpc dialect (pi docs/rpc.md): one JSON object per line. */
87
+ const piRpcFrame: StdinFrame = (input) => `${JSON.stringify({ type: 'prompt', message: input })}\n`;
88
+
89
+ /** claude stream-json input dialect: one user-message JSONL envelope per line. */
90
+ const claudeStreamJsonFrame: StdinFrame = (input) =>
91
+ `${JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: input }] } })}\n`;
92
+
71
93
  /** Deprecation metadata attached to a retired or superseded agent shim. */
72
94
  export interface AgentDeprecation {
73
95
  /** Release date (ISO) when the id was marked deprecated. */
@@ -96,6 +118,13 @@ export interface AgentShim {
96
118
  getPromptCommand(options: PromptOptions): ShimCommand;
97
119
  /** Build an auth-status command, or null when unsupported. */
98
120
  getAuthCommand(): ShimCommand | null;
121
+ /**
122
+ * Persistent-stdin framing contract; present iff the shim wires a
123
+ * stdin-listener dispatch (mirrors `supportsPersistentStdin`). Callers
124
+ * frame every prompt (including the identity preamble) through it before
125
+ * writing to the process stdin.
126
+ */
127
+ readonly persistentStdinProtocol?: { readonly frame: StdinFrame };
99
128
  }
100
129
 
101
130
  const claudeShim: AgentShim = {
@@ -107,7 +136,22 @@ const claudeShim: AgentShim = {
107
136
  getPromptCommand: (options) => {
108
137
  // Headless dispatch cannot answer edit approval prompts. Keep the grant
109
138
  // scoped to file edits; shell and broader tools remain gated.
110
- const args = ['-p', options.input ?? '', '--permission-mode', 'acceptEdits', '--allowedTools', 'Write', 'Edit'];
139
+ // Persistent stdin rides the print path: `--input-format` only works
140
+ // with --print, and stream-json input keeps the process alive reading
141
+ // JSONL envelopes until stdin closes (verified --help, CLI 2.1.274).
142
+ const persistent = options.persistentStdin === true;
143
+ const args = persistent
144
+ ? [
145
+ '-p',
146
+ '--input-format',
147
+ 'stream-json',
148
+ '--permission-mode',
149
+ 'acceptEdits',
150
+ '--allowedTools',
151
+ 'Write',
152
+ 'Edit',
153
+ ]
154
+ : ['-p', options.input ?? '', '--permission-mode', 'acceptEdits', '--allowedTools', 'Write', 'Edit'];
111
155
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
112
156
  if (hasSession) {
113
157
  // Session/pin path — never emit --continue. Claude has no session-dir
@@ -117,10 +161,13 @@ const claudeShim: AgentShim = {
117
161
  args.push('--continue');
118
162
  }
119
163
  if (options.model !== undefined) args.push('--model', options.model);
120
- args.push('--output-format', options.mode ?? 'text');
164
+ // stream-json output is part of the persistent protocol (the framing
165
+ // assumes JSONL stdout); one-shot keeps the caller-requested mode.
166
+ args.push('--output-format', persistent ? 'stream-json' : (options.mode ?? 'text'));
121
167
  return { command: 'claude', args };
122
168
  },
123
169
  getAuthCommand: () => ({ command: 'claude', args: ['auth', 'status'] }),
170
+ persistentStdinProtocol: { frame: claudeStreamJsonFrame },
124
171
  };
125
172
 
126
173
  const codexShim: AgentShim = {
@@ -131,13 +178,20 @@ const codexShim: AgentShim = {
131
178
  getVersionCommand: () => ({ command: 'codex', args: ['--version'] }),
132
179
  getPromptCommand: (options) => {
133
180
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
134
- // Session/pin path degrades to a fresh `exec` — codex has no reliable
135
- // one-shot resume-by-id, so never `exec resume --last` (no global last).
136
181
  if (options.continue === true && !hasSession && options.input !== undefined) {
137
182
  throw new Error('Codex resume mode does not accept a new prompt');
138
183
  }
184
+ // Non-interactive resume-by-id (verified codex-cli 0.154.0): `exec resume
185
+ // <id> <prompt>` continues the recorded thread headlessly — the former
186
+ // interactive-picker-only gap is closed (feature B8 R3). sessionDir-only
187
+ // still degrades to fresh exec (no session-dir flag); bare `continue`
188
+ // stays `resume --last` (input omitted — resume-last rejects a prompt).
139
189
  const args =
140
- options.continue === true && !hasSession ? ['exec', 'resume', '--last'] : ['exec', options.input ?? ''];
190
+ hasSession && options.sessionId !== undefined
191
+ ? ['exec', 'resume', options.sessionId, options.input ?? '']
192
+ : options.continue === true && !hasSession
193
+ ? ['exec', 'resume', '--last']
194
+ : ['exec', options.input ?? ''];
141
195
  if (options.model !== undefined) args.push('-m', options.model);
142
196
  if ((options.mode ?? 'text') === 'json') args.push('--json');
143
197
  return { command: 'codex', args };
@@ -171,17 +225,22 @@ const piShim: AgentShim = {
171
225
  getPromptCommand: (options) => {
172
226
  const args: string[] = [];
173
227
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
228
+ const persistent = options.persistentStdin === true;
174
229
  // Session/pin path: durable — never ephemeral --no-session, never global -c.
175
- if (!hasSession && options.continue !== true) args.push('--no-session');
176
- args.push('-p', options.input ?? '');
177
- if (!hasSession && options.continue === true) args.push('-c');
230
+ if (!hasSession && !persistent && options.continue !== true) args.push('--no-session');
231
+ // Persistent stdin: no -p positional (prompts arrive framed over stdin);
232
+ // rpc mode is the long-lived listener (verified headless probe + pi
233
+ // docs/rpc.md). It supersedes any text/json output mode.
234
+ if (!persistent) args.push('-p', options.input ?? '');
235
+ if (!hasSession && !persistent && options.continue === true) args.push('-c');
178
236
  if (options.sessionDir !== undefined) args.push('--session-dir', options.sessionDir);
179
237
  if (options.sessionId !== undefined) args.push('-r', options.sessionId);
180
238
  if (options.model !== undefined) args.push('--model', options.model);
181
- args.push('--mode', options.mode ?? 'text');
239
+ args.push('--mode', persistent ? 'rpc' : (options.mode ?? 'text'));
182
240
  return { command: 'pi', args };
183
241
  },
184
242
  getAuthCommand: () => ({ command: 'pi', args: ['--list-models'] }),
243
+ persistentStdinProtocol: { frame: piRpcFrame },
185
244
  };
186
245
 
187
246
  const opencodeShim: AgentShim = {
@@ -285,17 +344,21 @@ const ompShim: AgentShim = {
285
344
  getPromptCommand: (options) => {
286
345
  const args: string[] = [];
287
346
  const hasSession = options.sessionId !== undefined || options.sessionDir !== undefined;
347
+ const persistent = options.persistentStdin === true;
288
348
  // Session/pin path: durable — never ephemeral --no-session, never global -c.
289
- if (!hasSession && options.continue !== true) args.push('--no-session');
290
- args.push('-p', options.input ?? '');
291
- if (!hasSession && options.continue === true) args.push('-c');
349
+ if (!hasSession && !persistent && options.continue !== true) args.push('--no-session');
350
+ // Persistent stdin mirrors pi (omp is a pi fork; same rpc dialect) —
351
+ // verified `--mode rpc` in `omp --help` at the pi-compatible surface.
352
+ if (!persistent) args.push('-p', options.input ?? '');
353
+ if (!hasSession && !persistent && options.continue === true) args.push('-c');
292
354
  if (options.sessionDir !== undefined) args.push('--session-dir', options.sessionDir);
293
355
  if (options.sessionId !== undefined) args.push('-r', options.sessionId);
294
356
  if (options.model !== undefined) args.push('--model', options.model);
295
- args.push('--mode', options.mode ?? 'text');
357
+ args.push('--mode', persistent ? 'rpc' : (options.mode ?? 'text'));
296
358
  return { command: 'omp', args };
297
359
  },
298
360
  getAuthCommand: () => ({ command: 'omp', args: ['--list-models'] }),
361
+ persistentStdinProtocol: { frame: piRpcFrame },
299
362
  };
300
363
 
301
364
  /**
@@ -377,6 +440,14 @@ export interface AgentSessionCapability {
377
440
  readonly supportsResumeById: boolean;
378
441
  /** Can isolate session storage into a caller-supplied directory. */
379
442
  readonly supportsSessionDir: boolean;
443
+ /** Multi-turn conversation over stdin in one process (e.g. `--input-format stream-json`, `--mode rpc`). */
444
+ readonly supportsPersistentStdin: boolean;
445
+ /** Structured machine-readable output mode (json / stream-json). */
446
+ readonly supportsStructuredOutput: boolean;
447
+ /** Agent CLI version this row was last verified against; `'unverified …'` when the CLI was absent. */
448
+ readonly verifiedAgainst: string;
449
+ /** Recorded reason for any `false` — a CLI gap, or a CLI-supported flag the shim argv does not wire yet. */
450
+ readonly note?: string;
380
451
  }
381
452
 
382
453
  /**
@@ -385,25 +456,109 @@ export interface AgentSessionCapability {
385
456
  * `supportsResumeById` decides resume-by-id vs fresh-degrade, and
386
457
  * `supportsSessionDir` decides whether `sessionDir` is honored (ADR-047 R2).
387
458
  *
388
- * Agents not in the six-agent affinity matrix default to no resume-by-id and no
389
- * session-dir — they get the isolated-fresh / no-resume degrade.
459
+ * Each row records what the agent CLI verifiably supports at the version named
460
+ * in `verifiedAgainst` (probed via `<agent> --help` / documented flags; feature
461
+ * B8). Every `false` carries a `note` naming the gap — an unexplained `false`
462
+ * is a defect — and a CLI-supported flag the shim argv does not wire yet is
463
+ * called out in the note instead of being silently degraded or guessed `true`.
464
+ * CLIs that were absent on the probing host carry `verifiedAgainst:
465
+ * 'unverified …'`, never a fabricated version.
390
466
  */
391
467
  const AGENT_SESSION_CAPABILITY: Readonly<Record<AgentName, AgentSessionCapability>> = {
392
- omp: { supportsResumeById: true, supportsSessionDir: true },
393
- pi: { supportsResumeById: true, supportsSessionDir: true },
394
- claude: { supportsResumeById: true, supportsSessionDir: false },
395
- // codex resume is interactive-only (`exec resume` picker); no one-shot
396
- // resume-by-id and no session-dir — degrades to fresh exec.
397
- codex: { supportsResumeById: false, supportsSessionDir: false },
398
- 'antigravity-cli': { supportsResumeById: true, supportsSessionDir: false },
399
- grok: { supportsResumeById: true, supportsSessionDir: false },
400
- // Non-matrix agents: conservative default (isolated-fresh / no-resume).
401
- gemini: { supportsResumeById: false, supportsSessionDir: false },
402
- opencode: { supportsResumeById: false, supportsSessionDir: false },
403
- openclaw: { supportsResumeById: false, supportsSessionDir: false },
404
- hermes: { supportsResumeById: false, supportsSessionDir: false },
468
+ omp: {
469
+ supportsResumeById: true,
470
+ supportsSessionDir: true,
471
+ supportsPersistentStdin: true,
472
+ supportsStructuredOutput: true,
473
+ verifiedAgainst: '18.2.3',
474
+ },
475
+ pi: {
476
+ supportsResumeById: true,
477
+ supportsSessionDir: true,
478
+ supportsPersistentStdin: true,
479
+ supportsStructuredOutput: true,
480
+ verifiedAgainst: '0.85.1',
481
+ },
482
+ claude: {
483
+ supportsResumeById: true,
484
+ supportsSessionDir: false,
485
+ supportsPersistentStdin: true,
486
+ supportsStructuredOutput: true,
487
+ verifiedAgainst: '2.1.274',
488
+ // Persistent stdin is shim-wired: `-p --input-format stream-json --output-format
489
+ // stream-json` keeps the process alive reading JSONL envelopes (verified --help;
490
+ // --input-format only works with --print).
491
+ note: 'no session-dir flag — sessionDir is ignored (best-effort isolate)',
492
+ },
493
+ codex: {
494
+ supportsResumeById: true,
495
+ supportsSessionDir: false,
496
+ supportsPersistentStdin: false,
497
+ supportsStructuredOutput: true,
498
+ verifiedAgainst: '0.154.0',
499
+ // R3 branch 2: `exec resume <id> <prompt>` is the working non-interactive
500
+ // resume (verified 0.154.0) — wired in getPromptCommand below.
501
+ note: 'no session-dir flag — sessionDir is ignored; `exec` carries one prompt arg (stdin `-` is one-shot), so no multi-turn stdin',
502
+ },
503
+ 'antigravity-cli': {
504
+ supportsResumeById: true,
505
+ supportsSessionDir: false,
506
+ supportsPersistentStdin: false,
507
+ supportsStructuredOutput: false,
508
+ verifiedAgainst: 'unverified (CLI not installed)',
509
+ // resume-by-id is provenance from the shim's own `--conversation <id>`
510
+ // argv, not a guess; agy itself was absent at verification (2026-09-18).
511
+ note: 'resume-by-id per the shim’s documented --conversation <id> argv; agy absent at verification (2026-09-18) — no session-dir, stdin, or structured-output flags documented',
512
+ },
513
+ grok: {
514
+ supportsResumeById: true,
515
+ supportsSessionDir: false,
516
+ supportsPersistentStdin: false,
517
+ supportsStructuredOutput: true,
518
+ verifiedAgainst: '1.0.34',
519
+ note: 'no session-dir flag — sessionDir is ignored (best-effort isolate); no multi-turn stdin input mode',
520
+ },
521
+ gemini: {
522
+ supportsResumeById: false,
523
+ supportsSessionDir: false,
524
+ supportsPersistentStdin: false,
525
+ supportsStructuredOutput: true,
526
+ verifiedAgainst: '0.46.0',
527
+ note: '`-r/--resume` accepts `latest` or a `--list-sessions` index, not a session id; no session-dir flag; no stdin input mode',
528
+ },
529
+ opencode: {
530
+ supportsResumeById: false,
531
+ supportsSessionDir: false,
532
+ supportsPersistentStdin: false,
533
+ supportsStructuredOutput: true,
534
+ verifiedAgainst: '1.17.15',
535
+ note: 'CLI `run -s/--session <id>` supports resume-by-id but the shim argv does not wire it yet (follow-up before declaring true); no session-dir flag; no stdin input mode',
536
+ },
537
+ openclaw: {
538
+ supportsResumeById: false,
539
+ supportsSessionDir: false,
540
+ supportsPersistentStdin: false,
541
+ supportsStructuredOutput: true,
542
+ verifiedAgainst: '2026.6.11',
543
+ note: 'CLI `agent --session-id <id>` and `--json` are not yet shim-wired (follow-up before declaring resume-by-id true); gateway turns have no persistent stdin',
544
+ },
545
+ hermes: {
546
+ supportsResumeById: false,
547
+ supportsSessionDir: false,
548
+ supportsPersistentStdin: false,
549
+ supportsStructuredOutput: false,
550
+ verifiedAgainst: 'unverified (CLI not installed)',
551
+ note: 'CLI absent at verification (2026-09-18) — conservative defaults; `chat --continue` resumes the last session only, no session-dir/stdin/structured-output flags documented',
552
+ },
405
553
  // dsh headless has no resume/session flags — fresh-dispatch degrade.
406
- deepseek: { supportsResumeById: false, supportsSessionDir: false },
554
+ deepseek: {
555
+ supportsResumeById: false,
556
+ supportsSessionDir: false,
557
+ supportsPersistentStdin: false,
558
+ supportsStructuredOutput: false,
559
+ verifiedAgainst: '0.1.5-rc.1',
560
+ note: 'headless app takes only the task positional and -h at 0.1.5-rc.1 — no resume/session/mode flags; fresh-dispatch degrade',
561
+ },
407
562
  };
408
563
 
409
564
  /** Query a bundled agent's session-affinity capability by canonical name. */
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
2
2
  import { type EventBus, getLogger, type Logger } from '@gobing-ai/ts-infra';
3
3
  import { nodeBunFactory, type PipeProcess, type ProcessExecutor } from '@gobing-ai/ts-runtime';
4
4
  import type { AgentSpec } from './agent-spec';
5
+ import type { StdinFrame } from './agents/shims';
5
6
  import type { AgentEvents } from './events';
6
7
  import {
7
8
  buildQuotaObservation,
@@ -23,6 +24,11 @@ export interface AgentProcessOptions {
23
24
  events?: EventBus<AgentEvents>;
24
25
  /** Optional exact quota attribution; absent fields stay absent on observations — never inferred. */
25
26
  quotaContext?: QuotaAttribution;
27
+ /**
28
+ * Frames each sent prompt for the agent's persistent-stdin dialect (from
29
+ * the shim's `persistentStdinProtocol.frame`); absent = raw text + newline.
30
+ */
31
+ stdinFramer?: StdinFrame;
26
32
  }
27
33
 
28
34
  type ProcessStatus = 'running' | 'stopped' | 'errored';
@@ -47,6 +53,7 @@ export class TeamAgentProcess {
47
53
  private readonly quotaProducer: QuotaObservationProducer;
48
54
  private readonly spec: AgentSpec;
49
55
  private readonly quotaContext: QuotaAttribution | undefined;
56
+ private readonly stdinFramer: StdinFrame | undefined;
50
57
 
51
58
  constructor(options: AgentProcessOptions) {
52
59
  this.agentId = options.spec.id;
@@ -58,6 +65,7 @@ export class TeamAgentProcess {
58
65
  this.quotaProducer = new QuotaObservationProducer(options.events);
59
66
  this.spec = options.spec;
60
67
  this.quotaContext = options.quotaContext;
68
+ this.stdinFramer = options.stdinFramer;
61
69
  }
62
70
 
63
71
  async start(): Promise<void> {
@@ -117,7 +125,7 @@ export class TeamAgentProcess {
117
125
  return { ok: false };
118
126
  }
119
127
  try {
120
- this.subprocess.writeStdin(`${message}\n`);
128
+ this.subprocess.writeStdin(this.stdinFramer !== undefined ? this.stdinFramer(message) : `${message}\n`);
121
129
  return { ok: true };
122
130
  } catch (error) {
123
131
  this.warn('stdin write failed', 'send.writeStdin', error);