@inixiative/foundry-core 0.5.6 → 0.5.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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@inixiative/foundry-core",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
7
  "exports": {
8
8
  ".": "./src/index.ts",
9
- "./delivery-evidence": "./src/delivery-evidence.ts"
9
+ "./delivery-evidence": "./src/delivery-evidence.ts",
10
+ "./archive": "./src/feedback/archive-turns.ts"
10
11
  },
11
12
  "scripts": {
12
13
  "test": "bun test tests/",
@@ -30,5 +31,16 @@
30
31
  },
31
32
  "engines": {
32
33
  "bun": ">=1.4.2"
34
+ },
35
+ "peerDependencies": {
36
+ "@inixiative/archive": "^0.23.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "@inixiative/archive": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@inixiative/archive": "^0.23.0"
33
45
  }
34
46
  }
@@ -146,6 +146,11 @@ export interface CompositionSnapshot {
146
146
  readonly instructions: readonly OmittedSource[];
147
147
  readonly domainKnowledge: readonly OmittedSource[];
148
148
  };
149
+ /**
150
+ * The charter the call ran under, when it is not the Warden charter (a classify stage's own):
151
+ * its audited subject and text.
152
+ */
153
+ readonly charter?: { readonly subject: string; readonly text: string };
149
154
  }
150
155
 
151
156
  export interface OmittedSource {
package/src/executor.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  standingParts,
10
10
  } from './session-delivery';
11
11
  import type { ToolCallObservation } from './tools';
12
- import type { LLMMessage } from './types';
12
+ import { type ImageInput, type LLMMessage, withoutImageData } from './types';
13
13
 
14
14
  /** Runtime metadata passed to executor handlers at dispatch time. */
15
15
  export interface ExecuteMeta {
@@ -48,6 +48,8 @@ export interface ExecuteMeta {
48
48
  delivery?: SessionDelivery;
49
49
  /** The handler reports what a session provider delivered (CompletionResult.delivery). */
50
50
  recordDelivery?: (report: SessionDeliveryReport) => void;
51
+ /** Images sent with the payload; the handler puts them on the user message it sends. */
52
+ images?: readonly ImageInput[];
51
53
  }
52
54
 
53
55
  export type ExecuteHandler<TPayload, TResult> = (
@@ -228,7 +230,9 @@ export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<T
228
230
  observe(() => meta?.recordNative?.(evidence));
229
231
  }
230
232
  },
