@inixiative/foundry-core 0.4.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/executor.ts +46 -3
- package/src/feedback/correction.ts +366 -0
- package/src/harness.ts +16 -0
- package/src/index.ts +73 -9
- package/src/messages.ts +7 -1
- package/src/middleware.ts +5 -0
- package/src/model-registry.ts +57 -6
- 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/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
|
-
}
|