@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 +27 -14
- package/dist/agents/shims.d.ts +31 -0
- package/dist/agents/shims.d.ts.map +1 -1
- package/dist/agents/shims.js +153 -29
- package/dist/team-agent-process.d.ts +7 -0
- package/dist/team-agent-process.d.ts.map +1 -1
- package/dist/team-agent-process.js +3 -1
- package/package.json +4 -4
- package/src/agents/shims.ts +184 -29
- package/src/team-agent-process.ts +9 -1
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`
|
|
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 |
|
|
334
|
-
| ----- |
|
|
335
|
-
| omp | ✓ | ✓ |
|
|
336
|
-
| pi | ✓ | ✓ |
|
|
337
|
-
| claude | ✓ | ✗ (ignored) |
|
|
338
|
-
| codex |
|
|
339
|
-
| agy | ✓ | ✗ (ignored) |
|
|
340
|
-
| grok | ✓ | ✗ (ignored) |
|
|
341
|
-
|
|
|
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
|
|
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
|
package/dist/agents/shims.d.ts
CHANGED
|
@@ -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":"AAEA,gEAAgE;AAChE,MAAM,MAAM,SAAS,GACf,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,IAAI,GACJ,UAAU,GACV,iBAAiB,GACjB,UAAU,GACV,QAAQ,GACR,KAAK,GACL,MAAM,GACN,UAAU,CAAC;AAEjB,0CAA0C;AAC1C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzC,mEAAmE;AACnE,MAAM,WAAW,WAAW;IACxB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC1B,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/agents/shims.ts"],"names":[],"mappings":"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"}
|
package/dist/agents/shims.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
*
|
|
299
|
-
*
|
|
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: {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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: {
|
|
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;
|
|
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.
|
|
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.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
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.
|
|
55
|
+
"@gobing-ai/ts-db": "^0.4.69"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public"
|
package/src/agents/shims.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
*
|
|
389
|
-
*
|
|
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: {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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: {
|
|
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);
|