@inixiative/foundry-core 0.3.0 → 0.5.0
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 +1 -1
- package/src/action-prompt.ts +15 -16
- package/src/adapters/file-memory.ts +3 -2
- package/src/adapters/index.ts +37 -1
- package/src/adapters/markdown-docs.ts +243 -5
- package/src/adapters/markdown-hierarchy.ts +904 -0
- package/src/capability.ts +36 -28
- package/src/context-layer.ts +42 -4
- package/src/context-stack.ts +13 -4
- package/src/credentials.ts +2 -2
- package/src/executor.ts +46 -3
- package/src/feedback/correction.ts +366 -0
- package/src/harness.ts +16 -0
- package/src/index.ts +77 -9
- package/src/messages.ts +7 -1
- package/src/middleware.ts +5 -0
- package/src/model-registry.ts +118 -9
- package/src/native-evidence.ts +11 -1
- package/src/scope.ts +2 -0
- package/src/session-delivery.ts +76 -0
- package/src/thread.ts +18 -3
- package/src/types.ts +8 -0
- package/src/permission-middleware.ts +0 -80
package/src/model-registry.ts
CHANGED
|
@@ -23,6 +23,30 @@ export type ProviderType =
|
|
|
23
23
|
| 'custom';
|
|
24
24
|
export type RuntimeKind = 'api' | 'native-harness' | 'typed-decision';
|
|
25
25
|
|
|
26
|
+
/** Who trains the models a provider serves. Gateways and local hosts serve many labs and carry none. */
|
|
27
|
+
export type ModelLab =
|
|
28
|
+
| 'anthropic'
|
|
29
|
+
| 'openai'
|
|
30
|
+
| 'google'
|
|
31
|
+
| 'meta'
|
|
32
|
+
| 'xai'
|
|
33
|
+
| 'deepseek'
|
|
34
|
+
| 'moonshot'
|
|
35
|
+
| 'zhipu'
|
|
36
|
+
| 'alibaba'
|
|
37
|
+
| 'mistral'
|
|
38
|
+
| 'typesafe';
|
|
39
|
+
|
|
40
|
+
/** Labs whose models may run a main thread (the Artificer), on whatever transport reaches them. */
|
|
41
|
+
export const MAIN_THREAD_LABS = [
|
|
42
|
+
'anthropic',
|
|
43
|
+
'openai',
|
|
44
|
+
'google',
|
|
45
|
+
'meta',
|
|
46
|
+
'xai',
|
|
47
|
+
] as const satisfies readonly ModelLab[];
|
|
48
|
+
export type MainThreadLab = (typeof MAIN_THREAD_LABS)[number];
|
|
49
|
+
|
|
26
50
|
/**
|
|
27
51
|
* What a model is worth using for. Every model is a judgment client — that is
|
|
28
52
|
* not a provider category, it is a tag every entry carries. The other tags name
|
|
@@ -175,6 +199,8 @@ const MUSE_REASONING: ModelReasoning = {
|
|
|
175
199
|
export interface FoundryProviderInfo {
|
|
176
200
|
id: string;
|
|
177
201
|
type: ProviderType;
|
|
202
|
+
/** Absent for gateways and local hosts. */
|
|
203
|
+
lab?: ModelLab;
|
|
178
204
|
label: string;
|
|
179
205
|
description: string;
|
|
180
206
|
envKey?: string;
|
|
@@ -199,11 +225,17 @@ export interface ModelSweepOption {
|
|
|
199
225
|
export const MODEL_REGISTRY_UPDATED_AT = '2026-09-24';
|
|
200
226
|
|
|
201
227
|
/**
|
|
202
|
-
* Decision default: the cheapest current model tagged `judgment`. Held here
|
|
203
|
-
* rather than in an adapter so one map answers "what runs decisions".
|
|
228
|
+
* Decision default: Claude Haiku 5.5, the cheapest current Claude model tagged `judgment`. Held here
|
|
229
|
+
* rather than in an adapter so one map answers "what runs decisions". DECISION_PROVIDER is the
|
|
230
|
+
* API-token path; subscription decisions pick their model per runtime from SUBSCRIPTION_DECISION_MODELS.
|
|
204
231
|
*/
|
|
205
|
-
export const DECISION_PROVIDER = '
|
|
206
|
-
export const DECISION_MODEL = '
|
|
232
|
+
export const DECISION_PROVIDER = 'anthropic';
|
|
233
|
+
export const DECISION_MODEL = 'claude-haiku-5-5';
|
|
234
|
+
/** Subscription decision default per native runtime; settings choose the runtime. */
|
|
235
|
+
export const SUBSCRIPTION_DECISION_MODELS = {
|
|
236
|
+
claude: DECISION_MODEL,
|
|
237
|
+
codex: 'gpt-6-luna',
|
|
238
|
+
} as const;
|
|
207
239
|
|
|
208
240
|
/** Tag rule: every model judges. Cheap models review and route; capable models execute and advise. */
|
|
209
241
|
const FAST = ['judgment', 'learning-review', 'tool-use'] as ModelCapability[];
|
|
@@ -222,6 +254,7 @@ const POWERFUL_REASONING = [...POWERFUL, 'reasoning'] as ModelCapability[];
|
|
|
222
254
|
export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
223
255
|
'claude-code': {
|
|
224
256
|
id: 'claude-code',
|
|
257
|
+
lab: 'anthropic',
|
|
225
258
|
type: 'claude-code',
|
|
226
259
|
label: 'Claude Code (CLI subscription)',
|
|
227
260
|
description: "Native Claude Code harness using the user's authenticated subscription.",
|
|
@@ -243,7 +276,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
243
276
|
},
|
|
244
277
|
{
|
|
245
278
|
id: 'opus',
|
|
246
|
-
label: 'Opus 5',
|
|
279
|
+
label: 'Opus 5.5',
|
|
247
280
|
tier: 'powerful',
|
|
248
281
|
costTier: 'high',
|
|
249
282
|
contextWindow: 1_000_000,
|
|
@@ -251,10 +284,11 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
251
284
|
nativeAlias: true,
|
|
252
285
|
capabilities: POWERFUL_REASONING,
|
|
253
286
|
reasoning: CLAUDE_REASONING,
|
|
287
|
+
notes: 'Claude Code alias for claude-opus-5-5.',
|
|
254
288
|
},
|
|
255
289
|
{
|
|
256
290
|
id: 'sonnet',
|
|
257
|
-
label: 'Sonnet 5',
|
|
291
|
+
label: 'Sonnet 5.5',
|
|
258
292
|
tier: 'standard',
|
|
259
293
|
costTier: 'medium',
|
|
260
294
|
contextWindow: 1_000_000,
|
|
@@ -262,13 +296,46 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
262
296
|
nativeAlias: true,
|
|
263
297
|
capabilities: STANDARD_REASONING,
|
|
264
298
|
reasoning: CLAUDE_REASONING,
|
|
299
|
+
notes: 'Claude Code alias for claude-sonnet-5-5.',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'claude-haiku-5-5',
|
|
303
|
+
label: 'Haiku 5.5',
|
|
304
|
+
tier: 'fast',
|
|
305
|
+
costTier: 'low',
|
|
306
|
+
contextWindow: 1_000_000,
|
|
307
|
+
runtimeKind: 'native-harness',
|
|
308
|
+
capabilities: FAST_REASONING,
|
|
309
|
+
reasoning: { ...CLAUDE_REASONING, efforts: ['low', 'medium', 'high'] },
|
|
310
|
+
notes: "Foundry's decision model; the CLI reports this id as the served model.",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: 'claude-opus-5-5',
|
|
314
|
+
label: 'Opus 5.5',
|
|
315
|
+
tier: 'powerful',
|
|
316
|
+
costTier: 'high',
|
|
317
|
+
contextWindow: 1_000_000,
|
|
318
|
+
runtimeKind: 'native-harness',
|
|
319
|
+
capabilities: POWERFUL_REASONING,
|
|
320
|
+
reasoning: CLAUDE_REASONING,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'claude-fable-5-1',
|
|
324
|
+
label: 'Fable 5.1',
|
|
325
|
+
tier: 'powerful',
|
|
326
|
+
costTier: 'high',
|
|
327
|
+
contextWindow: 1_000_000,
|
|
328
|
+
maxOutputTokens: 128_000,
|
|
329
|
+
runtimeKind: 'native-harness',
|
|
330
|
+
capabilities: POWERFUL_REASONING,
|
|
331
|
+
reasoning: CLAUDE_REASONING,
|
|
265
332
|
},
|
|
266
333
|
{
|
|
267
334
|
id: 'haiku',
|
|
268
|
-
label: 'Haiku
|
|
335
|
+
label: 'Haiku 5.5',
|
|
269
336
|
tier: 'fast',
|
|
270
337
|
costTier: 'low',
|
|
271
|
-
contextWindow:
|
|
338
|
+
contextWindow: 1_000_000,
|
|
272
339
|
runtimeKind: 'native-harness',
|
|
273
340
|
nativeAlias: true,
|
|
274
341
|
capabilities: FAST_REASONING,
|
|
@@ -278,6 +345,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
278
345
|
},
|
|
279
346
|
anthropic: {
|
|
280
347
|
id: 'anthropic',
|
|
348
|
+
lab: 'anthropic',
|
|
281
349
|
type: 'anthropic',
|
|
282
350
|
label: 'Anthropic (API key)',
|
|
283
351
|
description: 'Direct Anthropic API access without the Claude Code native harness.',
|
|
@@ -297,6 +365,17 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
297
365
|
reasoning: CLAUDE_REASONING,
|
|
298
366
|
notes: 'Thinking is always on; steer with effort.',
|
|
299
367
|
},
|
|
368
|
+
{
|
|
369
|
+
id: 'claude-opus-5-5',
|
|
370
|
+
label: 'Claude Opus 5.5',
|
|
371
|
+
tier: 'powerful',
|
|
372
|
+
costTier: 'high',
|
|
373
|
+
contextWindow: 1_000_000,
|
|
374
|
+
maxOutputTokens: 128_000,
|
|
375
|
+
runtimeKind: 'api',
|
|
376
|
+
capabilities: POWERFUL_REASONING,
|
|
377
|
+
reasoning: CLAUDE_REASONING,
|
|
378
|
+
},
|
|
300
379
|
{
|
|
301
380
|
id: 'claude-opus-5',
|
|
302
381
|
label: 'Claude Opus 5',
|
|
@@ -319,6 +398,18 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
319
398
|
capabilities: STANDARD_REASONING,
|
|
320
399
|
reasoning: CLAUDE_REASONING,
|
|
321
400
|
},
|
|
401
|
+
{
|
|
402
|
+
id: 'claude-haiku-5-5',
|
|
403
|
+
label: 'Claude Haiku 5.5',
|
|
404
|
+
tier: 'fast',
|
|
405
|
+
costTier: 'low',
|
|
406
|
+
contextWindow: 1_000_000,
|
|
407
|
+
maxOutputTokens: 128_000,
|
|
408
|
+
runtimeKind: 'api',
|
|
409
|
+
capabilities: FAST_REASONING,
|
|
410
|
+
reasoning: { ...CLAUDE_REASONING, efforts: ['low', 'medium', 'high'] },
|
|
411
|
+
},
|
|
412
|
+
// Previous generation, still served.
|
|
322
413
|
{
|
|
323
414
|
id: 'claude-haiku-4-5',
|
|
324
415
|
label: 'Claude Haiku 4.5',
|
|
@@ -330,7 +421,6 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
330
421
|
reasoning: { ...CLAUDE_REASONING, efforts: ['low', 'medium', 'high'] },
|
|
331
422
|
notes: 'Takes a thinking budget, not an effort ladder.',
|
|
332
423
|
},
|
|
333
|
-
// Previous generation, still served.
|
|
334
424
|
{
|
|
335
425
|
id: 'claude-fable-5',
|
|
336
426
|
label: 'Claude Fable 5',
|
|
@@ -390,6 +480,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
390
480
|
},
|
|
391
481
|
codex: {
|
|
392
482
|
id: 'codex',
|
|
483
|
+
lab: 'openai',
|
|
393
484
|
type: 'codex',
|
|
394
485
|
label: 'Codex CLI (OpenAI subscription)',
|
|
395
486
|
description: "Native Codex harness using the user's authenticated OpenAI/Codex subscription.",
|
|
@@ -480,6 +571,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
480
571
|
},
|
|
481
572
|
openai: {
|
|
482
573
|
id: 'openai',
|
|
574
|
+
lab: 'openai',
|
|
483
575
|
type: 'openai',
|
|
484
576
|
label: 'OpenAI',
|
|
485
577
|
description: 'OpenAI API models for classification, routing, and execution.',
|
|
@@ -575,6 +667,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
575
667
|
},
|
|
576
668
|
gemini: {
|
|
577
669
|
id: 'gemini',
|
|
670
|
+
lab: 'google',
|
|
578
671
|
type: 'gemini',
|
|
579
672
|
label: 'Google Gemini',
|
|
580
673
|
description: 'Gemini API models for high-volume agent work.',
|
|
@@ -641,6 +734,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
641
734
|
},
|
|
642
735
|
meta: {
|
|
643
736
|
id: 'meta',
|
|
737
|
+
lab: 'meta',
|
|
644
738
|
type: 'openai-compatible',
|
|
645
739
|
// Llama is not Meta's frontier line any more; Muse Spark replaced it in April 2026
|
|
646
740
|
// and there is no "Llama 5". The open-weight Llama entries under openrouter,
|
|
@@ -721,6 +815,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
721
815
|
},
|
|
722
816
|
xai: {
|
|
723
817
|
id: 'xai',
|
|
818
|
+
lab: 'xai',
|
|
724
819
|
type: 'xai',
|
|
725
820
|
label: 'xAI (Grok)',
|
|
726
821
|
description:
|
|
@@ -803,6 +898,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
803
898
|
},
|
|
804
899
|
deepseek: {
|
|
805
900
|
id: 'deepseek',
|
|
901
|
+
lab: 'deepseek',
|
|
806
902
|
type: 'openai-compatible',
|
|
807
903
|
label: 'DeepSeek',
|
|
808
904
|
description:
|
|
@@ -839,6 +935,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
839
935
|
},
|
|
840
936
|
kimi: {
|
|
841
937
|
id: 'kimi',
|
|
938
|
+
lab: 'moonshot',
|
|
842
939
|
type: 'openai-compatible',
|
|
843
940
|
label: 'Kimi (Moonshot AI)',
|
|
844
941
|
description: 'Moonshot Kimi models. OpenAI-compatible; temperature range is 0-1, not 0-2.',
|
|
@@ -893,6 +990,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
893
990
|
},
|
|
894
991
|
glm: {
|
|
895
992
|
id: 'glm',
|
|
993
|
+
lab: 'zhipu',
|
|
896
994
|
type: 'openai-compatible',
|
|
897
995
|
label: 'GLM (Z.ai / Zhipu)',
|
|
898
996
|
description:
|
|
@@ -1030,6 +1128,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
1030
1128
|
},
|
|
1031
1129
|
qwen: {
|
|
1032
1130
|
id: 'qwen',
|
|
1131
|
+
lab: 'alibaba',
|
|
1033
1132
|
type: 'openai-compatible',
|
|
1034
1133
|
label: 'Qwen (Alibaba Model Studio)',
|
|
1035
1134
|
// The legacy static domain still works; Alibaba now recommends the per-workspace
|
|
@@ -1439,6 +1538,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
1439
1538
|
},
|
|
1440
1539
|
mistral: {
|
|
1441
1540
|
id: 'mistral',
|
|
1541
|
+
lab: 'mistral',
|
|
1442
1542
|
type: 'openai-compatible',
|
|
1443
1543
|
label: 'Mistral AI',
|
|
1444
1544
|
description:
|
|
@@ -1507,6 +1607,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
|
|
|
1507
1607
|
},
|
|
1508
1608
|
typesafe: {
|
|
1509
1609
|
id: 'typesafe',
|
|
1610
|
+
lab: 'typesafe',
|
|
1510
1611
|
type: 'typesafe',
|
|
1511
1612
|
label: 'TypeSafe (Jev)',
|
|
1512
1613
|
description:
|
|
@@ -1565,6 +1666,14 @@ export function providersWithCapability(capability: ModelCapability): FoundryPro
|
|
|
1565
1666
|
);
|
|
1566
1667
|
}
|
|
1567
1668
|
|
|
1669
|
+
/** The main-thread lab a provider serves, or undefined when it serves none (or many). */
|
|
1670
|
+
export function mainThreadLab(providerId: string): MainThreadLab | undefined {
|
|
1671
|
+
const lab = MODEL_REGISTRY[providerId]?.lab;
|
|
1672
|
+
return (MAIN_THREAD_LABS as readonly string[]).includes(lab ?? '')
|
|
1673
|
+
? (lab as MainThreadLab)
|
|
1674
|
+
: undefined;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1568
1677
|
/** The part of a Foundry configuration `resolveDecisionModel` reads. */
|
|
1569
1678
|
export interface DecisionModelDefaults {
|
|
1570
1679
|
defaults: {
|
package/src/native-evidence.ts
CHANGED
|
@@ -167,7 +167,7 @@ export interface NativeBridgeLease {
|
|
|
167
167
|
readonly toolPolicy?: NativeToolPolicy;
|
|
168
168
|
/** Private, empty native cwd owned by a restricted fixture lease. */
|
|
169
169
|
readonly fixtureCwd?: string;
|
|
170
|
-
readonly launch: { readonly claudeJson: string
|
|
170
|
+
readonly launch: { readonly claudeJson: string };
|
|
171
171
|
check(): void;
|
|
172
172
|
register(evidence: NativeEvidence): void;
|
|
173
173
|
observe(evidence: NativeEvidence): void;
|
|
@@ -194,7 +194,17 @@ export interface NativeObservation {
|
|
|
194
194
|
register(evidence: NativeEvidence): void | Promise<void>;
|
|
195
195
|
/** Fallible observer; never vetoes executor completion. */
|
|
196
196
|
observe(evidence: NativeEvidence): void | Promise<void>;
|
|
197
|
+
/** Receives the stop for this owner's registered admission, where the runtime acknowledges stops. */
|
|
198
|
+
interruptible?(interrupt: NativeInterrupt): void;
|
|
199
|
+
/**
|
|
200
|
+
* The executor's public thinking text, for live display only. It never becomes evidence:
|
|
201
|
+
* nothing here is journalled, traced or projected (reasoning stays out of NativeEvidence).
|
|
202
|
+
*/
|
|
203
|
+
thought?(text: string): void;
|
|
197
204
|
}
|
|
205
|
+
/** `acknowledged`: the runtime confirmed the stop and the turn reached its native terminal. */
|
|
206
|
+
export type NativeInterruptOutcome = 'acknowledged' | 'no-turn' | 'unacknowledged';
|
|
207
|
+
export type NativeInterrupt = (opts?: { timeoutMs?: number }) => Promise<NativeInterruptOutcome>;
|
|
198
208
|
export function freezeEvidence<T>(value: T): T {
|
|
199
209
|
const copy = structuredClone(value);
|
|
200
210
|
const freeze = (v: unknown) => {
|
package/src/scope.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
export interface OwnershipScope {
|
|
10
10
|
readonly threadId?: string;
|
|
11
11
|
readonly projectId?: string;
|
|
12
|
+
/** Where the thread's native session runs. Not ownership: sources that read the workspace resolve against it. */
|
|
13
|
+
readonly cwd?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/** Copy a scope, dropping undefined fields so equality checks are stable. */
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { computeHash } from './context-layer';
|
|
2
|
+
import type { PromptBlock } from './context-stack';
|
|
3
|
+
|
|
4
|
+
/** One standing part of a thread's context (a layer with its prompt, or the agent's own prompt), keyed by content hash. */
|
|
5
|
+
export interface StandingPart {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly hash: string;
|
|
8
|
+
readonly text: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A turn's context split for a provider that keeps a native session. Standing parts go into the
|
|
13
|
+
* session's system prompt once, when the session starts; afterwards only parts whose hash differs
|
|
14
|
+
* from what the session holds are sent with the turn. The per-turn text is sent every turn.
|
|
15
|
+
*/
|
|
16
|
+
export interface SessionDelivery {
|
|
17
|
+
/** Every standing part, in context order. */
|
|
18
|
+
readonly standing: readonly StandingPart[];
|
|
19
|
+
/** Per-turn context (routing, Warden advice); sent with every turn. */
|
|
20
|
+
readonly turn: string;
|
|
21
|
+
/**
|
|
22
|
+
* The thread's delivery ledger: part id → hash the session holds from an earlier turn's message.
|
|
23
|
+
* Cleared on compaction, reset when a session starts.
|
|
24
|
+
*/
|
|
25
|
+
readonly held: Readonly<Record<string, string>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeliveredPart {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly hash: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What a session provider actually delivered for one turn. */
|
|
34
|
+
export interface SessionDeliveryReport {
|
|
35
|
+
/** "started": this turn ran on a session that started with `base` as its system prompt. */
|
|
36
|
+
readonly session: 'started' | 'held';
|
|
37
|
+
/** The parts the session's system prompt carries. */
|
|
38
|
+
readonly base: readonly DeliveredPart[];
|
|
39
|
+
/** Standing parts sent in this turn's message because the session did not hold their hash. */
|
|
40
|
+
readonly sent: readonly DeliveredPart[];
|
|
41
|
+
/**
|
|
42
|
+
* A started session that resumed the conversation the thread's ledger describes (a relaunch in
|
|
43
|
+
* this process): the ledger still holds, so it is kept and extended rather than reset.
|
|
44
|
+
*/
|
|
45
|
+
readonly ledgerKept?: true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Group assembled layer blocks (a layer's prompt block, then its content block) into standing parts. */
|
|
49
|
+
export function standingParts(blocks: readonly PromptBlock[]): StandingPart[] {
|
|
50
|
+
const parts: Array<{ id: string; texts: string[] }> = [];
|
|
51
|
+
for (const block of blocks) {
|
|
52
|
+
const id = block.id ?? `${block.role}:${parts.length}`;
|
|
53
|
+
const last = parts.at(-1);
|
|
54
|
+
if (last && last.id === id) last.texts.push(block.text);
|
|
55
|
+
else parts.push({ id, texts: [block.text] });
|
|
56
|
+
}
|
|
57
|
+
// Two layers may share an id (a configured layer and a runtime-owned one); each is its own part.
|
|
58
|
+
const seen = new Map<string, number>();
|
|
59
|
+
return parts.map(({ id, texts }) => {
|
|
60
|
+
const n = (seen.get(id) ?? 0) + 1;
|
|
61
|
+
seen.set(id, n);
|
|
62
|
+
return standingPart(n === 1 ? id : `${id}#${n}`, texts.join('\n\n'));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A warm layer as the standing part the executor assembles: its prompt, then its content. */
|
|
67
|
+
export function layerPart(layer: { id: string; prompt?: string; content: string }): StandingPart {
|
|
68
|
+
return standingPart(
|
|
69
|
+
layer.id,
|
|
70
|
+
layer.prompt ? `${layer.prompt}\n\n${layer.content}` : layer.content,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function standingPart(id: string, text: string): StandingPart {
|
|
75
|
+
return { id, hash: computeHash(text), text };
|
|
76
|
+
}
|
package/src/thread.ts
CHANGED
|
@@ -125,6 +125,14 @@ export interface DispatchOptions {
|
|
|
125
125
|
messageId?: string;
|
|
126
126
|
/** How the caller invoked the agent: always, conditional, on-demand, background. */
|
|
127
127
|
invocation?: string;
|
|
128
|
+
/**
|
|
129
|
+
* What the agent itself receives when it differs from the payload that middleware and the
|
|
130
|
+
* dispatch observation read: a host's instruction wrappers go to the agent verbatim, while
|
|
131
|
+
* everything that learns from the message sees only the user's words.
|
|
132
|
+
*/
|
|
133
|
+
agentPayload?: unknown;
|
|
134
|
+
/** Lets this dispatch's middleware include an on-demand layer in its assembly (`RequestContext.requestLayer`). */
|
|
135
|
+
requestLayer?: (layerId: string) => void;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
/**
|
|
@@ -355,6 +363,7 @@ export class Thread {
|
|
|
355
363
|
stack: this._createStackView(),
|
|
356
364
|
projectId: this.meta.projectId,
|
|
357
365
|
messageId: opts?.messageId,
|
|
366
|
+
...(opts?.requestLayer ? { requestLayer: opts.requestLayer } : {}),
|
|
358
367
|
};
|
|
359
368
|
|
|
360
369
|
const start = performance.now();
|
|
@@ -398,6 +407,8 @@ export class Thread {
|
|
|
398
407
|
bridge: opts.nativeObservation.bridge,
|
|
399
408
|
register: opts.nativeObservation.register,
|
|
400
409
|
observe: opts.nativeObservation.observe,
|
|
410
|
+
interruptible: opts.nativeObservation.interruptible,
|
|
411
|
+
thought: opts.nativeObservation.thought,
|
|
401
412
|
owner: Object.freeze({
|
|
402
413
|
threadId: this.id,
|
|
403
414
|
projectId: this.meta.projectId,
|
|
@@ -411,7 +422,11 @@ export class Thread {
|
|
|
411
422
|
|
|
412
423
|
try {
|
|
413
424
|
const result = await this.middleware.execute(ctx, () =>
|
|
414
|
-
agent.run(
|
|
425
|
+
agent.run(
|
|
426
|
+
opts?.agentPayload !== undefined ? (opts.agentPayload as TPayload) : payload,
|
|
427
|
+
filterOverride,
|
|
428
|
+
meta,
|
|
429
|
+
),
|
|
415
430
|
);
|
|
416
431
|
|
|
417
432
|
const durationMs = performance.now() - start;
|
|
@@ -548,11 +563,11 @@ export class Thread {
|
|
|
548
563
|
return {
|
|
549
564
|
hasLayer: (id) => {
|
|
550
565
|
const l = stack.getLayer(id);
|
|
551
|
-
return l ? l.
|
|
566
|
+
return l ? l.assemblable : false;
|
|
552
567
|
},
|
|
553
568
|
getContent: (id) => {
|
|
554
569
|
const l = stack.getLayer(id);
|
|
555
|
-
return l?.
|
|
570
|
+
return l?.assemblable ? l.content : '';
|
|
556
571
|
},
|
|
557
572
|
getState: (id) => stack.getLayer(id)?.state,
|
|
558
573
|
get layerIds() {
|
package/src/types.ts
CHANGED
|
@@ -114,6 +114,12 @@ export interface CompletionOpts {
|
|
|
114
114
|
* cached prefix; others ignore it. Messages are sent unchanged either way.
|
|
115
115
|
*/
|
|
116
116
|
stablePrefix?: string;
|
|
117
|
+
/**
|
|
118
|
+
* The executor's context split into standing parts and per-turn text. A provider that keeps a
|
|
119
|
+
* native session delivers standing parts once and then only what changed (session-delivery.ts);
|
|
120
|
+
* others ignore it and send the messages unchanged.
|
|
121
|
+
*/
|
|
122
|
+
delivery?: import('./session-delivery').SessionDelivery;
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
export interface CompletionResult {
|
|
@@ -125,6 +131,8 @@ export interface CompletionResult {
|
|
|
125
131
|
/** Tool calls requested by the LLM (present when finishReason is "tool_use"). */
|
|
126
132
|
readonly toolCalls?: ToolCall[];
|
|
127
133
|
readonly raw?: unknown;
|
|
134
|
+
/** What a session provider delivered when it used `CompletionOpts.delivery`. */
|
|
135
|
+
readonly delivery?: import('./session-delivery').SessionDeliveryReport;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
export interface EmbeddingResult {
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { type Capability, CapabilityDeniedError, type CapabilityGate } from './capability';
|
|
2
|
-
import type { DispatchContext, Middleware, MiddlewareNext } from './middleware';
|
|
3
|
-
|
|
4
|
-
export interface PermissionCheckResult {
|
|
5
|
-
action: 'allow' | 'deny' | 'ask';
|
|
6
|
-
/** Optional modified payload (like Claude Code's updatedInput). */
|
|
7
|
-
modifiedPayload?: unknown;
|
|
8
|
-
/** Reason for denial. */
|
|
9
|
-
reason?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface PermissionCheck {
|
|
13
|
-
/** Which capability this check relates to. */
|
|
14
|
-
capability: Capability;
|
|
15
|
-
/** Narrow the check to specific agents. If omitted, applies to all. */
|
|
16
|
-
agentIds?: string[];
|
|
17
|
-
/** Custom check logic beyond the gate. */
|
|
18
|
-
check: (ctx: DispatchContext) => PermissionCheckResult | Promise<PermissionCheckResult>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface PermissionMiddlewareConfig {
|
|
22
|
-
/** The capability gate to check against. */
|
|
23
|
-
gate: CapabilityGate;
|
|
24
|
-
/** Thread ID for prompt context. */
|
|
25
|
-
threadId: string;
|
|
26
|
-
/** Default capability to check for all dispatches. Default: "llm:call". */
|
|
27
|
-
defaultCapability?: Capability;
|
|
28
|
-
/** Additional custom permission checks. */
|
|
29
|
-
checks?: PermissionCheck[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Creates a middleware that checks permissions before dispatch.
|
|
34
|
-
*
|
|
35
|
-
* Integrates the existing CapabilityGate into the dispatch pipeline.
|
|
36
|
-
* Records permission decisions in ctx.annotations.
|
|
37
|
-
*/
|
|
38
|
-
export function permissionMiddleware(config: PermissionMiddlewareConfig): Middleware {
|
|
39
|
-
const { gate, threadId, defaultCapability = 'llm:call', checks } = config;
|
|
40
|
-
|
|
41
|
-
return async (ctx: DispatchContext, next: MiddlewareNext) => {
|
|
42
|
-
// Check default capability via gate
|
|
43
|
-
const resolution = await gate.check(defaultCapability, {
|
|
44
|
-
agentId: ctx.agentId,
|
|
45
|
-
threadId,
|
|
46
|
-
detail: `Dispatch to agent ${ctx.agentId}`,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (resolution.action === 'rejected') {
|
|
50
|
-
ctx.annotations['permission:denied'] = true;
|
|
51
|
-
ctx.annotations['permission:capability'] = defaultCapability;
|
|
52
|
-
throw new CapabilityDeniedError(defaultCapability, 'deny');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
ctx.annotations['permission:approved'] = true;
|
|
56
|
-
ctx.annotations['permission:by'] = resolution.by;
|
|
57
|
-
|
|
58
|
-
// Run custom checks
|
|
59
|
-
if (checks) {
|
|
60
|
-
for (const check of checks) {
|
|
61
|
-
if (check.agentIds && !check.agentIds.includes(ctx.agentId)) continue;
|
|
62
|
-
|
|
63
|
-
const result = await check.check(ctx);
|
|
64
|
-
|
|
65
|
-
if (result.action === 'deny') {
|
|
66
|
-
ctx.annotations['permission:denied'] = true;
|
|
67
|
-
ctx.annotations['permission:capability'] = check.capability;
|
|
68
|
-
ctx.annotations['permission:reason'] = result.reason;
|
|
69
|
-
throw new CapabilityDeniedError(check.capability, 'deny');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (result.modifiedPayload !== undefined) {
|
|
73
|
-
ctx.annotations['permission:modifiedPayload'] = result.modifiedPayload;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return next();
|
|
79
|
-
};
|
|
80
|
-
}
|