@abelxiaoxing/cadence 1.2.1 → 1.2.3
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/README.md +7 -1
- package/agents/diagnosis-worker.md +6 -2
- package/agents/implementation-worker.md +9 -2
- package/config/routes.example.json +0 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +15 -4
- package/skills/abel-workflow/SKILL.md +5 -3
- package/src/apply-transaction.ts +1 -5
- package/src/child-session.ts +37 -46
- package/src/contracts.ts +278 -32
- package/src/control-contracts.ts +62 -0
- package/src/delivery-compiler.ts +239 -124
- package/src/design-control.ts +72 -27
- package/src/design-journal.ts +2 -2
- package/src/file-snapshot.ts +1 -1
- package/src/index.ts +295 -58
- package/src/packet-runtime.ts +79 -21
- package/src/parent-provider.ts +63 -30
- package/src/route-policy.ts +58 -14
- package/src/run-store.ts +115 -23
- package/src/subagent-activity.ts +82 -6
- package/src/submit-tool.ts +431 -39
- package/src/task-ledger.ts +1 -2
- package/src/worker-broker.ts +72 -13
- package/src/workflow-engine.ts +782 -54
- package/src/workspace-store.ts +2 -10
package/src/packet-runtime.ts
CHANGED
|
@@ -97,6 +97,16 @@ export type PacketChildRunner = (input: {
|
|
|
97
97
|
signal: AbortSignal;
|
|
98
98
|
}) => Promise<ChildSessionResult>;
|
|
99
99
|
|
|
100
|
+
type PacketRouteResult =
|
|
101
|
+
| {
|
|
102
|
+
kind: "phase-failure";
|
|
103
|
+
phase: Extract<
|
|
104
|
+
Awaited<ReturnType<typeof runtimeForWorkerRoute>>,
|
|
105
|
+
{ ok: false }
|
|
106
|
+
>;
|
|
107
|
+
}
|
|
108
|
+
| { kind: "child"; child: ChildSessionResult };
|
|
109
|
+
|
|
100
110
|
interface WaitingPacket {
|
|
101
111
|
controller: AbortController;
|
|
102
112
|
resolve(acquired: boolean): void;
|
|
@@ -251,7 +261,7 @@ export class PacketRuntime {
|
|
|
251
261
|
this.#notify(packet, observer, sequence, "running");
|
|
252
262
|
}
|
|
253
263
|
const usage = new UsageAggregator();
|
|
254
|
-
|
|
264
|
+
let child = await this.#dispatchChild(
|
|
255
265
|
packet,
|
|
256
266
|
context,
|
|
257
267
|
controller.signal,
|
|
@@ -259,6 +269,28 @@ export class PacketRuntime {
|
|
|
259
269
|
this.#notifyBrokerActivity(packet, observer, sequence, activity),
|
|
260
270
|
);
|
|
261
271
|
usage.add("launch:0", child.usage);
|
|
272
|
+
if (
|
|
273
|
+
this.#childRunner &&
|
|
274
|
+
!child.ok &&
|
|
275
|
+
child.failure.kind === "artifact" &&
|
|
276
|
+
!controller.signal.aborted
|
|
277
|
+
) {
|
|
278
|
+
this.#notifyBrokerActivity(packet, observer, sequence, {
|
|
279
|
+
state: "retrying",
|
|
280
|
+
attempt: 1,
|
|
281
|
+
maxAttempts: 2,
|
|
282
|
+
code: child.failure.code,
|
|
283
|
+
wait: "bounded-policy",
|
|
284
|
+
});
|
|
285
|
+
child = await this.#dispatchChild(
|
|
286
|
+
packet,
|
|
287
|
+
context,
|
|
288
|
+
controller.signal,
|
|
289
|
+
(activity) =>
|
|
290
|
+
this.#notifyBrokerActivity(packet, observer, sequence, activity),
|
|
291
|
+
);
|
|
292
|
+
usage.add("launch:1", child.usage);
|
|
293
|
+
}
|
|
262
294
|
if (!child.ok) {
|
|
263
295
|
const state = failureState(child.failureKind);
|
|
264
296
|
this.#notify(packet, observer, sequence, state);
|
|
@@ -319,12 +351,26 @@ export class PacketRuntime {
|
|
|
319
351
|
throw new Error("packet agent definition is unavailable");
|
|
320
352
|
}
|
|
321
353
|
const broker = this.#brokerFor(context);
|
|
322
|
-
const
|
|
354
|
+
const brokerUsage = new UsageAggregator();
|
|
355
|
+
let childAttempt = 0;
|
|
356
|
+
const withBrokerUsage = (
|
|
357
|
+
child: ChildSessionResult,
|
|
358
|
+
): ChildSessionResult => ({
|
|
359
|
+
...child,
|
|
360
|
+
usage: brokerUsage.total(),
|
|
361
|
+
});
|
|
362
|
+
const routed = await broker.run<PacketRouteResult>({
|
|
323
363
|
runId: `${packet.stage}:${packet.id}`,
|
|
324
364
|
operationId: packet.id,
|
|
325
365
|
role: packet.role as WorkerRole,
|
|
326
366
|
signal,
|
|
327
367
|
...(onActivity ? { onActivity } : {}),
|
|
368
|
+
classifyResult: (value) =>
|
|
369
|
+
value.kind === "child" &&
|
|
370
|
+
!value.child.ok &&
|
|
371
|
+
value.child.failure.kind === "artifact"
|
|
372
|
+
? value.child.failure.code
|
|
373
|
+
: undefined,
|
|
328
374
|
execute: async (attempt) => {
|
|
329
375
|
const phase = await runtimeForWorkerRoute(
|
|
330
376
|
attempt.route,
|
|
@@ -332,6 +378,7 @@ export class PacketRuntime {
|
|
|
332
378
|
this.#parentPayloadBridge,
|
|
333
379
|
attempt.signal,
|
|
334
380
|
this.#environment,
|
|
381
|
+
{ onResponse: attempt.onHeaders },
|
|
335
382
|
);
|
|
336
383
|
if (!phase.ok) {
|
|
337
384
|
if (phase.failure.kind === "cancelled") {
|
|
@@ -360,6 +407,15 @@ export class PacketRuntime {
|
|
|
360
407
|
role: packet.role,
|
|
361
408
|
phase: packet.phase,
|
|
362
409
|
output: packet.output as "evidence" | "diff",
|
|
410
|
+
...(packet.role === "diagnosis-worker" && packet.output === "diff"
|
|
411
|
+
? {
|
|
412
|
+
structuredPatch: {
|
|
413
|
+
workspaceRoot: context.cwd,
|
|
414
|
+
writePaths: packet.declared.write,
|
|
415
|
+
deletePaths: packet.declared.write,
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
: {}),
|
|
363
419
|
roots: packet.roots.map((root) => path.resolve(context.cwd, root)),
|
|
364
420
|
allowedPaths: [
|
|
365
421
|
...new Set([...packet.declared.read, ...packet.declared.write]),
|
|
@@ -367,9 +423,9 @@ export class PacketRuntime {
|
|
|
367
423
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
368
424
|
signal: attempt.signal,
|
|
369
425
|
failureOverride: phase.failureOverride,
|
|
370
|
-
onStreamStart: attempt.onHeaders,
|
|
371
426
|
onStreamProgress: attempt.onProgress,
|
|
372
427
|
});
|
|
428
|
+
brokerUsage.add(`attempt:${childAttempt++}`, child.usage);
|
|
373
429
|
if (!child.ok && child.failure.kind === "transport") {
|
|
374
430
|
throw new Error(child.failure.code);
|
|
375
431
|
}
|
|
@@ -379,29 +435,31 @@ export class PacketRuntime {
|
|
|
379
435
|
if (!routed.ok) {
|
|
380
436
|
const cancelled = routed.state === "cancelled" || signal.aborted;
|
|
381
437
|
const timedOut = !cancelled && routed.code === "phase-timeout";
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
438
|
+
return withBrokerUsage(
|
|
439
|
+
syntheticChildFailure(
|
|
440
|
+
cancelled ? "packet cancelled" : routed.code,
|
|
441
|
+
cancelled
|
|
442
|
+
? { kind: "cancelled", code: "cancelled" }
|
|
443
|
+
: timedOut
|
|
444
|
+
? {
|
|
445
|
+
kind: "transport",
|
|
446
|
+
code: "timeout",
|
|
447
|
+
stage: "child-timeout",
|
|
448
|
+
}
|
|
449
|
+
: {
|
|
450
|
+
kind: "transport",
|
|
451
|
+
code: "transport-failure",
|
|
452
|
+
stage: "child-provider-stream",
|
|
453
|
+
},
|
|
454
|
+
cancelled ? "cancelled" : timedOut ? "timed-out" : "failed",
|
|
455
|
+
),
|
|
398
456
|
);
|
|
399
457
|
}
|
|
400
458
|
if (routed.value.kind === "phase-failure") {
|
|
401
459
|
const phase = routed.value.phase;
|
|
402
|
-
return syntheticChildFailure(phase.error, phase.failure);
|
|
460
|
+
return withBrokerUsage(syntheticChildFailure(phase.error, phase.failure));
|
|
403
461
|
}
|
|
404
|
-
return routed.value.child;
|
|
462
|
+
return withBrokerUsage(routed.value.child);
|
|
405
463
|
}
|
|
406
464
|
|
|
407
465
|
#brokerFor(context: PacketContext): RunWorkerBroker {
|
package/src/parent-provider.ts
CHANGED
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
|
|
30
30
|
const SUBAGENT_PROVIDER_ID = "abel-subagent";
|
|
31
31
|
|
|
32
|
+
export interface PhaseTransportObserver {
|
|
33
|
+
onResponse?(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
function customStreamOptions<T extends StreamOptions>(
|
|
33
37
|
model: Model<string>,
|
|
34
38
|
options?: T,
|
|
@@ -49,32 +53,55 @@ function customStreamOptions<T extends StreamOptions>(
|
|
|
49
53
|
} as T;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
function observedStreamOptions<T extends StreamOptions>(
|
|
57
|
+
options?: T,
|
|
58
|
+
observer?: PhaseTransportObserver,
|
|
59
|
+
): T | undefined {
|
|
60
|
+
if (!observer?.onResponse) return options;
|
|
61
|
+
const childOnResponse = options?.onResponse;
|
|
62
|
+
return {
|
|
63
|
+
...options,
|
|
64
|
+
onResponse: async (response, requestModel) => {
|
|
65
|
+
observer.onResponse?.();
|
|
66
|
+
await childOnResponse?.(response, requestModel);
|
|
67
|
+
},
|
|
68
|
+
} as T;
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
// The compat dispatchers resolve the concrete API implementation from
|
|
53
72
|
// model.api at call time, so one pair serves all supported dialects.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
function subagentStreams(observer?: PhaseTransportObserver) {
|
|
74
|
+
return {
|
|
75
|
+
stream(
|
|
76
|
+
model: Parameters<typeof subagentStream>[0],
|
|
77
|
+
context: Parameters<typeof subagentStream>[1],
|
|
78
|
+
options?: Parameters<typeof subagentStream>[2],
|
|
79
|
+
) {
|
|
80
|
+
return subagentStream(
|
|
81
|
+
model,
|
|
82
|
+
context,
|
|
83
|
+
observedStreamOptions(
|
|
84
|
+
customStreamOptions(model as Model<string>, options),
|
|
85
|
+
observer,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
streamSimple(
|
|
90
|
+
model: Parameters<typeof subagentStreamSimple>[0],
|
|
91
|
+
context: Parameters<typeof subagentStreamSimple>[1],
|
|
92
|
+
options?: Parameters<typeof subagentStreamSimple>[2],
|
|
93
|
+
) {
|
|
94
|
+
return subagentStreamSimple(
|
|
95
|
+
model,
|
|
96
|
+
context,
|
|
97
|
+
observedStreamOptions(
|
|
98
|
+
customStreamOptions(model as Model<string>, options),
|
|
99
|
+
observer,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
78
105
|
|
|
79
106
|
function customSubagentModel(endpoint: ResolvedCustomRoute): Model<string> {
|
|
80
107
|
return {
|
|
@@ -97,6 +124,7 @@ function customSubagentModel(endpoint: ResolvedCustomRoute): Model<string> {
|
|
|
97
124
|
function customSubagentProvider(
|
|
98
125
|
endpoint: ResolvedCustomRoute,
|
|
99
126
|
model: Model<string>,
|
|
127
|
+
observer?: PhaseTransportObserver,
|
|
100
128
|
): Provider {
|
|
101
129
|
return createProvider({
|
|
102
130
|
id: SUBAGENT_PROVIDER_ID,
|
|
@@ -118,7 +146,7 @@ function customSubagentProvider(
|
|
|
118
146
|
},
|
|
119
147
|
},
|
|
120
148
|
models: [model],
|
|
121
|
-
api:
|
|
149
|
+
api: subagentStreams(observer),
|
|
122
150
|
}) as Provider;
|
|
123
151
|
}
|
|
124
152
|
|
|
@@ -126,11 +154,12 @@ export async function customPhaseRuntime(
|
|
|
126
154
|
route: CustomRoutePolicy,
|
|
127
155
|
signal?: AbortSignal,
|
|
128
156
|
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
157
|
+
observer?: PhaseTransportObserver,
|
|
129
158
|
): Promise<PhaseRuntimeResult> {
|
|
130
159
|
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
131
160
|
const endpoint = resolveCustomRoute(route, environment);
|
|
132
161
|
const model = customSubagentModel(endpoint);
|
|
133
|
-
const provider = customSubagentProvider(endpoint, model);
|
|
162
|
+
const provider = customSubagentProvider(endpoint, model, observer);
|
|
134
163
|
let modelRuntime: ModelRuntime;
|
|
135
164
|
try {
|
|
136
165
|
modelRuntime = await runtimeForProvider(provider, signal);
|
|
@@ -236,6 +265,7 @@ export function phaseProvider(
|
|
|
236
265
|
},
|
|
237
266
|
payloadBridge: PhasePayloadBridge,
|
|
238
267
|
diagnostic?: { failure?: PhaseRuntimeFailure },
|
|
268
|
+
observer?: PhaseTransportObserver,
|
|
239
269
|
): Provider {
|
|
240
270
|
return {
|
|
241
271
|
...parent,
|
|
@@ -262,7 +292,7 @@ export function phaseProvider(
|
|
|
262
292
|
diagnostic,
|
|
263
293
|
);
|
|
264
294
|
return parent.stream(model, context, {
|
|
265
|
-
...options,
|
|
295
|
+
...observedStreamOptions(options, observer),
|
|
266
296
|
apiKey: auth.apiKey,
|
|
267
297
|
headers: { ...options?.headers, ...auth.headers },
|
|
268
298
|
env: { ...options?.env, ...auth.env },
|
|
@@ -278,7 +308,7 @@ export function phaseProvider(
|
|
|
278
308
|
diagnostic,
|
|
279
309
|
);
|
|
280
310
|
return parent.streamSimple(model, context, {
|
|
281
|
-
...options,
|
|
311
|
+
...observedStreamOptions(options, observer),
|
|
282
312
|
apiKey: auth.apiKey,
|
|
283
313
|
headers: { ...options?.headers, ...auth.headers },
|
|
284
314
|
env: { ...options?.env, ...auth.env },
|
|
@@ -314,6 +344,7 @@ export async function runtimeFromContext(
|
|
|
314
344
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
315
345
|
payloadBridge: ParentPayloadBridge,
|
|
316
346
|
signal?: AbortSignal,
|
|
347
|
+
observer?: PhaseTransportObserver,
|
|
317
348
|
): Promise<PhaseRuntimeResult> {
|
|
318
349
|
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
319
350
|
if (!ctx.model) {
|
|
@@ -398,6 +429,7 @@ export async function runtimeFromContext(
|
|
|
398
429
|
capture,
|
|
399
430
|
},
|
|
400
431
|
diagnostic,
|
|
432
|
+
observer,
|
|
401
433
|
);
|
|
402
434
|
let modelRuntime: ModelRuntime;
|
|
403
435
|
try {
|
|
@@ -432,10 +464,11 @@ export async function runtimeForWorkerRoute(
|
|
|
432
464
|
payloadBridge: ParentPayloadBridge,
|
|
433
465
|
signal?: AbortSignal,
|
|
434
466
|
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
467
|
+
observer?: PhaseTransportObserver,
|
|
435
468
|
): Promise<PhaseRuntimeResult> {
|
|
436
469
|
return route.kind === "custom"
|
|
437
|
-
? customPhaseRuntime(route, signal, environment)
|
|
438
|
-
: runtimeFromContext(ctx, payloadBridge, signal);
|
|
470
|
+
? customPhaseRuntime(route, signal, environment, observer)
|
|
471
|
+
: runtimeFromContext(ctx, payloadBridge, signal, observer);
|
|
439
472
|
}
|
|
440
473
|
|
|
441
474
|
function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
package/src/route-policy.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ROLES } from "./contracts.ts";
|
|
6
6
|
|
|
7
|
-
export const ROUTE_POLICY_VERSION = 2 as const;
|
|
8
7
|
export const ROUTE_DIALECTS = [
|
|
9
8
|
"openai-completions",
|
|
10
9
|
"openai-responses",
|
|
@@ -48,7 +47,6 @@ export interface CustomRoutePolicy extends RouteBase {
|
|
|
48
47
|
export type WorkerRoutePolicy = InheritedRoutePolicy | CustomRoutePolicy;
|
|
49
48
|
|
|
50
49
|
export interface RoutePolicy {
|
|
51
|
-
version: typeof ROUTE_POLICY_VERSION;
|
|
52
50
|
routes: Readonly<Record<string, WorkerRoutePolicy>>;
|
|
53
51
|
roles: Readonly<Record<WorkerRole, readonly string[]>>;
|
|
54
52
|
}
|
|
@@ -61,11 +59,11 @@ export type RoutePolicyDiagnosticCode =
|
|
|
61
59
|
| "policy-unreadable"
|
|
62
60
|
| "policy-invalid-json"
|
|
63
61
|
| "policy-invalid"
|
|
62
|
+
| "policy-version-unsupported"
|
|
64
63
|
| "route-invalid"
|
|
65
64
|
| "route-duplicate"
|
|
66
65
|
| "role-invalid"
|
|
67
|
-
| "route-reference-invalid"
|
|
68
|
-
| "unsupported-policy-version";
|
|
66
|
+
| "route-reference-invalid";
|
|
69
67
|
|
|
70
68
|
export interface RoutePolicyDiagnostic {
|
|
71
69
|
code: RoutePolicyDiagnosticCode;
|
|
@@ -102,7 +100,6 @@ export type RoutePolicyResolution =
|
|
|
102
100
|
*/
|
|
103
101
|
export function unavailableRoutePolicy(): RoutePolicy {
|
|
104
102
|
return {
|
|
105
|
-
version: ROUTE_POLICY_VERSION,
|
|
106
103
|
routes: Object.freeze({}),
|
|
107
104
|
roles: Object.freeze(
|
|
108
105
|
Object.fromEntries(
|
|
@@ -273,18 +270,21 @@ function parseRoute(
|
|
|
273
270
|
}
|
|
274
271
|
|
|
275
272
|
export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
|
|
276
|
-
if (
|
|
277
|
-
!isRecord(value) ||
|
|
278
|
-
!hasExactKeys(value, ["version", "routes", "roles"])
|
|
279
|
-
) {
|
|
273
|
+
if (!isRecord(value)) {
|
|
280
274
|
return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
|
|
281
275
|
}
|
|
282
|
-
|
|
276
|
+
const legacyVersion = Object.hasOwn(value, "version")
|
|
277
|
+
? value.version
|
|
278
|
+
: undefined;
|
|
279
|
+
if (legacyVersion !== undefined && legacyVersion !== 2) {
|
|
283
280
|
return {
|
|
284
281
|
ok: false,
|
|
285
|
-
diagnostics: [{ code: "
|
|
282
|
+
diagnostics: [{ code: "policy-version-unsupported", field: "version" }],
|
|
286
283
|
};
|
|
287
284
|
}
|
|
285
|
+
if (!hasExactKeys(value, ["routes", "roles"], ["version"])) {
|
|
286
|
+
return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
|
|
287
|
+
}
|
|
288
288
|
if (!isRecord(value.routes) || !isRecord(value.roles)) {
|
|
289
289
|
return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
|
|
290
290
|
}
|
|
@@ -341,7 +341,6 @@ export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
|
|
|
341
341
|
return {
|
|
342
342
|
ok: true,
|
|
343
343
|
policy: {
|
|
344
|
-
version: ROUTE_POLICY_VERSION,
|
|
345
344
|
routes: Object.freeze(structuredClone(routes)),
|
|
346
345
|
roles: Object.freeze(
|
|
347
346
|
Object.fromEntries(
|
|
@@ -355,6 +354,48 @@ export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
|
|
|
355
354
|
};
|
|
356
355
|
}
|
|
357
356
|
|
|
357
|
+
function constrainInheritedRoutes(
|
|
358
|
+
policy: RoutePolicy,
|
|
359
|
+
parentModel?: ParentModelCapabilities,
|
|
360
|
+
): RoutePolicy {
|
|
361
|
+
if (!parentModel) return policy;
|
|
362
|
+
const routes = Object.fromEntries(
|
|
363
|
+
Object.entries(policy.routes).map(([routeId, route]) => {
|
|
364
|
+
if (route.kind !== "inherited") return [routeId, structuredClone(route)];
|
|
365
|
+
const capabilities = {
|
|
366
|
+
...structuredClone(route.capabilities),
|
|
367
|
+
contextWindow: Math.min(
|
|
368
|
+
route.capabilities.contextWindow,
|
|
369
|
+
parentModel.contextWindow,
|
|
370
|
+
),
|
|
371
|
+
maxTokens: Math.min(
|
|
372
|
+
route.capabilities.maxTokens,
|
|
373
|
+
parentModel.maxTokens,
|
|
374
|
+
route.capabilities.contextWindow,
|
|
375
|
+
parentModel.contextWindow,
|
|
376
|
+
),
|
|
377
|
+
};
|
|
378
|
+
const raw = { kind: "inherited" as const, capabilities };
|
|
379
|
+
return [
|
|
380
|
+
routeId,
|
|
381
|
+
{
|
|
382
|
+
id: route.id,
|
|
383
|
+
...raw,
|
|
384
|
+
fingerprint: routeFingerprint(raw),
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
return {
|
|
390
|
+
routes: Object.freeze(routes),
|
|
391
|
+
roles: Object.freeze(
|
|
392
|
+
Object.fromEntries(
|
|
393
|
+
ROLES.map((role) => [role, Object.freeze([...policy.roles[role]])]),
|
|
394
|
+
),
|
|
395
|
+
) as unknown as RoutePolicy["roles"],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
358
399
|
/** Default parent-model route used when no explicit policy file exists. */
|
|
359
400
|
export function parentRoutePolicy(
|
|
360
401
|
parentModel?: ParentModelCapabilities,
|
|
@@ -366,7 +407,6 @@ export function parentRoutePolicy(
|
|
|
366
407
|
? Math.min(parentModel.maxTokens, contextWindow)
|
|
367
408
|
: 1;
|
|
368
409
|
const parsed = parseRoutePolicy({
|
|
369
|
-
version: ROUTE_POLICY_VERSION,
|
|
370
410
|
routes: {
|
|
371
411
|
parent: {
|
|
372
412
|
kind: "inherited",
|
|
@@ -451,7 +491,11 @@ export function loadRoutePolicy(
|
|
|
451
491
|
}
|
|
452
492
|
const parsed = parseRoutePolicy(value);
|
|
453
493
|
return parsed.ok
|
|
454
|
-
? {
|
|
494
|
+
? {
|
|
495
|
+
ok: true,
|
|
496
|
+
source,
|
|
497
|
+
policy: constrainInheritedRoutes(parsed.policy, options.parentModel),
|
|
498
|
+
}
|
|
455
499
|
: { ok: false, source, diagnostics: parsed.diagnostics };
|
|
456
500
|
}
|
|
457
501
|
|
package/src/run-store.ts
CHANGED
|
@@ -17,7 +17,21 @@ import {
|
|
|
17
17
|
StateRootError,
|
|
18
18
|
} from "./state-root.ts";
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export class RunStoreFormatError extends Error {
|
|
21
|
+
readonly code = "run-store-reset-required" as const;
|
|
22
|
+
readonly databasePath: string;
|
|
23
|
+
readonly recovery = Object.freeze({
|
|
24
|
+
action: "reset-private-run-store" as const,
|
|
25
|
+
requiresBackup: true as const,
|
|
26
|
+
retryCommand: "status-or-start" as const,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
constructor(databasePath: string) {
|
|
30
|
+
super("run-store-reset-required");
|
|
31
|
+
this.name = "RunStoreFormatError";
|
|
32
|
+
this.databasePath = databasePath;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
21
35
|
|
|
22
36
|
const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
|
|
23
37
|
const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
@@ -84,10 +98,6 @@ export interface OperationLeaseStatus {
|
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
const SCHEMA = `
|
|
87
|
-
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
88
|
-
version INTEGER PRIMARY KEY CHECK (version = 4)
|
|
89
|
-
) STRICT;
|
|
90
|
-
|
|
91
101
|
CREATE TABLE IF NOT EXISTS runs (
|
|
92
102
|
run_id TEXT PRIMARY KEY,
|
|
93
103
|
root_hash TEXT NOT NULL,
|
|
@@ -191,6 +201,74 @@ function requireIdentifier(value: string, label: string): void {
|
|
|
191
201
|
if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
|
|
192
202
|
}
|
|
193
203
|
|
|
204
|
+
function configureDatabase(
|
|
205
|
+
database: DatabaseSync,
|
|
206
|
+
busyTimeoutMs: number | undefined,
|
|
207
|
+
): void {
|
|
208
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
209
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
210
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
211
|
+
database.exec(
|
|
212
|
+
`PRAGMA busy_timeout = ${Math.max(1, Math.min(30_000, busyTimeoutMs ?? 5_000))}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const REQUIRED_TABLES = [
|
|
217
|
+
"runs",
|
|
218
|
+
"delivery_bindings",
|
|
219
|
+
"events",
|
|
220
|
+
"tasks",
|
|
221
|
+
"operations",
|
|
222
|
+
"route_health",
|
|
223
|
+
"artifacts",
|
|
224
|
+
"workspace_revisions",
|
|
225
|
+
"apply_transactions",
|
|
226
|
+
"bootstrap_handoffs",
|
|
227
|
+
] as const;
|
|
228
|
+
|
|
229
|
+
const REQUIRED_DELIVERY_BINDING_COLUMNS = [
|
|
230
|
+
"run_id",
|
|
231
|
+
"gate",
|
|
232
|
+
"revision",
|
|
233
|
+
"receipt_hash",
|
|
234
|
+
"approval_revision",
|
|
235
|
+
"contract_hash",
|
|
236
|
+
"record_hash",
|
|
237
|
+
"operation_id",
|
|
238
|
+
] as const;
|
|
239
|
+
|
|
240
|
+
function applicationTables(database: DatabaseSync): string[] {
|
|
241
|
+
return (
|
|
242
|
+
database
|
|
243
|
+
.prepare(
|
|
244
|
+
`SELECT name FROM sqlite_master
|
|
245
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
246
|
+
ORDER BY name`,
|
|
247
|
+
)
|
|
248
|
+
.all() as unknown as Array<{ name: string }>
|
|
249
|
+
).map(({ name }) => name);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function tableColumns(database: DatabaseSync, table: string): string[] {
|
|
253
|
+
return (
|
|
254
|
+
database.prepare(`PRAGMA table_info(${table})`).all() as unknown as Array<{
|
|
255
|
+
name: string;
|
|
256
|
+
}>
|
|
257
|
+
).map(({ name }) => name);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasCurrentSchema(database: DatabaseSync): boolean {
|
|
261
|
+
const tables = applicationTables(database);
|
|
262
|
+
const deliveryBindingColumns = tableColumns(database, "delivery_bindings");
|
|
263
|
+
return (
|
|
264
|
+
!tables.includes("schema_meta") &&
|
|
265
|
+
REQUIRED_TABLES.every((table) => tables.includes(table)) &&
|
|
266
|
+
REQUIRED_DELIVERY_BINDING_COLUMNS.every((column) =>
|
|
267
|
+
deliveryBindingColumns.includes(column),
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
194
272
|
function parseProjection(value: string): RunProjection {
|
|
195
273
|
const parsed = JSON.parse(value) as RunProjection;
|
|
196
274
|
if (
|
|
@@ -262,25 +340,22 @@ export class RunStore {
|
|
|
262
340
|
}
|
|
263
341
|
chmodSync(stateRoot.databasePath, 0o600);
|
|
264
342
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
.run(RUN_STORE_SCHEMA_VERSION);
|
|
280
|
-
} else if (schemaRow.version !== RUN_STORE_SCHEMA_VERSION) {
|
|
281
|
-
database.close();
|
|
282
|
-
throw new Error("run-store-schema-version-rejected");
|
|
343
|
+
let database: DatabaseSync | undefined;
|
|
344
|
+
try {
|
|
345
|
+
database = new DatabaseSync(stateRoot.databasePath);
|
|
346
|
+
configureDatabase(database, options.busyTimeoutMs);
|
|
347
|
+
const tables = applicationTables(database);
|
|
348
|
+
if (tables.length === 0) {
|
|
349
|
+
database.exec(SCHEMA);
|
|
350
|
+
} else if (!hasCurrentSchema(database)) {
|
|
351
|
+
throw new RunStoreFormatError(stateRoot.databasePath);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
database?.close();
|
|
355
|
+
if (error instanceof RunStoreFormatError) throw error;
|
|
356
|
+
throw new RunStoreFormatError(stateRoot.databasePath);
|
|
283
357
|
}
|
|
358
|
+
if (!database) throw new RunStoreFormatError(stateRoot.databasePath);
|
|
284
359
|
const store = new RunStore(stateRoot, database, options);
|
|
285
360
|
store.#secureDatabaseFiles();
|
|
286
361
|
store.recoverExpiredLeases();
|
|
@@ -917,7 +992,23 @@ export class RunStore {
|
|
|
917
992
|
code?: string;
|
|
918
993
|
lease?: OperationLease;
|
|
919
994
|
}): RunProjection {
|
|
995
|
+
return this.transitionAtomically(input, () => {});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
transitionAtomically(
|
|
999
|
+
input: {
|
|
1000
|
+
runId: string;
|
|
1001
|
+
to: RunState;
|
|
1002
|
+
operationId: string;
|
|
1003
|
+
code?: string;
|
|
1004
|
+
lease?: OperationLease;
|
|
1005
|
+
},
|
|
1006
|
+
write: (database: DatabaseSync) => void,
|
|
1007
|
+
): RunProjection {
|
|
920
1008
|
requireIdentifier(input.operationId, "operation-id");
|
|
1009
|
+
if (typeof write !== "function") {
|
|
1010
|
+
throw new Error("state-transition-write-invalid");
|
|
1011
|
+
}
|
|
921
1012
|
return this.#transaction(() => {
|
|
922
1013
|
if (input.lease) {
|
|
923
1014
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
@@ -935,6 +1026,7 @@ export class RunStore {
|
|
|
935
1026
|
to: input.to,
|
|
936
1027
|
...(input.code ? { code: input.code } : {}),
|
|
937
1028
|
});
|
|
1029
|
+
write(this.#database);
|
|
938
1030
|
this.#recordOperation(
|
|
939
1031
|
input.runId,
|
|
940
1032
|
input.operationId,
|