@inixiative/foundry-core 0.5.5 → 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 +14 -2
- package/src/context-layer.ts +5 -0
- package/src/executor.ts +6 -2
- package/src/feedback/archive-turns.ts +52 -0
- package/src/feedback/correction.ts +2 -0
- package/src/feedback/warden-labels.ts +6 -1
- package/src/feedback/warden-prefilter.ts +1 -0
- package/src/feedback/warden-probes.ts +63 -2
- package/src/harness.ts +3 -0
- package/src/index.ts +16 -0
- package/src/messages.ts +2 -0
- package/src/model-registry.ts +97 -16
- package/src/thread.ts +4 -1
- package/src/token-tracker.ts +154 -3
- package/src/types.ts +27 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/foundry-core",
|
|
3
|
-
"version": "0.5.
|
|
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
|
}
|
package/src/context-layer.ts
CHANGED
|
@@ -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
|
|
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: (
|
|
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 =
|
|
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)
|
|
224
|
-
|
|
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
|
|
@@ -410,9 +417,15 @@ export {
|
|
|
410
417
|
type ModelPricing,
|
|
411
418
|
TokenTracker,
|
|
412
419
|
type TokenUsage,
|
|
420
|
+
type UsageAttribution,
|
|
413
421
|
type UsageBreakdown,
|
|
414
422
|
type UsageEntry,
|
|
423
|
+
type UsageFilter,
|
|
424
|
+
type UsageRecord,
|
|
415
425
|
type UsageSummary,
|
|
426
|
+
type UsageWindow,
|
|
427
|
+
type WindowReading,
|
|
428
|
+
windowDelta,
|
|
416
429
|
} from './token-tracker';
|
|
417
430
|
// Tools — typed execution interfaces
|
|
418
431
|
export {
|
|
@@ -458,6 +471,8 @@ export type {
|
|
|
458
471
|
CompletionResult,
|
|
459
472
|
EmbeddingProvider,
|
|
460
473
|
EmbeddingResult,
|
|
474
|
+
ImageInput,
|
|
475
|
+
ImageMediaType,
|
|
461
476
|
InvocationCondition,
|
|
462
477
|
LayerModeConfig,
|
|
463
478
|
LLMMessage,
|
|
@@ -467,3 +482,4 @@ export type {
|
|
|
467
482
|
ToolCallResult,
|
|
468
483
|
ToolDefinition,
|
|
469
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". */
|
package/src/model-registry.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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/token-tracker.ts
CHANGED
|
@@ -15,15 +15,28 @@ export interface TokenUsage extends TokenCounts {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* What a usage entry or window reading is for, beyond the built-in dimensions: e.g. a replay's
|
|
19
|
+
* `fixtureId`, `arm`, `runId` and `role`. Filters match on these keys exactly.
|
|
20
|
+
*/
|
|
21
|
+
export type UsageAttribution = Readonly<Record<string, string>>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A single recorded usage entry — one LLM call (for a native session, one turn).
|
|
19
25
|
*/
|
|
20
26
|
export interface UsageEntry {
|
|
21
27
|
readonly timestamp: number;
|
|
22
28
|
readonly provider: string;
|
|
23
29
|
readonly model: string;
|
|
30
|
+
readonly effort?: string;
|
|
24
31
|
readonly agentId?: string;
|
|
25
32
|
readonly threadId?: string;
|
|
26
33
|
readonly spanId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The subscription login the call ran on, by a non-secret id: a Kingdom Integration id, or a
|
|
36
|
+
* local label (`local`, `setup-token:<sha256 prefix>`). Never the credential itself.
|
|
37
|
+
*/
|
|
38
|
+
readonly integrationId?: string;
|
|
39
|
+
readonly attribution?: UsageAttribution;
|
|
27
40
|
readonly tokens: Readonly<TokenCounts>;
|
|
28
41
|
readonly cost: number;
|
|
29
42
|
/** False means cost is an unpriced subtotal, not a zero-price acknowledgment. */
|
|
@@ -31,6 +44,96 @@ export interface UsageEntry {
|
|
|
31
44
|
readonly cached?: boolean;
|
|
32
45
|
}
|
|
33
46
|
|
|
47
|
+
/**
|
|
48
|
+
* One reading of a subscription login's usage windows (percent used per window id, e.g.
|
|
49
|
+
* `five_hour`), taken over the provider's control channel, not a model call. A window covers the
|
|
50
|
+
* whole login, so its growth between two readings is an upper bound on what the calls between them
|
|
51
|
+
* spent.
|
|
52
|
+
*/
|
|
53
|
+
/** One usage window in a reading. */
|
|
54
|
+
export interface UsageWindow {
|
|
55
|
+
/** 0–100. */
|
|
56
|
+
readonly usedPercent: number;
|
|
57
|
+
/** When the window resets (epoch ms); a different value in a later reading means it reset. */
|
|
58
|
+
readonly resetsAt?: number;
|
|
59
|
+
readonly windowMinutes?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WindowReading {
|
|
63
|
+
/** When the provider observed it (epoch ms). */
|
|
64
|
+
readonly timestamp: number;
|
|
65
|
+
readonly provider: string;
|
|
66
|
+
readonly integrationId?: string;
|
|
67
|
+
/** Where in the work it was taken, e.g. `before`, `after`, `gate`. */
|
|
68
|
+
readonly phase: string;
|
|
69
|
+
/** How it was read, e.g. `session` (the work's own session) or `probe` (a separate one). */
|
|
70
|
+
readonly source: string;
|
|
71
|
+
readonly windows: Readonly<Record<string, UsageWindow>>;
|
|
72
|
+
/** The provider reports ordinary usage blocked. Absent means not reported. */
|
|
73
|
+
readonly blocked?: boolean;
|
|
74
|
+
/** The provider's reason label when a limit is reached. */
|
|
75
|
+
readonly reachedType?: string;
|
|
76
|
+
readonly plan?: string;
|
|
77
|
+
readonly attribution?: UsageAttribution;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Selects entries and readings: every given field must match, and every given attribution key.
|
|
82
|
+
* Window readings carry no model, effort, agent, thread or span, so only `provider`,
|
|
83
|
+
* `integrationId` and `attribution` select them.
|
|
84
|
+
*/
|
|
85
|
+
export interface UsageFilter {
|
|
86
|
+
readonly provider?: string;
|
|
87
|
+
readonly model?: string;
|
|
88
|
+
readonly effort?: string;
|
|
89
|
+
readonly agentId?: string;
|
|
90
|
+
readonly threadId?: string;
|
|
91
|
+
readonly spanId?: string;
|
|
92
|
+
readonly integrationId?: string;
|
|
93
|
+
readonly attribution?: UsageAttribution;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The usage a filter selects: its calls, their totals and the window readings, as recorded. */
|
|
97
|
+
export interface UsageRecord {
|
|
98
|
+
readonly calls: number;
|
|
99
|
+
readonly totals: TokenUsage;
|
|
100
|
+
readonly entries: ReadonlyArray<UsageEntry>;
|
|
101
|
+
readonly windowReadings: ReadonlyArray<WindowReading>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Two readings of one reset time this far apart are different windows (reset times jitter). */
|
|
105
|
+
const RESET_TOLERANCE_MS = 60_000;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Growth per window id present in both readings, in percentage points (2 decimals). A window
|
|
109
|
+
* whose reset time moved between the readings reset in between: its growth is unknown, so it is
|
|
110
|
+
* listed under `reset` instead of `delta`.
|
|
111
|
+
*/
|
|
112
|
+
export function windowDelta(
|
|
113
|
+
before: WindowReading['windows'],
|
|
114
|
+
after: WindowReading['windows'],
|
|
115
|
+
): { delta: Record<string, number>; reset: string[] } {
|
|
116
|
+
const delta: Record<string, number> = {};
|
|
117
|
+
const reset: string[] = [];
|
|
118
|
+
for (const [id, a] of Object.entries(after)) {
|
|
119
|
+
const b = before[id];
|
|
120
|
+
if (!b) continue;
|
|
121
|
+
if (
|
|
122
|
+
b.resetsAt !== undefined &&
|
|
123
|
+
a.resetsAt !== undefined &&
|
|
124
|
+
Math.abs(a.resetsAt - b.resetsAt) > RESET_TOLERANCE_MS
|
|
125
|
+
)
|
|
126
|
+
reset.push(id);
|
|
127
|
+
else delta[id] = Number((a.usedPercent - b.usedPercent).toFixed(2));
|
|
128
|
+
}
|
|
129
|
+
return { delta, reset };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const attributionMatches = (
|
|
133
|
+
attribution: UsageAttribution | undefined,
|
|
134
|
+
want: UsageAttribution | undefined,
|
|
135
|
+
): boolean => !want || Object.entries(want).every(([key, value]) => attribution?.[key] === value);
|
|
136
|
+
|
|
34
137
|
/**
|
|
35
138
|
* Per-model pricing: cost in dollars per 1 million tokens.
|
|
36
139
|
*/
|
|
@@ -188,6 +291,7 @@ type EventHandler = (status: BudgetStatus) => void;
|
|
|
188
291
|
|
|
189
292
|
export class TokenTracker {
|
|
190
293
|
private readonly _entries: UsageEntry[] = [];
|
|
294
|
+
private readonly _windowReadings: WindowReading[] = [];
|
|
191
295
|
private readonly _costTable: CostTable;
|
|
192
296
|
private readonly _budget: BudgetConfig;
|
|
193
297
|
private readonly _warningHandlers: EventHandler[] = [];
|
|
@@ -232,10 +336,56 @@ export class TokenTracker {
|
|
|
232
336
|
return full;
|
|
233
337
|
}
|
|
234
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Record a reading of a login's usage windows. It spends nothing, so no budget is checked.
|
|
341
|
+
* `timestamp` defaults to now.
|
|
342
|
+
*/
|
|
343
|
+
recordWindows(reading: Omit<WindowReading, 'timestamp'> & { timestamp?: number }): WindowReading {
|
|
344
|
+
const full: WindowReading = { ...reading, timestamp: reading.timestamp ?? Date.now() };
|
|
345
|
+
this._windowReadings.push(full);
|
|
346
|
+
return full;
|
|
347
|
+
}
|
|
348
|
+
|
|
235
349
|
// -----------------------------------------------------------------------
|
|
236
350
|
// Queries
|
|
237
351
|
// -----------------------------------------------------------------------
|
|
238
352
|
|
|
353
|
+
/** Every entry the filter selects, in recording order. */
|
|
354
|
+
entries(filter: UsageFilter = {}): ReadonlyArray<UsageEntry> {
|
|
355
|
+
return this._entries.filter(
|
|
356
|
+
(e) =>
|
|
357
|
+
(filter.provider === undefined || e.provider === filter.provider) &&
|
|
358
|
+
(filter.model === undefined || e.model === filter.model) &&
|
|
359
|
+
(filter.effort === undefined || e.effort === filter.effort) &&
|
|
360
|
+
(filter.agentId === undefined || e.agentId === filter.agentId) &&
|
|
361
|
+
(filter.threadId === undefined || e.threadId === filter.threadId) &&
|
|
362
|
+
(filter.spanId === undefined || e.spanId === filter.spanId) &&
|
|
363
|
+
(filter.integrationId === undefined || e.integrationId === filter.integrationId) &&
|
|
364
|
+
attributionMatches(e.attribution, filter.attribution),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Every window reading the filter selects (by provider, integrationId and attribution). */
|
|
369
|
+
windowReadings(filter: UsageFilter = {}): ReadonlyArray<WindowReading> {
|
|
370
|
+
return this._windowReadings.filter(
|
|
371
|
+
(r) =>
|
|
372
|
+
(filter.provider === undefined || r.provider === filter.provider) &&
|
|
373
|
+
(filter.integrationId === undefined || r.integrationId === filter.integrationId) &&
|
|
374
|
+
attributionMatches(r.attribution, filter.attribution),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The calls, totals and window readings a filter selects, as one record. */
|
|
379
|
+
usageRecord(filter: UsageFilter = {}): UsageRecord {
|
|
380
|
+
const entries = this.entries(filter);
|
|
381
|
+
return {
|
|
382
|
+
calls: entries.length,
|
|
383
|
+
totals: this._aggregate(entries),
|
|
384
|
+
entries,
|
|
385
|
+
windowReadings: this.windowReadings(filter),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
239
389
|
get totalTokens(): TokenUsage {
|
|
240
390
|
return this._aggregate(this._entries);
|
|
241
391
|
}
|
|
@@ -291,9 +441,10 @@ export class TokenTracker {
|
|
|
291
441
|
};
|
|
292
442
|
}
|
|
293
443
|
|
|
294
|
-
/** Clear all recorded entries and reset warning state. */
|
|
444
|
+
/** Clear all recorded entries and window readings, and reset warning state. */
|
|
295
445
|
reset(): void {
|
|
296
446
|
this._entries.length = 0;
|
|
447
|
+
this._windowReadings.length = 0;
|
|
297
448
|
this._warningEmitted = false;
|
|
298
449
|
}
|
|
299
450
|
|
|
@@ -340,7 +491,7 @@ export class TokenTracker {
|
|
|
340
491
|
);
|
|
341
492
|
}
|
|
342
493
|
|
|
343
|
-
private _aggregate(entries: UsageEntry
|
|
494
|
+
private _aggregate(entries: ReadonlyArray<UsageEntry>): TokenUsage {
|
|
344
495
|
const tokens = sumTokenCounts(entries.map((e) => e.tokens));
|
|
345
496
|
return {
|
|
346
497
|
...tokens,
|
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. */
|