@inixiative/foundry-core 0.2.0 → 0.4.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.
@@ -11,19 +11,32 @@ export interface NativeOwner {
11
11
  }
12
12
  /** Compare typed ownership fields, not serialization order or prompt text. */
13
13
  export function sameNativeOwner(a: NativeOwner | undefined, b: NativeOwner | undefined): boolean {
14
- return !!a && !!b && (["threadId", "projectId", "generation", "messageId", "dispatchId", "reviewJobId", "providerSessionKey"] as const)
15
- .every(key => a[key] === b[key]);
14
+ return (
15
+ !!a &&
16
+ !!b &&
17
+ (
18
+ [
19
+ 'threadId',
20
+ 'projectId',
21
+ 'generation',
22
+ 'messageId',
23
+ 'dispatchId',
24
+ 'reviewJobId',
25
+ 'providerSessionKey',
26
+ ] as const
27
+ ).every((key) => a[key] === b[key])
28
+ );
16
29
  }
17
30
  export interface OwnedAdmissionInspection {
18
31
  readonly evidence: NativeEvidence;
19
32
  /** Physical call settlement is independent of a native terminal and local waiter. */
20
- readonly call: "settled" | "pending" | "unknown";
21
- readonly cleanup: "not-requested" | "pending" | "released" | "unknown";
22
- readonly capacity: "settled" | "unknown";
33
+ readonly call: 'settled' | 'pending' | 'unknown';
34
+ readonly cleanup: 'not-requested' | 'pending' | 'released' | 'unknown';
35
+ readonly capacity: 'settled' | 'unknown';
23
36
  }
