@inixiative/foundry-core 0.1.0 → 0.3.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 -6
- package/src/action-prompt.ts +47 -28
- package/src/adapters/file-memory.ts +314 -122
- package/src/adapters/http-memory.ts +19 -24
- package/src/adapters/index.ts +4 -4
- package/src/adapters/markdown-docs.ts +15 -15
- package/src/adapters/sqlite-memory.ts +29 -27
- package/src/base-agent.ts +5 -7
- package/src/cache-lifecycle.ts +14 -14
- package/src/capability.ts +76 -83
- package/src/clarifier.ts +46 -0
- package/src/classifier.ts +6 -9
- package/src/context-layer.ts +125 -78
- package/src/context-stack.ts +15 -14
- package/src/credentials.ts +4 -2
- package/src/decider.ts +7 -8
- package/src/delivery-evidence.ts +206 -55
- package/src/event-stream.ts +34 -20
- package/src/executor.ts +81 -43
- package/src/harness.ts +225 -126
- package/src/hooks.ts +26 -26
- package/src/hydrator.ts +6 -8
- package/src/id.ts +3 -3
- package/src/index.ts +248 -218
- package/src/intervention.ts +21 -12
- package/src/messages.ts +73 -62
- package/src/middleware.ts +9 -19
- package/src/model-registry.ts +1587 -0
- package/src/native-evidence.ts +124 -30
- package/src/permission-middleware.ts +16 -28
- package/src/retry.ts +6 -6
- package/src/router.ts +5 -5
- package/src/signal.ts +16 -11
- package/src/thread.ts +138 -61
- package/src/token-counts.ts +9 -1
- package/src/token-tracker.ts +43 -34
- package/src/tools.ts +138 -95
- package/src/trace.ts +44 -23
- package/src/types.ts +33 -24
package/src/native-evidence.ts
CHANGED
|
@@ -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
|
|
15
|
-
|
|
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:
|
|
21
|
-
readonly cleanup:
|
|
22
|
-
readonly capacity:
|
|
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?:
|
|
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:
|
|
37
|
-
readonly localOutcome?:
|
|
38
|
-
readonly dispatch?:
|
|
39
|
-
readonly transportOutcome?:
|
|
40
|
-
readonly rpcOutcome?:
|
|
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<{
|
|
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<{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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?:
|
|
70
|
-
readonly textPhase?:
|
|
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<{
|
|
73
|
-
|
|
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: {
|
|
80
|
-
|
|
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:
|
|
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:
|
|
95
|
-
readonly publication:
|
|
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: {
|
|
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<{
|
|
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:
|
|
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) => {
|
|
139
|
-
|
|
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
|
|
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:
|
|
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 ===
|
|
59
|
-
ctx.annotations[
|
|
60
|
-
ctx.annotations[
|
|
61
|
-
throw new CapabilityDeniedError(defaultCapability,
|
|
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[
|
|
65
|
-
ctx.annotations[
|
|
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 ===
|
|
75
|
-
ctx.annotations[
|
|
76
|
-
ctx.annotations[
|
|
77
|
-
ctx.annotations[
|
|
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[
|
|
73
|
+
ctx.annotations['permission:modifiedPayload'] = result.modifiedPayload;
|
|
86
74
|
}
|
|
87
75
|
}
|
|
88
76
|
}
|
package/src/retry.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { BudgetExceededError } from
|
|
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 *
|
|
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[
|
|
83
|
-
ctx.annotations[
|
|
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
|
|
2
|
-
import type {
|
|
3
|
-
import type
|
|
4
|
-
import type { ExecuteMeta } from
|
|
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
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
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(
|
|
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(
|
|
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
|
|