@inixiative/agent-session 0.6.6 → 0.6.8
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 +10 -0
- package/package.json +2 -2
- package/src/claude-code-session.ts +60 -11
- package/src/codex-session.ts +41 -9
- package/src/harness-session.ts +66 -0
- package/src/transport.ts +9 -0
package/README.md
CHANGED
|
@@ -44,8 +44,18 @@ session.kill();
|
|
|
44
44
|
`ANTHROPIC_API_KEY` and authenticates via the CLI's subscription / an
|
|
45
45
|
`CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` — works headless and in
|
|
46
46
|
containers.
|
|
47
|
+
- **Images with a turn.** `send(text, { images: [{ mediaType, data }] })` (png, jpeg, gif or
|
|
48
|
+
webp, base64) where the transport declares `capabilities.images`: Claude gets base64 `image`
|
|
49
|
+
blocks in the same user message, Codex `image` inputs with a `data:` URL. Anything else is
|
|
50
|
+
refused before a byte is written.
|
|
47
51
|
- **fork / resume / interrupt** and pre-send hooks (rewrite the outgoing
|
|
48
52
|
message — e.g. inject just-in-time context per turn).
|
|
53
|
+
- **A resume whose native conversation is not found is typed.** Claude finding no transcript
|
|
54
|
+
("No conversation found with session ID": cleaned up, deleted, or under another project
|
|
55
|
+
directory or config home) and Codex finding no rollout ("no rollout found for thread id") both
|
|
56
|
+
raise `NativeBindingMissingError` (`nativeBindingMissing(error)` finds it behind a turn error,
|
|
57
|
+
with the `cwd` the lookup ran in). No turn ran, so the attempt settles as a no-send instead of
|
|
58
|
+
uncertain native work, and the caller can start fresh from its own record.
|
|
49
59
|
- **Transports with declared capabilities.** How a model is reached is a
|
|
50
60
|
pluggable transport; switching it is a routing change, not a rewrite.
|
|
51
61
|
- **Zero dependencies.** Just Bun + the agent CLI you're driving. The Agent SDK
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/agent-session",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions over interchangeable transports, with primed decision sessions, subscription pools, routing, limit polling and continuity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@biomejs/biome": "2.5.2",
|
|
43
|
-
"@inixiative/config": "^0.5.
|
|
43
|
+
"@inixiative/config": "^0.5.118",
|
|
44
44
|
"@types/bun": "1.4.2",
|
|
45
45
|
"lefthook": "2.1.9",
|
|
46
46
|
"typescript": "6.0.3"
|
|
@@ -54,6 +54,7 @@ import type {
|
|
|
54
54
|
SessionArtifact,
|
|
55
55
|
SessionEvent,
|
|
56
56
|
SessionEventHandler,
|
|
57
|
+
SessionImage,
|
|
57
58
|
SessionResult,
|
|
58
59
|
SessionSendOptions,
|
|
59
60
|
SessionTokens,
|
|
@@ -62,6 +63,7 @@ import type {
|
|
|
62
63
|
ToolGate,
|
|
63
64
|
ToolRequest,
|
|
64
65
|
} from './harness-session';
|
|
66
|
+
import { NativeBindingMissingError, sendableImages } from './harness-session';
|
|
65
67
|
|
|
66
68
|
// Re-export types so existing import paths keep working
|
|
67
69
|
export type {
|
|
@@ -211,6 +213,7 @@ interface QueuedTurn {
|
|
|
211
213
|
onAdmission?: SessionSendOptions['onAdmission'];
|
|
212
214
|
evidence: TurnState;
|
|
213
215
|
message: string;
|
|
216
|
+
images: readonly SessionImage[];
|
|
214
217
|
timeout: number;
|
|
215
218
|
resolve: (result: SessionResult) => void;
|
|
216
219
|
reject: (error: Error) => void;
|
|
@@ -351,6 +354,8 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
351
354
|
// so we know the concrete types at runtime.
|
|
352
355
|
private _proc: PipedSubprocess | null = null;
|
|
353
356
|
private _stderr = '';
|
|
357
|
+
/** The stderr reader of the running process; awaited (capped) before stderr is interpreted. */
|
|
358
|
+
private _stderrDone: Promise<void> = Promise.resolve();
|
|
354
359
|
|
|
355
360
|
// -- Session state --
|
|
356
361
|
/**
|
|
@@ -557,7 +562,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
557
562
|
// Background readers — run for session lifetime (don't await)
|
|
558
563
|
const proc = this._proc;
|
|
559
564
|
const stdoutDone = this._readStdout(proc);
|
|
560
|
-
this._readStderr();
|
|
565
|
+
this._stderrDone = this._readStderr();
|
|
561
566
|
|
|
562
567
|
// Monitor process exit for cleanup
|
|
563
568
|
// The process read its base context file at startup; it goes when the process does.
|
|
@@ -574,7 +579,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
574
579
|
const errMsg = this._stderr.trim()
|
|
575
580
|
? `Process exited (code ${code}): ${this._stderr.trim().slice(0, 500)}`
|
|
576
581
|
: `Process exited with code ${code}`;
|
|
577
|
-
this._rejectInflight(new Error(errMsg));
|
|
582
|
+
this._rejectInflight((await this._settleMissing(errMsg)) ?? new Error(errMsg));
|
|
578
583
|
this._rejectQueue(new Error('Session ended'));
|
|
579
584
|
this._rejectControl(new Error('Session ended'));
|
|
580
585
|
this._emit({ kind: 'session_end', timestamp: Date.now(), transportOutcome: 'failed' });
|
|
@@ -613,6 +618,8 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
613
618
|
);
|
|
614
619
|
} catch (error) {
|
|
615
620
|
this.kill();
|
|
621
|
+
const missing = this._missingBinding((error as Error).message);
|
|
622
|
+
if (missing) throw missing;
|
|
616
623
|
throw new Error(
|
|
617
624
|
`${this._toolGate ? 'Tool gate' : 'Extension'} registration failed: ${(error as Error).message}`,
|
|
618
625
|
);
|
|
@@ -627,6 +634,41 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
627
634
|
}
|
|
628
635
|
}
|
|
629
636
|
|
|
637
|
+
/**
|
|
638
|
+
* The CLI refused to resume because the transcript is gone (Claude Code's cleanup of
|
|
639
|
+
* `~/.claude/projects`, or a deleted file): it says so and exits before reading a turn.
|
|
640
|
+
*/
|
|
641
|
+
private _missingBinding(detail: string): NativeBindingMissingError | undefined {
|
|
642
|
+
const binding = this._externalSessionId;
|
|
643
|
+
if (!binding) return undefined;
|
|
644
|
+
const marker = `No conversation found with session ID: ${binding}`;
|
|
645
|
+
return this._stderr.includes(marker) || detail.includes(marker)
|
|
646
|
+
? new NativeBindingMissingError('claude-code', binding, marker, this._cwd)
|
|
647
|
+
: undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* After the process ended: was the binding missing? stderr is read first (capped), since the
|
|
652
|
+
* marker is there. A turn written to a CLI that never loaded the conversation (no `system/init`,
|
|
653
|
+
* no terminal) could not run: it settles as a no-send rather than uncertain native work.
|
|
654
|
+
*/
|
|
655
|
+
private async _settleMissing(detail: string): Promise<NativeBindingMissingError | undefined> {
|
|
656
|
+
await Promise.race([this._stderrDone, Bun.sleep(250)]);
|
|
657
|
+
const missing = this._missingBinding(detail);
|
|
658
|
+
const a = this._inflight?.evidence;
|
|
659
|
+
if (
|
|
660
|
+
missing &&
|
|
661
|
+
a &&
|
|
662
|
+
a.nativeOutcome === 'unknown' &&
|
|
663
|
+
!a.events.some((event) => {
|
|
664
|
+
const raw = event.raw as { type?: unknown; subtype?: unknown } | undefined;
|
|
665
|
+
return raw?.type === 'system' && raw.subtype === 'init';
|
|
666
|
+
})
|
|
667
|
+
)
|
|
668
|
+
a.dispatch = 'not-dispatched';
|
|
669
|
+
return missing;
|
|
670
|
+
}
|
|
671
|
+
|
|
630
672
|
private _detachExtension(reason: string): void {
|
|
631
673
|
try {
|
|
632
674
|
this._extension?.detach(reason);
|
|
@@ -671,6 +713,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
671
713
|
if (!this._alive) throw new Error('Session ended');
|
|
672
714
|
|
|
673
715
|
const timeout = opts?.timeout ?? this._defaultTimeout;
|
|
716
|
+
const images = sendableImages(this.transport, opts?.images);
|
|
674
717
|
|
|
675
718
|
// Compose pre-send hooks in registration order. Each sees the previous
|
|
676
719
|
// hook's output. Errors in a hook reject the send() — callers should
|
|
@@ -686,6 +729,7 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
686
729
|
const turn: QueuedTurn = {
|
|
687
730
|
evidence,
|
|
688
731
|
message: transformed,
|
|
732
|
+
images,
|
|
689
733
|
timeout,
|
|
690
734
|
resolve,
|
|
691
735
|
reject,
|
|
@@ -937,11 +981,18 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
937
981
|
// Wire format validated empirically against claude 2.1.114:
|
|
938
982
|
// {type:"user", message:{role,content:[{type:"text",text}]}}
|
|
939
983
|
// Alternative shapes ({type:"user_message"}, {role,content}) are silently dropped.
|
|
984
|
+
// Images are Messages API base64 image blocks, ahead of the text they accompany.
|
|
940
985
|
const payload = `${JSON.stringify({
|
|
941
986
|
type: 'user',
|
|
942
987
|
message: {
|
|
943
988
|
role: 'user',
|
|
944
|
-
content: [
|
|
989
|
+
content: [
|
|
990
|
+
...turn.images.map((image) => ({
|
|
991
|
+
type: 'image',
|
|
992
|
+
source: { type: 'base64', media_type: image.mediaType, data: image.data },
|
|
993
|
+
})),
|
|
994
|
+
{ type: 'text', text: turn.message },
|
|
995
|
+
],
|
|
945
996
|
},
|
|
946
997
|
})}\n`;
|
|
947
998
|
try {
|
|
@@ -1053,14 +1104,12 @@ export class ClaudeCodeSession implements HarnessSession {
|
|
|
1053
1104
|
if (buffer.trim() && (this._proc === proc || !this._proc)) {
|
|
1054
1105
|
this._processLine(buffer);
|
|
1055
1106
|
}
|
|
1056
|
-
if (this._proc === proc && this._alive)
|
|
1057
|
-
this.
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
),
|
|
1063
|
-
);
|
|
1107
|
+
if (this._proc === proc && this._alive) {
|
|
1108
|
+
const detail = this._stderr.trim()
|
|
1109
|
+
? `Native stdout closed: ${this._stderr.trim().slice(0, 500)}`
|
|
1110
|
+
: 'Native stdout closed';
|
|
1111
|
+
this._transportFailure((await this._settleMissing(detail)) ?? new Error(detail));
|
|
1112
|
+
}
|
|
1064
1113
|
} catch (err) {
|
|
1065
1114
|
if (this._proc === proc) this._transportFailure(err as Error);
|
|
1066
1115
|
}
|
package/src/codex-session.ts
CHANGED
|
@@ -23,9 +23,11 @@ import type {
|
|
|
23
23
|
SessionArtifact,
|
|
24
24
|
SessionEvent,
|
|
25
25
|
SessionEventHandler,
|
|
26
|
+
SessionImage,
|
|
26
27
|
SessionResult,
|
|
27
28
|
ToolGate,
|
|
28
29
|
} from './harness-session';
|
|
30
|
+
import { NativeBindingMissingError, sendableImages } from './harness-session';
|
|
29
31
|
|
|
30
32
|
// Re-export types so importers can stay on one path (mirrors claude-code-session).
|
|
31
33
|
export type {
|
|
@@ -153,6 +155,7 @@ interface QueuedTurn {
|
|
|
153
155
|
onAdmission?: import('./harness-session').SessionSendOptions['onAdmission'];
|
|
154
156
|
evidence: TurnState;
|
|
155
157
|
message: string;
|
|
158
|
+
images: readonly SessionImage[];
|
|
156
159
|
timeout: number;
|
|
157
160
|
resolve: (result: SessionResult) => void;
|
|
158
161
|
reject: (error: Error) => void;
|
|
@@ -455,6 +458,7 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
455
458
|
if (!this._alive) throw new Error('Session ended');
|
|
456
459
|
|
|
457
460
|
const timeout = opts?.timeout ?? this._defaultTimeout;
|
|
461
|
+
const images = sendableImages(this.transport, opts?.images);
|
|
458
462
|
|
|
459
463
|
let transformed = message;
|
|
460
464
|
for (const hook of this._beforeSendHooks) {
|
|
@@ -473,6 +477,7 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
473
477
|
const turn: QueuedTurn = {
|
|
474
478
|
evidence,
|
|
475
479
|
message: transformed,
|
|
480
|
+
images,
|
|
476
481
|
timeout,
|
|
477
482
|
resolve,
|
|
478
483
|
reject,
|
|
@@ -539,7 +544,7 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
539
544
|
this._resultText = '';
|
|
540
545
|
|
|
541
546
|
// Resolves once the native terminal and the turn/start RPC have both settled.
|
|
542
|
-
this._sendTurn(turn.message)
|
|
547
|
+
this._sendTurn(turn.message, turn.images)
|
|
543
548
|
.then((tokens) => {
|
|
544
549
|
if (this._inflight !== turn) return;
|
|
545
550
|
turn.evidence.rpcSettled = true;
|
|
@@ -587,8 +592,11 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
587
592
|
|
|
588
593
|
private _releaseKnownTurn(): void {
|
|
589
594
|
const a = this._inflight?.evidence;
|
|
590
|
-
if (!a
|
|
591
|
-
|
|
595
|
+
if (!a) return;
|
|
596
|
+
// Refused before `turn/start` was written: nothing native is outstanding.
|
|
597
|
+
const noSend = a.dispatch === 'not-dispatched' && a.localOutcome === 'rejected';
|
|
598
|
+
if (!noSend && (!this._canReleaseAttempt(a) || a.nativeOutcome === 'unknown')) return;
|
|
599
|
+
if (!noSend) this._turns++;
|
|
592
600
|
this._inflight = null;
|
|
593
601
|
this._processNextTurn();
|
|
594
602
|
}
|
|
@@ -1209,13 +1217,17 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
1209
1217
|
this._rpcNotify('initialized');
|
|
1210
1218
|
}
|
|
1211
1219
|
|
|
1212
|
-
private async _sendTurn(
|
|
1220
|
+
private async _sendTurn(
|
|
1221
|
+
message: string,
|
|
1222
|
+
images: readonly SessionImage[],
|
|
1223
|
+
): Promise<{ input: number; output: number } | undefined> {
|
|
1213
1224
|
const a = this._inflight!.evidence;
|
|
1214
1225
|
if (!this._threadReady) {
|
|
1215
1226
|
const binding = this._externalSessionId;
|
|
1216
1227
|
const method = binding ? 'thread/resume' : 'thread/start';
|
|
1217
|
-
|
|
1218
|
-
|
|
1228
|
+
let resumed: unknown;
|
|
1229
|
+
try {
|
|
1230
|
+
resumed = await this._rpcRequest(method, {
|
|
1219
1231
|
...(binding ? { threadId: binding } : {}),
|
|
1220
1232
|
model: this._model,
|
|
1221
1233
|
cwd: this._cwd,
|
|
@@ -1224,8 +1236,22 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
1224
1236
|
...(this._gated ? { approvalsReviewer: 'user' } : {}),
|
|
1225
1237
|
sandbox: this._sandbox,
|
|
1226
1238
|
...(this._options.config ? { config: this._options.config } : {}),
|
|
1227
|
-
})
|
|
1228
|
-
)
|
|
1239
|
+
});
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
// The rollout is gone (Codex's own cleanup, or deleted). Codex answered that no thread
|
|
1242
|
+
// exists, so no turn could run and none was written: a settled no-send, not uncertain
|
|
1243
|
+
// native work. Any other failure here keeps its outcome unknown.
|
|
1244
|
+
if (binding && /^no rollout found for thread id\b/i.test((error as Error).message ?? '')) {
|
|
1245
|
+
a.dispatch = 'not-dispatched';
|
|
1246
|
+
throw new NativeBindingMissingError(
|
|
1247
|
+
'codex-app-server',
|
|
1248
|
+
binding,
|
|
1249
|
+
(error as Error).message,
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
const response = object(resumed);
|
|
1229
1255
|
const thread = object(response?.thread),
|
|
1230
1256
|
id = thread?.id;
|
|
1231
1257
|
if (typeof id !== 'string' || !id || (binding && id !== binding))
|
|
@@ -1344,7 +1370,13 @@ export class CodexAppServerSession implements HarnessSession {
|
|
|
1344
1370
|
});
|
|
1345
1371
|
await this._rpcRequest('turn/start', {
|
|
1346
1372
|
threadId: this._externalSessionId,
|
|
1347
|
-
input: [
|
|
1373
|
+
input: [
|
|
1374
|
+
{ type: 'text', text: message },
|
|
1375
|
+
...images.map((image) => ({
|
|
1376
|
+
type: 'image',
|
|
1377
|
+
url: `data:${image.mediaType};base64,${image.data}`,
|
|
1378
|
+
})),
|
|
1379
|
+
],
|
|
1348
1380
|
...(this._effort ? { effort: this._effort } : {}),
|
|
1349
1381
|
});
|
|
1350
1382
|
await completed; // Native terminal and RPC acknowledgment are both required.
|
package/src/harness-session.ts
CHANGED
|
@@ -79,6 +79,35 @@ export class SessionTurnError extends Error {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The native binding a session was told to resume was not found by the runtime, from this
|
|
84
|
+
* session's cwd and config home: Claude found no transcript ("No conversation found with session
|
|
85
|
+
* ID": cleaned up, deleted, or kept under another project directory or `CLAUDE_CONFIG_DIR`), or
|
|
86
|
+
* Codex found no rollout ("no rollout found for thread id": deleted, or under another
|
|
87
|
+
* `CODEX_HOME`). No turn ran. The caller decides what comes next, typically a fresh session seeded
|
|
88
|
+
* from its own record of the conversation; `cwd` says where the lookup was made.
|
|
89
|
+
*/
|
|
90
|
+
export class NativeBindingMissingError extends Error {
|
|
91
|
+
constructor(
|
|
92
|
+
readonly runtime: 'claude-code' | 'codex-app-server',
|
|
93
|
+
readonly binding: string,
|
|
94
|
+
message: string,
|
|
95
|
+
readonly cwd?: string,
|
|
96
|
+
) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = 'NativeBindingMissingError';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The missing-binding error behind a session error, if any (it may ride as a turn error's cause). */
|
|
103
|
+
export function nativeBindingMissing(error: unknown): NativeBindingMissingError | undefined {
|
|
104
|
+
for (let current = error, depth = 0; current && depth < 5; depth++) {
|
|
105
|
+
if (current instanceof NativeBindingMissingError) return current;
|
|
106
|
+
current = (current as { cause?: unknown }).cause;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
export type SessionEventKind =
|
|
83
112
|
| 'text_delta'
|
|
84
113
|
| 'native_status'
|
|
@@ -266,10 +295,47 @@ export type SessionEventHandler = (event: SessionEvent) => void;
|
|
|
266
295
|
*/
|
|
267
296
|
export type BeforeSendHook = (message: string) => string | Promise<string>;
|
|
268
297
|
|
|
298
|
+
export const SESSION_IMAGE_MEDIA_TYPES = [
|
|
299
|
+
'image/png',
|
|
300
|
+
'image/jpeg',
|
|
301
|
+
'image/gif',
|
|
302
|
+
'image/webp',
|
|
303
|
+
] as const;
|
|
304
|
+
export type SessionImageMediaType = (typeof SESSION_IMAGE_MEDIA_TYPES)[number];
|
|
305
|
+
|
|
306
|
+
/** An image sent with a turn's text, where the transport declares `capabilities.images`. */
|
|
307
|
+
export interface SessionImage {
|
|
308
|
+
readonly mediaType: SessionImageMediaType;
|
|
309
|
+
/** The image bytes, base64 (no `data:` prefix). */
|
|
310
|
+
readonly data: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
269
313
|
export interface SessionSendOptions {
|
|
270
314
|
timeout?: number;
|
|
271
315
|
/** Required ownership registration, not an observer. Rejection prevents the native write. */
|
|
272
316
|
onAdmission?: (attempt: SessionAttempt) => void | Promise<void>;
|
|
317
|
+
/**
|
|
318
|
+
* Images sent with the message, in order, as part of the same user turn. Refused (before
|
|
319
|
+
* anything is written) by a transport without `capabilities.images` and for any other media type.
|
|
320
|
+
*/
|
|
321
|
+
images?: readonly SessionImage[];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Images a send may write: a copy of each, or a refusal naming the first one that is not. */
|
|
325
|
+
export function sendableImages(
|
|
326
|
+
transport: TransportDescriptor | undefined,
|
|
327
|
+
images: readonly SessionImage[] | undefined,
|
|
328
|
+
): SessionImage[] {
|
|
329
|
+
if (!images?.length) return [];
|
|
330
|
+
if (!transport?.capabilities.images)
|
|
331
|
+
throw new Error(`Transport "${transport?.kind ?? 'unknown'}" does not accept image input`);
|
|
332
|
+
return images.map((image, index) => {
|
|
333
|
+
if (!SESSION_IMAGE_MEDIA_TYPES.includes(image?.mediaType))
|
|
334
|
+
throw new Error(`Image ${index + 1}: unsupported media type ${String(image?.mediaType)}`);
|
|
335
|
+
if (typeof image.data !== 'string' || !image.data || /[^A-Za-z0-9+/=]/.test(image.data))
|
|
336
|
+
throw new Error(`Image ${index + 1}: data must be non-empty base64`);
|
|
337
|
+
return { mediaType: image.mediaType, data: image.data };
|
|
338
|
+
});
|
|
273
339
|
}
|
|
274
340
|
|
|
275
341
|
// ---------------------------------------------------------------------------
|
package/src/transport.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface TransportCapabilities {
|
|
|
36
36
|
readonly limits: { readonly stream: boolean; readonly poll: boolean };
|
|
37
37
|
/** A tool-free, text-only session can be enforced (decisions). */
|
|
38
38
|
readonly textOnly: boolean;
|
|
39
|
+
/** send() takes images beside the text (`SessionSendOptions.images`): png, jpeg, gif, webp. */
|
|
40
|
+
readonly images: boolean;
|
|
39
41
|
/** Primed decision host on this transport and how it returns to its primed state each cycle (see primed.ts). */
|
|
40
42
|
readonly primed: 'fork' | 'none';
|
|
41
43
|
readonly billing: 'subscription' | 'api';
|
|
@@ -70,6 +72,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
70
72
|
usage: true,
|
|
71
73
|
limits: { stream: true, poll: true },
|
|
72
74
|
textOnly: true,
|
|
75
|
+
images: true,
|
|
73
76
|
primed: 'fork',
|
|
74
77
|
billing: 'subscription',
|
|
75
78
|
},
|
|
@@ -80,6 +83,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
80
83
|
"interruptNative() and readLimits() use the CLI's stdio control protocol (`interrupt`, `get_usage`).",
|
|
81
84
|
'toolGate: true launches with `--permission-prompt-tool stdio` and registers a PreToolUse callback over the control protocol: onToolRequest() gates see every call (`pre-use`, any mode, bypass included; only a deny counts) and the asks the permission mode raises (`permission`, final).',
|
|
82
85
|
'Rollback (`--resume-session-at`) exists natively but is not wired here.',
|
|
86
|
+
'Images ride in the same stream-json user message as base64 `image` content blocks, before the text.',
|
|
83
87
|
],
|
|
84
88
|
},
|
|
85
89
|
'codex-app-server': {
|
|
@@ -97,6 +101,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
97
101
|
usage: true,
|
|
98
102
|
limits: { stream: true, poll: true },
|
|
99
103
|
textOnly: false,
|
|
104
|
+
images: true,
|
|
100
105
|
primed: 'fork',
|
|
101
106
|
billing: 'subscription',
|
|
102
107
|
},
|
|
@@ -107,6 +112,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
107
112
|
'appServer.approvalPolicy/sandbox (default never / danger-full-access; `codexPermissions(mode)`) set the thread policy; command and file-change approvals for the owned turn go to onToolRequest() gates (`permission`) and are answered accept/decline. Other server requests are refused (-32601).',
|
|
108
113
|
'Primed decisions (CodexPrimedSessions) host every key as a thread of one process per account and fork the primed thread per cycle.',
|
|
109
114
|
'thread/rollback is refused by codex-cli 0.155 for both ephemeral and paginated threads; session fork() is not wired.',
|
|
115
|
+
'Images go to turn/start as `image` inputs with a `data:` URL, after the text.',
|
|
110
116
|
],
|
|
111
117
|
},
|
|
112
118
|
'claude-agent-sdk': {
|
|
@@ -124,6 +130,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
124
130
|
usage: true,
|
|
125
131
|
limits: { stream: true, poll: false },
|
|
126
132
|
textOnly: true,
|
|
133
|
+
images: true,
|
|
127
134
|
primed: 'none',
|
|
128
135
|
billing: 'subscription',
|
|
129
136
|
},
|
|
@@ -147,6 +154,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
147
154
|
usage: false,
|
|
148
155
|
limits: none,
|
|
149
156
|
textOnly: false,
|
|
157
|
+
images: false,
|
|
150
158
|
primed: 'none',
|
|
151
159
|
billing: 'subscription',
|
|
152
160
|
},
|
|
@@ -169,6 +177,7 @@ export const TRANSPORTS: Readonly<Record<TransportKind, TransportDescriptor>> =
|
|
|
169
177
|
usage: false,
|
|
170
178
|
limits: none,
|
|
171
179
|
textOnly: false,
|
|
180
|
+
images: false,
|
|
172
181
|
primed: 'none',
|
|
173
182
|
billing: 'api',
|
|
174
183
|
},
|