24
37
  export interface NativeEvidence {
25
38
  readonly schema: 1;
26
- readonly executionMode?: "controlled-fixture";
39
+ readonly executionMode?: 'controlled-fixture';
27
40
  readonly owner?: NativeOwner;
28
41
  readonly admissionId?: string;
29
42
  readonly nativeSessionId?: string;
@@ -33,15 +46,22 @@ export interface NativeEvidence {
33
46
  readonly itemId?: string;
34
47
  readonly callId?: string;
35
48
  readonly rpcRequestId?: string | number;
36
- readonly nativeOutcome: "unknown" | "completed" | "failed";
37
- readonly localOutcome?: "pending" | "resolved" | "rejected";
38
- readonly dispatch?: "not-dispatched" | "attempted";
39
- readonly transportOutcome?: "open" | "failed" | "closed";
40
- readonly rpcOutcome?: "pending" | "resolved" | "failed" | "unknown";
49
+ readonly nativeOutcome: 'unknown' | 'completed' | 'failed';
50
+ readonly localOutcome?: 'pending' | 'resolved' | 'rejected';
51
+ readonly dispatch?: 'not-dispatched' | 'attempted';
52
+ readonly transportOutcome?: 'open' | 'failed' | 'closed';
53
+ readonly rpcOutcome?: 'pending' | 'resolved' | 'failed' | 'unknown';
41
54
  readonly localFailure?: string;
42
55
  readonly localError?: string;
43
56
  readonly observedAt?: number;
44
- readonly terminal?: Readonly<{ type: string; eventId?: string; turnId?: string; subtype?: string; reason?: string; apiErrorStatus?: number }>;
57
+ readonly terminal?: Readonly<{
58
+ type: string;
59
+ eventId?: string;
60
+ turnId?: string;
61
+ subtype?: string;
62
+ reason?: string;
63
+ apiErrorStatus?: number;
64
+ }>;
45
65
  readonly content?: string;
46
66
  readonly correlation?: string;
47
67
  readonly kind?: string;
@@ -60,27 +80,60 @@ export interface NativeEvidence {
60
80
  readonly toolOutputOmittedTypes?: readonly string[];
61
81
  readonly toolError?: boolean;
62
82
  readonly observationFailures?: number;
63
- readonly configuration?: Readonly<{ requestedModel: string; observedModel?: string; requestedMaxTurns?: number | null;
64
- engine?: "mcp" | "app-server"; transportMode?: "controlled-fixture"; requestedEffort?: string; observedEffort?: string | null;
65
- history?: Readonly<{source:"thread/start"|"thread/resume";available:boolean;hasMore:boolean;turns:readonly Readonly<{id:string;status:string}>[]}>;
66
- turnBudgetEnforcement: "launch-option" | "unavailable"; tokenBudget: "unavailable"; effortBudget: "unavailable" }>;
83
+ readonly configuration?: Readonly<{
84
+ requestedModel: string;
85
+ observedModel?: string;
86
+ requestedMaxTurns?: number | null;
87
+ engine?: 'mcp' | 'app-server';
88
+ transportMode?: 'controlled-fixture';
89
+ requestedEffort?: string;
90
+ observedEffort?: string | null;
91
+ history?: Readonly<{
92
+ source: 'thread/start' | 'thread/resume';
93
+ available: boolean;
94
+ hasMore: boolean;
95
+ turns: readonly Readonly<{ id: string; status: string }>[];
96
+ }>;
97
+ turnBudgetEnforcement: 'launch-option' | 'unavailable';
98
+ tokenBudget: 'unavailable';
99
+ effortBudget: 'unavailable';
100
+ }>;
67
101
  readonly text?: string;
68
102
  /** Public text form only; reasoning is never projected. Missing phase is not a final-answer acknowledgment. */
69
- readonly textKind?: "delta" | "snapshot";
70
- readonly textPhase?: "commentary" | "final_answer";
103
+ readonly textKind?: 'delta' | 'snapshot';
104
+ readonly textPhase?: 'commentary' | 'final_answer';
71
105
  /** Safe setup/status evidence, never launch paths or config values. */
72
- readonly runtimeStatus?: Readonly<{ type: string; status?: string; reason?: string; server?: string; tools?: readonly string[] }>;
73
- readonly bridge?: { readonly id: string; readonly configurationHash: string; readonly toolPolicy?: NativeToolPolicy; readonly tools: readonly NativeToolEvidence[] };
106
+ readonly runtimeStatus?: Readonly<{
107
+ type: string;
108
+ status?: string;
109
+ reason?: string;
110
+ server?: string;
111
+ tools?: readonly string[];
112
+ }>;
113
+ readonly bridge?: {
114
+ readonly id: string;
115
+ readonly configurationHash: string;
116
+ readonly toolPolicy?: NativeToolPolicy;
117
+ readonly tools: readonly NativeToolEvidence[];
118
+ };
74
119
  }
75
120
  export interface NativeToolRecord {
76
121
  readonly id: string;
77
122
  readonly bridgeId: string;
78
123
  readonly operation: string;
79
- readonly owner: { readonly threadId: string; readonly projectId?: string; readonly generation: string };
80
- readonly association: { readonly kind: "registered-admission-window" | "unassociated"; readonly admissionId?: string; readonly owner?: NativeOwner };
124
+ readonly owner: {
125
+ readonly threadId: string;
126
+ readonly projectId?: string;
127
+ readonly generation: string;
128
+ };
129
+ readonly association: {
130
+ readonly kind: 'registered-admission-window' | 'unassociated';
131
+ readonly admissionId?: string;
132
+ readonly owner?: NativeOwner;
133
+ };
81
134
  readonly sdkRequestId?: string | number;
82
135
  readonly sdkSessionId?: string;
83
- readonly nativeCorrelation: "unknown";
136
+ readonly nativeCorrelation: 'unknown';
84
137
  readonly startedAt: number;
85
138
  readonly finishedAt: number;
86
139
  readonly arguments: Readonly<Record<string, unknown>>;
@@ -91,8 +144,8 @@ export interface NativeToolRecord {
91
144
  }
92
145
  export interface NativeToolEvidence {
93
146
  readonly record: NativeToolRecord;
94
- readonly persistence: "pending" | "committed" | "failed";
95
- readonly publication: "pending" | "published" | "reconciliation-needed";
147
+ readonly persistence: 'pending' | 'committed' | 'failed';
148
+ readonly publication: 'pending' | 'published' | 'reconciliation-needed';
96
149
  readonly error?: string;
97
150
  }
98
151
  /** Process lifetime grant. Data/functions never become part of a provider prompt. */
@@ -105,7 +158,11 @@ export interface NativeBridgeSource {
105
158
  export interface NativeBridgeLease {
106
159
  readonly id: string;
107
160
  readonly name: string;
108
- readonly owner: { readonly threadId: string; readonly projectId?: string; readonly generation: string };
161
+ readonly owner: {
162
+ readonly threadId: string;
163
+ readonly projectId?: string;
164
+ readonly generation: string;
165
+ };
109
166
  readonly configurationHash: string;
110
167
  readonly toolPolicy?: NativeToolPolicy;
111
168
  /** Private, empty native cwd owned by a restricted fixture lease. */
@@ -115,12 +172,17 @@ export interface NativeBridgeLease {
115
172
  register(evidence: NativeEvidence): void;
116
173
  observe(evidence: NativeEvidence): void;
117
174
  evidence(admissionId?: string): readonly NativeToolEvidence[];
118
- status?(): Readonly<{ closed: boolean; pendingCleanups: number; cleanupFailures: number; evictedRecords: number }>;
175
+ status?(): Readonly<{
176
+ closed: boolean;
177
+ pendingCleanups: number;
178
+ cleanupFailures: number;
179
+ evictedRecords: number;
180
+ }>;
119
181
  close(): Promise<void>;
120
182
  }
121
183
  /** Immutable policy identity; contains no credentials, launch paths or tool input. */
122
184
  export interface NativeToolPolicy {
123
- readonly version: "isolated-fixture-v1";
185
+ readonly version: 'isolated-fixture-v1';
124
186
  readonly digest: string;
125
187
  }
126
188
  export interface NativeObservation {
@@ -135,6 +197,38 @@ export interface NativeObservation {
135
197
  }
136
198
  export function freezeEvidence<T>(value: T): T {
137
199
  const copy = structuredClone(value);
138
- const freeze = (v: unknown) => { if (v && typeof v === "object" && !Object.isFrozen(v)) { for (const nested of Object.values(v)) freeze(nested); Object.freeze(v); } };
139
- freeze(copy); return copy;
200
+ const freeze = (v: unknown) => {
201
+ if (v && typeof v === 'object' && !Object.isFrozen(v)) {
202
+ for (const nested of Object.values(v)) freeze(nested);
203
+ Object.freeze(v);
204
+ }
205
+ };
206
+ freeze(copy);
207
+ return copy;
208
+ }
209
+ /** The model and effort a call ran on. */
210
+ export interface ServedModel {
211
+ readonly model: string;
212
+ readonly effort?: string;
213
+ }
214
+ /** What a native configuration says ran: the engine's acknowledgment over the request. */
215
+ export function configuredModel(
216
+ configuration: NativeEvidence['configuration'] | undefined,
217
+ ): ServedModel | undefined {
218
+ if (!configuration) return undefined;
219
+ const model = configuration.observedModel ?? configuration.requestedModel;
220
+ const effort =
221
+ configuration.observedEffort === null
222
+ ? undefined
223
+ : (configuration.observedEffort ?? configuration.requestedEffort);
224
+ return model ? { model, ...(effort ? { effort } : {}) } : undefined;
225
+ }
226
+ /** What served a completion: its native configuration, else the model the provider reports. */
227
+ export function servedModel(
228
+ result: Pick<import('./types').CompletionResult, 'model' | 'native'>,
229
+ ): ServedModel | undefined {
230
+ return (
231
+ configuredModel(result.native?.configuration) ??
232
+ (result.model ? { model: result.model } : undefined)
233
+ );
140
234
  }
@@ -1,12 +1,8 @@
1
- import type { Middleware, DispatchContext, MiddlewareNext } from "./middleware";
2
- import {
3
- CapabilityGate,
4
- CapabilityDeniedError,
5
- type Capability,
6
- } from "./capability";
1
+ import { type Capability, CapabilityDeniedError, type CapabilityGate } from './capability';
2
+ import type { DispatchContext, Middleware, MiddlewareNext } from './middleware';
7
3
 
8
4
  export interface PermissionCheckResult {
9
- action: "allow" | "deny" | "ask";
5
+ action: 'allow' | 'deny' | 'ask';
10
6
  /** Optional modified payload (like Claude Code's updatedInput). */
11
7
  modifiedPayload?: unknown;
12
8
  /** Reason for denial. */
@@ -40,12 +36,7 @@ export interface PermissionMiddlewareConfig {
40
36
  * Records permission decisions in ctx.annotations.
41
37
  */
42
38
  export function permissionMiddleware(config: PermissionMiddlewareConfig): Middleware {
43
- const {
44
- gate,
45
- threadId,
46
- defaultCapability = "llm:call",
47
- checks,
48
- } = config;
39
+ const { gate, threadId, defaultCapability = 'llm:call', checks } = config;
49
40
 
50
41
  return async (ctx: DispatchContext, next: MiddlewareNext) => {
51
42
  // Check default capability via gate
@@ -55,14 +46,14 @@ export function permissionMiddleware(config: PermissionMiddlewareConfig): Middle
55
46
  detail: `Dispatch to agent ${ctx.agentId}`,
56
47
  });
57
48
 
58
- if (resolution.action === "rejected") {
59
- ctx.annotations["permission:denied"] = true;
60
- ctx.annotations["permission:capability"] = defaultCapability;
61
- throw new CapabilityDeniedError(defaultCapability, "deny");
49
+ if (resolution.action === 'rejected') {
50
+ ctx.annotations['permission:denied'] = true;
51
+ ctx.annotations['permission:capability'] = defaultCapability;
52
+ throw new CapabilityDeniedError(defaultCapability, 'deny');
62
53
  }
63
54
 
64
- ctx.annotations["permission:approved"] = true;
65
- ctx.annotations["permission:by"] = resolution.by;
55
+ ctx.annotations['permission:approved'] = true;
56
+ ctx.annotations['permission:by'] = resolution.by;
66
57
 
67
58
  // Run custom checks
68
59
  if (checks) {
@@ -71,18 +62,15 @@ export function permissionMiddleware(config: PermissionMiddlewareConfig): Middle
71
62
 
72
63
  const result = await check.check(ctx);
73
64
 
74
- if (result.action === "deny") {
75
- ctx.annotations["permission:denied"] = true;
76
- ctx.annotations["permission:capability"] = check.capability;
77
- ctx.annotations["permission:reason"] = result.reason;
78
- throw new CapabilityDeniedError(
79
- check.capability,
80
- "deny",
81
- );
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');
82
70
  }
83
71
 
84
72
  if (result.modifiedPayload !== undefined) {
85
- ctx.annotations["permission:modifiedPayload"] = result.modifiedPayload;
73
+ ctx.annotations['permission:modifiedPayload'] = result.modifiedPayload;
86
74
  }
87
75
  }
88
76
  }
package/src/retry.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Middleware, DispatchContext, MiddlewareNext } from "./middleware";
2
- import { CapabilityDeniedError } from "./capability";
3
- import { BudgetExceededError } from "./token-tracker";
1
+ import { CapabilityDeniedError } from './capability';
2
+ import type { DispatchContext, Middleware, MiddlewareNext } from './middleware';
3
+ import { BudgetExceededError } from './token-tracker';
4
4
 
5
5
  export interface RetryConfig {
6
6
  /** Maximum retry attempts. Default: 3. */
@@ -64,7 +64,7 @@ export function retryMiddleware(config: RetryConfig = {}): Middleware {
64
64
  const elapsed = Date.now() - startTime;
65
65
  if (elapsed >= giveUpAfterMs) break;
66
66
 
67
- let delay = initialDelayMs * Math.pow(backoffFactor, attempt);
67
+ let delay = initialDelayMs * backoffFactor ** attempt;
68
68
  delay = Math.min(delay, maxDelayMs);
69
69
 
70
70
  if (jitter) {
@@ -79,8 +79,8 @@ export function retryMiddleware(config: RetryConfig = {}): Middleware {
79
79
  }
80
80
 
81
81
  onRetry?.(attempt + 1, error, delay);
82
- ctx.annotations["retry:attempt"] = attempt + 1;
83
- ctx.annotations["retry:lastError"] = String(error);
82
+ ctx.annotations['retry:attempt'] = attempt + 1;
83
+ ctx.annotations['retry:lastError'] = String(error);
84
84
 
85
85
  await sleep(delay);
86
86
  }
package/src/router.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Decider, type DeciderConfig, type Decision } from "./decider";
2
- import type { AgentConfig } from "./base-agent";
3
- import type { ContextStack } from "./context-stack";
4
- import type { ExecuteMeta } from "./executor";
1
+ import type { AgentConfig } from './base-agent';
2
+ import type { ContextStack } from './context-stack';
3
+ import { Decider, type DeciderConfig, type Decision } from './decider';
4
+ import type { ExecuteMeta } from './executor';
5
5
 
6
6
  /**
7
7
  * A routing decision — "where should this go?"
@@ -16,7 +16,7 @@ export interface Route {
16
16
  export type RouteHandler<TPayload> = (
17
17
  context: string,
18
18
  payload: TPayload,
19
- meta?: ExecuteMeta
19
+ meta?: ExecuteMeta,
20
20
  ) => Promise<Decision<Route>>;
21
21
 
22
22
  export interface RouterConfig<TPayload = unknown> extends AgentConfig {
package/src/signal.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  * Extensible via string union, not a closed enum.
4
4
  */
5
5
  export type SignalKind =
6
- | "correction"
7
- | "convention"
8
- | "taste"
9
- | "ci_rule"
10
- | "adr"
11
- | "security"
6
+ | 'correction'
7
+ | 'convention'
8
+ | 'taste'
9
+ | 'ci_rule'
10
+ | 'adr'
11
+ | 'security'
12
+ | 'guard_finding'
12
13
  | (string & {});
13
14
 
14
15
  /**
@@ -89,7 +90,10 @@ export class SignalBus {
89
90
  try {
90
91
  await handler(signal);
91
92
  } catch (err) {
92
- console.warn(`[SignalBus] handler error for signal "${signal.kind}":`, (err as Error).message ?? err);
93
+ console.warn(
94
+ `[SignalBus] handler error for signal "${signal.kind}":`,
95
+ (err as Error).message ?? err,
96
+ );
93
97
  }
94
98
  }
95
99
 
@@ -97,16 +101,17 @@ export class SignalBus {
97
101
  try {
98
102
  await handler(signal);
99
103
  } catch (err) {
100
- console.warn(`[SignalBus] global handler error for signal "${signal.kind}":`, (err as Error).message ?? err);
104
+ console.warn(
105
+ `[SignalBus] global handler error for signal "${signal.kind}":`,
106
+ (err as Error).message ?? err,
107
+ );
101
108
  }
102
109
  }
103
110
  }
104
111
 
105
112
  /** Get recent signals, optionally filtered by kind. */
106
113
  recent(kind?: SignalKind, limit: number = 50): ReadonlyArray<Signal> {
107
- const filtered = kind
108
- ? this._history.filter((s) => s.kind === kind)
109
- : this._history;
114
+ const filtered = kind ? this._history.filter((s) => s.kind === kind) : this._history;
110
115
  return filtered.slice(-limit);
111
116
  }
112
117