@inixiative/agent-session 0.6.6 → 0.6.7

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
@@ -44,6 +44,10 @@ 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).
49
53
  - **Transports with declared capabilities.** How a model is reached is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
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.88",
43
+ "@inixiative/config": "^0.5.105",
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 { 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;
@@ -671,6 +674,7 @@ export class ClaudeCodeSession implements HarnessSession {
671
674
  if (!this._alive) throw new Error('Session ended');
672
675
 
673
676
  const timeout = opts?.timeout ?? this._defaultTimeout;
677
+ const images = sendableImages(this.transport, opts?.images);
674
678
 
675
679
  // Compose pre-send hooks in registration order. Each sees the previous
676
680
  // hook's output. Errors in a hook reject the send() — callers should
@@ -686,6 +690,7 @@ export class ClaudeCodeSession implements HarnessSession {
686
690
  const turn: QueuedTurn = {
687
691
  evidence,
688
692
  message: transformed,
693
+ images,
689
694
  timeout,
690
695
  resolve,
691
696
  reject,
@@ -937,11 +942,18 @@ export class ClaudeCodeSession implements HarnessSession {
937
942
  // Wire format validated empirically against claude 2.1.114:
938
943
  // {type:"user", message:{role,content:[{type:"text",text}]}}
939
944
  // Alternative shapes ({type:"user_message"}, {role,content}) are silently dropped.
945
+ // Images are Messages API base64 image blocks, ahead of the text they accompany.
940
946
  const payload = `${JSON.stringify({
941
947
  type: 'user',
942
948
  message: {
943
949
  role: 'user',
944
- content: [{ type: 'text', text: turn.message }],
950
+ content: [
951
+ ...turn.images.map((image) => ({
952
+ type: 'image',
953
+ source: { type: 'base64', media_type: image.mediaType, data: image.data },
954
+ })),
955
+ { type: 'text', text: turn.message },
956
+ ],
945
957
  },
946
958
  })}\n`;
947
959
  try {
@@ -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 { 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;
@@ -1209,7 +1214,10 @@ export class CodexAppServerSession implements HarnessSession {
1209
1214
  this._rpcNotify('initialized');
1210
1215
  }
1211
1216
 
1212
- private async _sendTurn(message: string): Promise<{ input: number; output: number } | undefined> {
1217
+ private async _sendTurn(
1218
+ message: string,
1219
+ images: readonly SessionImage[],
1220
+ ): Promise<{ input: number; output: number } | undefined> {
1213
1221
  const a = this._inflight!.evidence;
1214
1222
  if (!this._threadReady) {
1215
1223
  const binding = this._externalSessionId;
@@ -1344,7 +1352,13 @@ export class CodexAppServerSession implements HarnessSession {
1344
1352
  });
1345
1353
  await this._rpcRequest('turn/start', {
1346
1354
  threadId: this._externalSessionId,
1347
- input: [{ type: 'text', text: message }],
1355
+ input: [
1356
+ { type: 'text', text: message },
1357
+ ...images.map((image) => ({
1358
+ type: 'image',
1359
+ url: `data:${image.mediaType};base64,${image.data}`,
1360
+ })),
1361
+ ],
1348
1362
  ...(this._effort ? { effort: this._effort } : {}),
1349
1363
  });
1350
1364
  await completed; // Native terminal and RPC acknowledgment are both required.
@@ -266,10 +266,47 @@ export type SessionEventHandler = (event: SessionEvent) => void;
266
266
  */
267
267
  export type BeforeSendHook = (message: string) => string | Promise<string>;
268
268
 
269
+ export const SESSION_IMAGE_MEDIA_TYPES = [
270
+ 'image/png',
271
+ 'image/jpeg',
272
+ 'image/gif',
273
+ 'image/webp',
274
+ ] as const;
275
+ export type SessionImageMediaType = (typeof SESSION_IMAGE_MEDIA_TYPES)[number];
276
+
277
+ /** An image sent with a turn's text, where the transport declares `capabilities.images`. */
278
+ export interface SessionImage {
279
+ readonly mediaType: SessionImageMediaType;
280
+ /** The image bytes, base64 (no `data:` prefix). */
281
+ readonly data: string;
282
+ }
283
+
269
284
  export interface SessionSendOptions {
270
285
  timeout?: number;
271
286
  /** Required ownership registration, not an observer. Rejection prevents the native write. */
272
287
  onAdmission?: (attempt: SessionAttempt) => void | Promise<void>;
288
+ /**
289
+ * Images sent with the message, in order, as part of the same user turn. Refused (before
290
+ * anything is written) by a transport without `capabilities.images` and for any other media type.
291
+ */
292
+ images?: readonly SessionImage[];
293
+ }
294
+
295
+ /** Images a send may write: a copy of each, or a refusal naming the first one that is not. */
296
+ export function sendableImages(
297
+ transport: TransportDescriptor | undefined,
298
+ images: readonly SessionImage[] | undefined,
299
+ ): SessionImage[] {
300
+ if (!images?.length) return [];
301
+ if (!transport?.capabilities.images)
302
+ throw new Error(`Transport "${transport?.kind ?? 'unknown'}" does not accept image input`);
303
+ return images.map((image, index) => {
304
+ if (!SESSION_IMAGE_MEDIA_TYPES.includes(image?.mediaType))
305
+ throw new Error(`Image ${index + 1}: unsupported media type ${String(image?.mediaType)}`);
306
+ if (typeof image.data !== 'string' || !image.data || /[^A-Za-z0-9+/=]/.test(image.data))
307
+ throw new Error(`Image ${index + 1}: data must be non-empty base64`);
308
+ return { mediaType: image.mediaType, data: image.data };
309
+ });
273
310
  }
274
311
 
275
312
  // ---------------------------------------------------------------------------
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
  },