@inixiative/foundry-core 0.4.0 → 0.5.1

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.
@@ -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; readonly codexOverrides: readonly 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(payload, filterOverride, meta),
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.isWarm : false;
566
+ return l ? l.assemblable : false;
552
567
  },
553
568
  getContent: (id) => {
554
569
  const l = stack.getLayer(id);
555
- return l?.isWarm ? l.content : '';
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
- }