231
- recordProviderInput: (messages) => {
233
+ recordProviderInput: (sentMessages) => {
234
+ // Evidence holds each image by identity; its bytes stay with the attachment.
235
+ const messages = withoutImageData(sentMessages);
232
236
  // The initial boundary input must not be replaced by tool followups.
233
237
  if (providerMessages === undefined) {
234
238
  providerMessages = structuredClone(messages);
@@ -0,0 +1,52 @@
1
+ import type { ArchiveSnapshot } from '@inixiative/archive';
2
+ import { typedPrompt } from '@inixiative/archive/import';
3
+ import type { TranscriptTurn } from './correction';
4
+
5
+ // An Archive session as transcript turns, the input `extractCorrections` reads. Shared by Foundry's
6
+ // `foundry repeats` and Oracle's session miner; its own export, so the core root never loads Archive.
7
+
8
+ const INTERRUPTED = /^\s*\[Request interrupted by user/;
9
+ const COMMIT_BRACKET = /\[[^[\]\n]+\s([0-9a-f]{7,40})\]/g;
10
+ const COMMIT_LINE = /^commit ([0-9a-f]{40})/gm;
11
+
12
+ /** Commit SHAs a tool result printed: `git commit`'s `[branch sha]` line, `git log`'s `commit <sha>`. */
13
+ export const commitsIn = (text: string) => [
14
+ ...[...text.matchAll(COMMIT_BRACKET)].map((m) => m[1]!),
15
+ ...[...text.matchAll(COMMIT_LINE)].map((m) => m[1]!),
16
+ ];
17
+
18
+ export interface SnapshotTurnsOptions {
19
+ /** Also a turn per tool result that printed commits (`commits`): where the work landed. */
20
+ readonly commits?: boolean;
21
+ }
22
+
23
+ /** The transcript as turns: what the person typed, the agent's prose, and interruptions. */
24
+ export function snapshotTurns(
25
+ snapshot: Pick<ArchiveSnapshot, 'entries'>,
26
+ options: SnapshotTurnsOptions = {},
27
+ ): TranscriptTurn[] {
28
+ const turns: TranscriptTurn[] = [];
29
+ for (const entry of snapshot.entries) {
30
+ const timestamp = entry.timestamp === null ? '' : new Date(entry.timestamp).toISOString();
31
+ if (entry.kind === 'assistant') turns.push({ role: 'assistant', text: entry.text, timestamp });
32
+ else if ((entry.kind === 'user' || entry.kind === 'event') && INTERRUPTED.test(entry.text))
33
+ turns.push({ role: 'user', text: '', timestamp, interrupted: true });
34
+ else if (entry.kind === 'user') {
35
+ const text = typedPrompt(entry.text);
36
+ if (text)
37
+ turns.push({ role: 'user', id: entry.id, text, timestamp, sourceRef: entry.sourceRef });
38
+ } else if (options.commits && entry.kind === 'tool-result') {
39
+ const commits = commitsIn(entry.text);
40
+ if (commits.length)
41
+ turns.push({
42
+ role: 'assistant',
43
+ id: entry.id,
44
+ text: '',
45
+ timestamp,
46
+ sourceRef: entry.sourceRef,
47
+ commits,
48
+ });
49
+ }
50
+ }
51
+ return turns;
52
+ }
@@ -20,6 +20,8 @@ export interface TranscriptTurn {
20
20
  interrupted?: boolean;
21
21
  /** Where the turn sits in its source, e.g. `line:42`. */
22
22
  sourceRef?: string;
23
+ /** Commit SHAs a tool result printed in this turn (`snapshotTurns` with `commits`). */
24
+ commits?: string[];
23
25
  }
24
26
 
25
27
  export interface CorrectionEvent {
@@ -14,7 +14,12 @@ export interface WardenLabel {
14
14
  readonly shouldKnow?: string;
15
15
  }
16
16
 
17
- export type WardenTriggerKind = 'user-message' | 'agent-action' | 'agent-plan' | 'subagent-spawn';
17
+ export type WardenTriggerKind =
18
+ | 'user-message'
19
+ | 'consumed'
20
+ | 'agent-action'
21
+ | 'agent-plan'
22
+ | 'subagent-spawn';
18
23
 
19
24
  /**
20
25
  * A labelled moment for the Warden prefilter: which Warden domains the trigger touches. Never commit
@@ -173,6 +173,7 @@ export function prefilterQuestion(description: string) {
173
173
  export function prefilterState(trigger: PrefilterTrigger, text: string, context?: string): string {
174
174
  const label = {
175
175
  'user-message': "The user's message",
176
+ consumed: 'A work item the agent took in (a ticket, spec, issue or page)',
176
177
  'agent-action': "The agent's actions this turn",
177
178
  'agent-plan': "The agent's plan",
178
179
  'subagent-spawn': 'The task the agent is handing to a subagent',
@@ -66,6 +66,55 @@ export function probeTriggers(probe: WardenProbe): WardenProbeTrigger[] {
66
66
  ];
67
67
  }
68
68
 
69
+ /** A classification's answer: in the region (`yes`), possibly (`unsure`, ternary stages only), or not. */
70
+ export type ClassifyOutcome = 'yes' | 'unsure' | 'no';
71
+
72
+ /**
73
+ * One stage of a classification run (Foundry's `classify`), as answered: what it read, what it said
74
+ * and what it cost. A stage that failed or answered out of shape carries `error` and no outcome:
75
+ * counted, never guessed.
76
+ */
77
+ export interface ClassifyStageTrace {
78
+ /** 0-based, in run order. */
79
+ readonly stage: number;
80
+ readonly task: 'recognise' | 'point';
81
+ /** The region doc it read (e.g. `knowledge.md`) and the content hash of what it read. */
82
+ readonly doc: string;
83
+ readonly docHash?: string;
84
+ /**
85
+ * sha256 of the full system message the stage sent (its charter above the Warden's instructions),
86
+ * so runs that change any prompt section, the region line included, are told apart per call.
87
+ */
88
+ readonly systemHash?: string;
89
+ /** The model and effort that answered, as the provider reported them. */
90
+ readonly model?: string;
91
+ readonly effort?: string;
92
+ readonly outcome?: ClassifyOutcome;
93
+ /** The why, when the stage was asked for one (`reason: inline | followup`). */
94
+ readonly why?: string;
95
+ /** `reason: followup`: whether the stage, asked why, kept its answer or names the one it now holds. */
96
+ readonly revised?: 'keep' | ClassifyOutcome;
97
+ /** `reason: followup`: the follow-up call failed (the stage's own answer stands). */
98
+ readonly followupError?: string;
99
+ /** Its remarks in order (advice snippets or guard finding descriptions). */
100
+ readonly remarks?: readonly string[];
101
+ /** A recognise stage: the regions it named, with their line and outcome. */
102
+ readonly regions?: readonly {
103
+ readonly regionId: string;
104
+ readonly why: string;
105
+ readonly outcome: ClassifyOutcome;
106
+ }[];
107
+ readonly tokens?: Omit<TokenCounts, 'providerUsage'>;
108
+ readonly elapsedMs?: number;
109
+ /** The answer was out of protocol (counted apart from failures of the call). */
110
+ readonly malformed?: true;
111
+ readonly error?: string;
112
+ /** No answer within the stage's share of the trigger's deadline: the run kept the previous stage's outcome. */
113
+ readonly timedOut?: true;
114
+ /** Region ids a recogniser named that it does not hold: counted, never followed. */
115
+ readonly unknownRegions?: readonly string[];
116
+ }
117
+
69
118
  /** One Warden's answer on one probe, and the judge's verdict when it spoke. */
70
119
  export interface WardenProbeAnswer {
71
120
  readonly probeId: string;
@@ -79,9 +128,15 @@ export interface WardenProbeAnswer {
79
128
  readonly elapsedMs?: number;
80
129
  /** What the call used, as the provider reported it (cache reads show a warm, primed prefix). */
81
130
  readonly tokens?: Omit<TokenCounts, 'providerUsage'>;
131
+ /** An unstaged call: sha256 of its full system message (a staged row has it on each stage's trace). */
132
+ readonly systemHash?: string;
82
133
  /** Transport failures of earlier attempts at this call, re-asked before this answer. */
83
134
  readonly retried?: readonly string[];
84
135
  readonly judged?: WardenJudgement;
136
+ /** The run's final outcome; a final `unsure` is delivered as nothing (`noComment`). */
137
+ readonly outcome?: ClassifyOutcome;
138
+ /** Every stage the run asked, in order. */
139
+ readonly stages?: readonly ClassifyStageTrace[];
85
140
  }
86
141
 
87
142
  /** One probe run as a turn: every Warden asked at once, as the live chorus is. */
@@ -144,6 +199,8 @@ export interface CharterScore {
144
199
  >;
145
200
  readonly remarks: number;
146
201
  readonly silences: number;
202
+ /** Silences whose final outcome was `unsure` (a ternary last stage): delivered as nothing, counted apart. */
203
+ readonly unsure: number;
147
204
  /** Remarks the judge could not grade: counted as unmatched, reported apart. */
148
205
  readonly unjudged: number;
149
206
  /** Per-turn load at roster scale: calls and wall clock, when the run recorded turns. */
@@ -192,6 +249,7 @@ export function scoreCharterRun(
192
249
  remarksRight = 0,
193
250
  scope = 0,
194
251
  silences = 0,
252
+ unsure = 0,
195
253
  unjudged = 0;
196
254
  const recalled = new Set<string>();
197
255
  const anticipated = new Set<string>();
@@ -220,8 +278,10 @@ export function scoreCharterRun(
220
278
  };
221
279
  perTrigger.set(trigger, tally);
222
280
  tally.probes.add(probe.id);
223
- if (a.noComment) silences++;
224
- else {
281
+ if (a.noComment) {
282
+ silences++;
283
+ if (a.outcome === 'unsure') unsure++;
284
+ } else {
225
285
  remarks++;
226
286
  tally.remarks++;
227
287
  if (!a.judged) unjudged++;
@@ -284,6 +344,7 @@ export function scoreCharterRun(
284
344
  byTrigger,
285
345
  remarks,
286
346
  silences,
347
+ unsure,
287
348
  unjudged,
288
349
  ...(turns.length
289
350
  ? {
package/src/harness.ts CHANGED
@@ -144,6 +144,8 @@ interface SendOptions {
144
144
  onExecutorInput?: () => void;
145
145
  /** The payload the execute stage's agent receives instead of `message.payload` (see DispatchOptions.agentPayload). */
146
146
  executorPayload?: unknown;
147
+ /** Images sent with the message; only the execute stage's agent receives them. */
148
+ images?: readonly import('./types').ImageInput[];
147
149
  }
148
150
 
149
151
  /**
@@ -670,6 +672,7 @@ export class Harness {
670
672
  ...(stage.role === 'execute' && opts?.executorPayload !== undefined
671
673
  ? { agentPayload: opts.executorPayload }
672
674
  : {}),
675
+ ...(stage.role === 'execute' && opts?.images?.length ? { images: opts.images } : {}),
673
676
  recordInjection: (artifact) => {
674
677
  stageSpan.annotations.injection = artifact;
675
678
  if (stage.role === 'execute' && !executorInput) {
package/src/index.ts CHANGED
@@ -251,6 +251,8 @@ export {
251
251
  export {
252
252
  type Authorship,
253
253
  type CharterScore,
254
+ type ClassifyOutcome,
255
+ type ClassifyStageTrace,
254
256
  CONCEPT_CLUSTERS,
255
257
  CORRECTION_TARGET_PROMPT,
256
258
  type CorrectionTarget,
@@ -346,16 +348,19 @@ export {
346
348
  MODEL_CAPABILITIES,
347
349
  MODEL_REGISTRY,
348
350
  MODEL_REGISTRY_UPDATED_AT,
351
+ MODEL_VENDORS,
349
352
  type ModelCapability,
350
353
  type ModelLab,
351
354
  type ModelReasoning,
352
355
  type ModelSweepOption,
353
356
  type ModelTier,
357
+ type ModelVendor,
354
358
  mainThreadLab,
355
359
  modelCapabilities,
356
360
  modelHasCapability,
357
361
  modelOptionsByCapability,
358
362
  type ProviderCredential,
363
+ type ProviderMode,
359
364
  type ProviderType,
360
365
  providersWithCapability,
361
366
  type ReasoningEffort,
@@ -363,6 +368,8 @@ export {
363
368
  registryModel,
364
369
  resolveDecisionModel,
365
370
  SUBSCRIPTION_DECISION_MODELS,
371
+ vendorModeProvider,
372
+ vendorOfProvider,
366
373
  } from './model-registry';
367
374
  export * from './native-evidence';
368
375
  // Retry middleware
@@ -464,6 +471,8 @@ export type {
464
471
  CompletionResult,
465
472
  EmbeddingProvider,
466
473
  EmbeddingResult,
474
+ ImageInput,
475
+ ImageMediaType,
467
476
  InvocationCondition,
468
477
  LayerModeConfig,
469
478
  LLMMessage,
@@ -473,3 +482,4 @@ export type {
473
482
  ToolCallResult,
474
483
  ToolDefinition,
475
484
  } from './types';
485
+ export { IMAGE_MEDIA_TYPES, withoutImageData } from './types';
package/src/messages.ts CHANGED
@@ -61,6 +61,8 @@ export type ParticipantRequest =
61
61
  readonly served?: ServedModel;
62
62
  /** What the call's instructions and domain knowledge were composed of; the journal stores version ids for it. */
63
63
  readonly composition?: CompositionSnapshot;
64
+ /** A staged classification (Foundry's classify): which stage, 0-based, this call was. */
65
+ readonly stage?: number;
64
66
  }
65
67
  | { readonly status: 'not-sent'; readonly phase: string; readonly reason: string }
66
68
  /** A call may have been made, but its input was never observed (e.g. the caller threw first). Not "not-sent". */
@@ -234,7 +234,7 @@ export const DECISION_MODEL = 'claude-haiku-5-5';
234
234
  /** Subscription decision default per native runtime; settings choose the runtime. */
235
235
  export const SUBSCRIPTION_DECISION_MODELS = {
236
236
  claude: DECISION_MODEL,
237
- codex: 'gpt-6-luna',
237
+ codex: 'gpt-5.6-luna',
238
238
  } as const;
239
239
 
240
240
  /** Tag rule: every model judges. Cheap models review and route; capable models execute and advise. */
@@ -490,6 +490,10 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
490
490
  // Slugs, effort ladders and the 272k session window come from the local
491
491
  // Codex catalogue (~/.codex/models_cache.json); the same models carry a
492
492
  // 1,050,000 window on the platform API, which the openai entry records.
493
+ // Only models a ChatGPT login serves are listed: Codex refuses gpt-5.5 on a
494
+ // ChatGPT account ("not supported when using Codex with a ChatGPT account"). The
495
+ // GPT-6 models need a current Codex: 0.153.4 is refused them, 0.163.0 is served
496
+ // them (2026-10-16); the GPT-5.6 models run on both.
493
497
  {
494
498
  id: 'gpt-6-astra',
495
499
  label: 'GPT-6 Astra',
@@ -521,7 +525,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
521
525
  runtimeKind: 'native-harness',
522
526
  capabilities: FAST_REASONING,
523
527
  reasoning: CODEX_REASONING,
524
- notes: "Foundry's decision model.",
528
+ notes: 'Needs a current Codex CLI on a ChatGPT login (0.153.4 is refused it).',
525
529
  },
526
530
  {
527
531
  id: 'gpt-5.6-sol',
@@ -552,20 +556,8 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
552
556
  runtimeKind: 'native-harness',
553
557
  capabilities: FAST_REASONING,
554
558
  reasoning: CODEX_REASONING,
555
- },
556
- {
557
- id: 'gpt-5.5',
558
- label: 'GPT-5.5',
559
- tier: 'standard',
560
- costTier: 'medium',
561
- contextWindow: 272_000,
562
- runtimeKind: 'native-harness',
563
- capabilities: STANDARD_REASONING,
564
- reasoning: {
565
- ...CODEX_REASONING,
566
- efforts: ['low', 'medium', 'high', 'xhigh'],
567
- fallback: 'xhigh',
568
- },
559
+ notes:
560
+ "Foundry's Codex decision model: fast, and served to a ChatGPT login on every Codex CLI.",
569
561
  },
570
562
  ],
571
563
  },
@@ -1631,6 +1623,95 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
1631
1623
  },
1632
1624
  };
1633
1625
 
1626
+ /**
1627
+ * A vendor, the way a person thinks of who serves their models: one row for Anthropic, not one for
1628
+ * the Claude Code subscription and another for the API key. A vendor reaches its models in up to two
1629
+ * modes, and a person may have either or both:
1630
+ *
1631
+ * - subscription: the person's own subscription login, through a native CLI transport (Claude Code,
1632
+ * Codex). `comingSoon` marks a vendor that sells a subscription Foundry has no transport for yet.
1633
+ * - api: an API key (or, for a local server, none), through the registry provider named here.
1634
+ *
1635
+ * Each mode names the registry provider that serves it, so the provider table stays the one source
1636
+ * of models, transports and credentials; the vendor only groups them.
1637
+ */
1638
+ export type ProviderMode = 'subscription' | 'api';
1639
+ export interface ModelVendor {
1640
+ id: string;
1641
+ label: string;
1642
+ /** Absent for gateways and local hosts. */
1643
+ lab?: ModelLab;
1644
+ subscription?: { provider: string } | { comingSoon: true };
1645
+ api?: { provider: string };
1646
+ }
1647
+
1648
+ /** The five main-thread labs first, in this order, then every other vendor alphabetically. */
1649
+ export const MODEL_VENDORS: readonly ModelVendor[] = [
1650
+ {
1651
+ id: 'anthropic',
1652
+ label: 'Anthropic (Claude)',
1653
+ lab: 'anthropic',
1654
+ subscription: { provider: 'claude-code' },
1655
+ api: { provider: 'anthropic' },
1656
+ },
1657
+ {
1658
+ id: 'openai',
1659
+ label: 'OpenAI (Codex, GPT)',
1660
+ lab: 'openai',
1661
+ subscription: { provider: 'codex' },
1662
+ api: { provider: 'openai' },
1663
+ },
1664
+ {
1665
+ id: 'google',
1666
+ label: 'Google (Gemini)',
1667
+ lab: 'google',
1668
+ subscription: { comingSoon: true },
1669
+ api: { provider: 'gemini' },
1670
+ },
1671
+ {
1672
+ id: 'meta',
1673
+ label: 'Meta (Muse)',
1674
+ lab: 'meta',
1675
+ subscription: { comingSoon: true },
1676
+ api: { provider: 'meta' },
1677
+ },
1678
+ {
1679
+ id: 'xai',
1680
+ label: 'xAI (Grok)',
1681
+ lab: 'xai',
1682
+ subscription: { comingSoon: true },
1683
+ api: { provider: 'xai' },
1684
+ },
1685
+ { id: 'alibaba', label: 'Alibaba (Qwen)', lab: 'alibaba', api: { provider: 'qwen' } },
1686
+ { id: 'deepseek', label: 'DeepSeek', lab: 'deepseek', api: { provider: 'deepseek' } },
1687
+ { id: 'fireworks', label: 'Fireworks AI', api: { provider: 'fireworks' } },
1688
+ { id: 'groq', label: 'Groq', api: { provider: 'groq' } },
1689
+ { id: 'mistral', label: 'Mistral AI', lab: 'mistral', api: { provider: 'mistral' } },
1690
+ { id: 'moonshot', label: 'Moonshot AI (Kimi)', lab: 'moonshot', api: { provider: 'kimi' } },
1691
+ { id: 'ollama', label: 'Ollama (local)', api: { provider: 'ollama' } },
1692
+ { id: 'openrouter', label: 'OpenRouter', api: { provider: 'openrouter' } },
1693
+ { id: 'together', label: 'Together AI', api: { provider: 'together' } },
1694
+ { id: 'typesafe', label: 'TypeSafe (Jev)', lab: 'typesafe', api: { provider: 'typesafe' } },
1695
+ { id: 'vllm', label: 'vLLM (local)', api: { provider: 'vllm' } },
1696
+ { id: 'zhipu', label: 'Zhipu (GLM)', lab: 'zhipu', api: { provider: 'glm' } },
1697
+ ];
1698
+
1699
+ /** The registry provider serving a vendor's mode, or undefined when the mode has no transport. */
1700
+ export function vendorModeProvider(vendor: ModelVendor, mode: ProviderMode): string | undefined {
1701
+ const entry = vendor[mode];
1702
+ return entry && 'provider' in entry ? entry.provider : undefined;
1703
+ }
1704
+
1705
+ /** The vendor and mode a registry provider serves. Every registered provider serves exactly one. */
1706
+ export function vendorOfProvider(
1707
+ providerId: string,
1708
+ ): { vendor: ModelVendor; mode: ProviderMode } | undefined {
1709
+ for (const vendor of MODEL_VENDORS)
1710
+ for (const mode of ['subscription', 'api'] as const)
1711
+ if (vendorModeProvider(vendor, mode) === providerId) return { vendor, mode };
1712
+ return undefined;
1713
+ }
1714
+
1634
1715
  /** Every registered model carrying all of the requested capabilities. */
1635
1716
  export function modelOptionsByCapability(...capabilities: ModelCapability[]): ModelSweepOption[] {
1636
1717
  return Object.values(MODEL_REGISTRY).flatMap((provider) =>
package/src/thread.ts CHANGED
@@ -90,7 +90,7 @@ export interface ThreadMeta {
90
90
  /** The parent's turn that spawned this thread (a subagent). */
91
91
  parentTurnId?: string;
92
92
 
93
- /** How this subagent thread was spawned (docs/reference/SUBAGENTS.md §4.1). */
93
+ /** How this subagent thread was spawned. */
94
94
  spawn?: ThreadSpawn;
95
95
 
96
96
  /** 1 for a child of a main thread. */
@@ -170,6 +170,8 @@ export interface DispatchOptions {
170
170
  * everything that learns from the message sees only the user's words.
171
171
  */
172
172
  agentPayload?: unknown;
173
+ /** Images sent with the payload, for the agent alone (`ExecuteMeta.images`). */
174
+ images?: readonly import('./types').ImageInput[];
173
175
  /** Lets this dispatch's middleware include an on-demand layer in its assembly (`RequestContext.requestLayer`). */
174
176
  requestLayer?: (layerId: string) => void;
175
177
  }
@@ -437,6 +439,7 @@ export class Thread {
437
439
  },
438
440
  annotations: ctx.annotations,
439
441
  onDelta: opts?.onDelta,
442
+ ...(opts?.images?.length ? { images: opts.images } : {}),
440
443
  recordInjection: opts?.recordInjection,
441
444
  recordNative: opts?.recordNative,
442
445
  recordCompleted: opts?.recordCompleted,
package/src/types.ts CHANGED
@@ -38,9 +38,34 @@ export interface LayerModeConfig {
38
38
  // LLM provider interface — the abstraction, not implementations
39
39
  // ---------------------------------------------------------------------------
40
40
 
41
+ export const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'] as const;
42
+ export type ImageMediaType = (typeof IMAGE_MEDIA_TYPES)[number];
43
+
44
+ /** An image sent with a user message. */
45
+ export interface ImageInput {
46
+ /** Identity of the stored image; what evidence records in place of the bytes. */
47
+ readonly id: string;
48
+ readonly mediaType: ImageMediaType;
49
+ /** Size of the image in bytes. */
50
+ readonly bytes: number;
51
+ /** The image, base64. Present at the provider boundary, dropped from recorded evidence. */
52
+ readonly data?: string;
53
+ }
54
+
41
55
  export interface LLMMessage {
42
56
  readonly role: 'system' | 'user' | 'assistant';
43
57
  readonly content: string;
58
+ /** Images sent with this (user) message, for a provider that declares `imageInput`. */
59
+ readonly images?: readonly ImageInput[];
60
+ }
61
+
62
+ /** Messages as evidence records them: each image by identity, without its bytes. */
63
+ export function withoutImageData(messages: readonly LLMMessage[]): LLMMessage[] {
64
+ return messages.map((message) =>
65
+ message.images
66
+ ? { ...message, images: message.images.map(({ data: _data, ...image }) => image) }
67
+ : message,
68
+ );
44
69
  }
45
70
 
46
71
  /** Tool definition sent to the LLM so it can call tools. */
@@ -155,6 +180,8 @@ export interface LLMStreamEvent {
155
180
 
156
181
  export interface LLMProvider {
157
182
  readonly id: string;
183
+ /** Accepts `LLMMessage.images`. A provider without it refuses a message that carries images. */
184
+ readonly imageInput?: boolean;
158
185
  /** Native callback support, distinct from stateless/legacy serializable options. */
159
186
  readonly nativeOwnership?: 'required-prewrite';
160
187
  /** Optional owned-call evidence. A rejected waiter alone never proves remote settlement. */