@abelxiaoxing/cadence 1.3.0 → 1.4.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.
- package/README.md +172 -13
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +9 -5
- package/prompts/abel-design.md +141 -38
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/activity-contracts.ts +82 -0
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +33 -12
- package/src/candidate-workspace.ts +74 -0
- package/src/canonical.ts +16 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +273 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +296 -328
- package/src/contracts.ts +325 -109
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +241 -141
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +182 -15
- package/src/design-diagnostics.ts +150 -0
- package/src/design-journal.ts +216 -72
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +963 -0
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +14 -16
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +341 -1405
- package/src/isolation-backend.ts +84 -23
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +46 -67
- package/src/submit-tool.ts +40 -61
- package/src/task-ledger.ts +14 -55
- package/src/transport-budget.ts +32 -0
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +134 -91
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1388 -0
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +4086 -0
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +991 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/worker-broker.ts
CHANGED
|
@@ -5,11 +5,9 @@ import {
|
|
|
5
5
|
type WorkerRole,
|
|
6
6
|
type WorkerRoutePolicy,
|
|
7
7
|
} from "./route-policy.ts";
|
|
8
|
+
import { TransportTimeout } from "./transport-budget.ts";
|
|
8
9
|
|
|
9
10
|
export const ROUTE_ATTEMPT_BOUNDS = Object.freeze({
|
|
10
|
-
connectMs: 10_000,
|
|
11
|
-
firstResponseMs: 90_000,
|
|
12
|
-
idleMs: 3 * 60_000,
|
|
13
11
|
totalMs: 20 * 60_000,
|
|
14
12
|
cooldownMs: 30_000,
|
|
15
13
|
attemptsPerRoute: 2,
|
|
@@ -21,6 +19,8 @@ export interface RouteRequirements {
|
|
|
21
19
|
dialects?: RouteDialect[];
|
|
22
20
|
minContextWindow?: number;
|
|
23
21
|
minOutputTokens?: number;
|
|
22
|
+
preferredContextWindow?: number;
|
|
23
|
+
preferredOutputTokens?: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
interface RouteHealth {
|
|
@@ -56,6 +56,7 @@ export interface WorkerBrokerOptions {
|
|
|
56
56
|
|
|
57
57
|
export interface RunWorkerAttemptInput<T> {
|
|
58
58
|
runId: string;
|
|
59
|
+
taskId?: string;
|
|
59
60
|
operationId: string;
|
|
60
61
|
role: WorkerRole;
|
|
61
62
|
requirements?: RouteRequirements;
|
|
@@ -68,6 +69,7 @@ export interface RunWorkerAttemptInput<T> {
|
|
|
68
69
|
export interface RouteAttemptContext {
|
|
69
70
|
route: WorkerRoutePolicy;
|
|
70
71
|
signal: AbortSignal;
|
|
72
|
+
onRequestStart(): void;
|
|
71
73
|
onHeaders(): void;
|
|
72
74
|
onProgress(): void;
|
|
73
75
|
}
|
|
@@ -78,7 +80,7 @@ export interface BrokerAttemptEvidence {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export interface BrokerActivityUpdate {
|
|
81
|
-
state: "
|
|
83
|
+
state: "preparing" | "waiting-first-response" | "running" | "retrying";
|
|
82
84
|
attempt: number;
|
|
83
85
|
maxAttempts: number;
|
|
84
86
|
code?: string;
|
|
@@ -97,12 +99,7 @@ function emitBrokerActivity(
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
class RouteAttemptStop extends Error {
|
|
100
|
-
readonly code:
|
|
101
|
-
| "connect-timeout"
|
|
102
|
-
| "first-response-timeout"
|
|
103
|
-
| "idle-timeout"
|
|
104
|
-
| "phase-timeout"
|
|
105
|
-
| "cancelled";
|
|
102
|
+
readonly code: "cancelled";
|
|
106
103
|
|
|
107
104
|
constructor(code: RouteAttemptStop["code"]) {
|
|
108
105
|
super(code);
|
|
@@ -126,7 +123,13 @@ function validRequirements(value: unknown): value is RouteRequirements {
|
|
|
126
123
|
if (
|
|
127
124
|
Object.keys(candidate).some(
|
|
128
125
|
(key) =>
|
|
129
|
-
![
|
|
126
|
+
![
|
|
127
|
+
"dialects",
|
|
128
|
+
"minContextWindow",
|
|
129
|
+
"minOutputTokens",
|
|
130
|
+
"preferredContextWindow",
|
|
131
|
+
"preferredOutputTokens",
|
|
132
|
+
].includes(key),
|
|
130
133
|
)
|
|
131
134
|
) {
|
|
132
135
|
return false;
|
|
@@ -147,7 +150,12 @@ function validRequirements(value: unknown): value is RouteRequirements {
|
|
|
147
150
|
(candidate.minContextWindow as number) > 0)) &&
|
|
148
151
|
(candidate.minOutputTokens === undefined ||
|
|
149
152
|
(Number.isSafeInteger(candidate.minOutputTokens) &&
|
|
150
|
-
(candidate.minOutputTokens as number) > 0))
|
|
153
|
+
(candidate.minOutputTokens as number) > 0)) &&
|
|
154
|
+
[candidate.preferredContextWindow, candidate.preferredOutputTokens].every(
|
|
155
|
+
(value) =>
|
|
156
|
+
value === undefined ||
|
|
157
|
+
(Number.isSafeInteger(value) && (value as number) > 0),
|
|
158
|
+
)
|
|
151
159
|
);
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -168,7 +176,8 @@ function capable(
|
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
function safeFailureCode(error: unknown): string {
|
|
171
|
-
if (error instanceof RouteAttemptStop
|
|
179
|
+
if (error instanceof RouteAttemptStop || error instanceof TransportTimeout)
|
|
180
|
+
return error.code;
|
|
172
181
|
return "transport-failure";
|
|
173
182
|
}
|
|
174
183
|
|
|
@@ -176,7 +185,6 @@ export class WorkerBroker {
|
|
|
176
185
|
readonly #policy: RoutePolicy;
|
|
177
186
|
readonly #now: () => number;
|
|
178
187
|
readonly #healthStore: RouteHealthStore;
|
|
179
|
-
readonly #health = new Map<string, RouteHealth>();
|
|
180
188
|
|
|
181
189
|
constructor(policy: RoutePolicy, options: WorkerBrokerOptions = {}) {
|
|
182
190
|
this.#policy = structuredClone(policy);
|
|
@@ -187,7 +195,6 @@ export class WorkerBroker {
|
|
|
187
195
|
#routeHealth(routeId: string): RouteHealth {
|
|
188
196
|
const route = this.#policy.routes[routeId];
|
|
189
197
|
return (
|
|
190
|
-
this.#health.get(routeId) ??
|
|
191
198
|
(route ? this.#healthStore.get(route.fingerprint) : undefined) ?? {
|
|
192
199
|
state: "healthy",
|
|
193
200
|
}
|
|
@@ -197,7 +204,6 @@ export class WorkerBroker {
|
|
|
197
204
|
#setRouteHealth(routeId: string, health: RouteHealth): void {
|
|
198
205
|
const route = this.#policy.routes[routeId];
|
|
199
206
|
if (!route) throw new Error("route-not-declared");
|
|
200
|
-
this.#health.set(routeId, structuredClone(health));
|
|
201
207
|
this.#healthStore.set(route.fingerprint, health);
|
|
202
208
|
}
|
|
203
209
|
|
|
@@ -229,14 +235,13 @@ export class WorkerBroker {
|
|
|
229
235
|
});
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
select(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}):
|
|
238
|
+
select(
|
|
239
|
+
input: RouteRequirements & {
|
|
240
|
+
role: WorkerRole;
|
|
241
|
+
exclude?: readonly string[];
|
|
242
|
+
routeId?: string;
|
|
243
|
+
},
|
|
244
|
+
):
|
|
240
245
|
| { ok: true; route: WorkerRoutePolicy; health: RouteHealthState }
|
|
241
246
|
| { ok: false; code: "endpoint-unavailable" | "route-not-declared" } {
|
|
242
247
|
const requirements: RouteRequirements = {
|
|
@@ -247,6 +252,12 @@ export class WorkerBroker {
|
|
|
247
252
|
...(input.minOutputTokens === undefined
|
|
248
253
|
? {}
|
|
249
254
|
: { minOutputTokens: input.minOutputTokens }),
|
|
255
|
+
...(input.preferredContextWindow === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { preferredContextWindow: input.preferredContextWindow }),
|
|
258
|
+
...(input.preferredOutputTokens === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: { preferredOutputTokens: input.preferredOutputTokens }),
|
|
250
261
|
};
|
|
251
262
|
if (!isRole(input.role) || !validRequirements(requirements)) {
|
|
252
263
|
return { ok: false, code: "endpoint-unavailable" };
|
|
@@ -261,7 +272,22 @@ export class WorkerBroker {
|
|
|
261
272
|
return { ok: false, code: "route-not-declared" };
|
|
262
273
|
}
|
|
263
274
|
const excluded = new Set(input.exclude ?? []);
|
|
264
|
-
|
|
275
|
+
// Stable preference tiers preserve declared order within each tier. Health,
|
|
276
|
+
// hard minima and explicit pins still determine eligibility.
|
|
277
|
+
const preferred = (routeId: string) => {
|
|
278
|
+
const route = this.#policy.routes[routeId];
|
|
279
|
+
return route &&
|
|
280
|
+
route.capabilities.contextWindow >=
|
|
281
|
+
(requirements.preferredContextWindow ?? 0) &&
|
|
282
|
+
route.capabilities.maxTokens >=
|
|
283
|
+
(requirements.preferredOutputTokens ?? 0)
|
|
284
|
+
? 1
|
|
285
|
+
: 0;
|
|
286
|
+
};
|
|
287
|
+
const ordered = [...candidates].sort(
|
|
288
|
+
(left, right) => preferred(right) - preferred(left),
|
|
289
|
+
);
|
|
290
|
+
for (const routeId of ordered) {
|
|
265
291
|
if (excluded.has(routeId)) continue;
|
|
266
292
|
const route = this.#policy.routes[routeId];
|
|
267
293
|
if (!route || !capable(route, input.role, requirements)) continue;
|
|
@@ -300,59 +326,39 @@ export class WorkerBroker {
|
|
|
300
326
|
onActivity?: (state: "waiting-first-response" | "running") => void,
|
|
301
327
|
): Promise<T> {
|
|
302
328
|
const controller = new AbortController();
|
|
303
|
-
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const stop = (code: RouteAttemptStop["code"]) => {
|
|
310
|
-
if (!controller.signal.aborted)
|
|
311
|
-
controller.abort(new RouteAttemptStop(code));
|
|
312
|
-
};
|
|
313
|
-
const schedule = (code: RouteAttemptStop["code"], milliseconds: number) => {
|
|
314
|
-
const timer = setTimeout(() => stop(code), milliseconds);
|
|
315
|
-
timers.add(timer);
|
|
316
|
-
return timer;
|
|
317
|
-
};
|
|
318
|
-
const clear = (timer: ReturnType<typeof setTimeout> | undefined) => {
|
|
319
|
-
if (timer !== undefined) {
|
|
320
|
-
clearTimeout(timer);
|
|
321
|
-
timers.delete(timer);
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
const onParentAbort = () => stop("cancelled");
|
|
325
|
-
if (parentSignal?.aborted) stop("cancelled");
|
|
329
|
+
let accepting = true;
|
|
330
|
+
let running = false;
|
|
331
|
+
const onParentAbort = () =>
|
|
332
|
+
controller.abort(new RouteAttemptStop("cancelled"));
|
|
333
|
+
if (parentSignal?.aborted) onParentAbort();
|
|
326
334
|
else parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
335
|
+
const timer = setTimeout(
|
|
336
|
+
() => controller.abort(new TransportTimeout("attempt-timeout")),
|
|
337
|
+
ROUTE_ATTEMPT_BOUNDS.totalMs,
|
|
338
|
+
);
|
|
330
339
|
let removeAbortRace: () => void = () => {};
|
|
331
340
|
try {
|
|
332
|
-
const execution = Promise.resolve().then(() =>
|
|
333
|
-
|
|
341
|
+
const execution = Promise.resolve().then(() => {
|
|
342
|
+
controller.signal.throwIfAborted();
|
|
343
|
+
return execute({
|
|
334
344
|
route: structuredClone(route),
|
|
335
345
|
signal: controller.signal,
|
|
346
|
+
onRequestStart: () => {
|
|
347
|
+
if (!accepting || controller.signal.aborted) return;
|
|
348
|
+
running = false;
|
|
349
|
+
onActivity?.("waiting-first-response");
|
|
350
|
+
},
|
|
336
351
|
onHeaders: () => {
|
|
337
|
-
if (controller.signal.aborted ||
|
|
338
|
-
headersSeen = true;
|
|
339
|
-
clear(connectTimer);
|
|
352
|
+
if (!accepting || controller.signal.aborted || running) return;
|
|
340
353
|
onActivity?.("waiting-first-response");
|
|
341
|
-
firstResponseTimer = schedule(
|
|
342
|
-
"first-response-timeout",
|
|
343
|
-
ROUTE_ATTEMPT_BOUNDS.firstResponseMs,
|
|
344
|
-
);
|
|
345
354
|
},
|
|
346
355
|
onProgress: () => {
|
|
347
|
-
if (controller.signal.aborted) return;
|
|
348
|
-
|
|
349
|
-
clear(firstResponseTimer);
|
|
350
|
-
clear(idleTimer);
|
|
351
|
-
idleTimer = schedule("idle-timeout", ROUTE_ATTEMPT_BOUNDS.idleMs);
|
|
356
|
+
if (!accepting || controller.signal.aborted) return;
|
|
357
|
+
running = true;
|
|
352
358
|
onActivity?.("running");
|
|
353
359
|
},
|
|
354
|
-
})
|
|
355
|
-
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
356
362
|
// A timed-out Provider may ignore its AbortSignal and settle later. The
|
|
357
363
|
// broker owns the bounded result, so absorb that late settlement.
|
|
358
364
|
void execution.catch(() => undefined);
|
|
@@ -372,7 +378,8 @@ export class WorkerBroker {
|
|
|
372
378
|
return await Promise.race([execution, aborted]);
|
|
373
379
|
} finally {
|
|
374
380
|
removeAbortRace();
|
|
375
|
-
|
|
381
|
+
accepting = false;
|
|
382
|
+
clearTimeout(timer);
|
|
376
383
|
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
377
384
|
}
|
|
378
385
|
}
|
|
@@ -452,10 +459,10 @@ export class WorkerBroker {
|
|
|
452
459
|
).length;
|
|
453
460
|
const attempt = evidence.length + 1;
|
|
454
461
|
emitBrokerActivity(input.onActivity, {
|
|
455
|
-
state: "
|
|
462
|
+
state: "preparing",
|
|
456
463
|
attempt,
|
|
457
464
|
maxAttempts,
|
|
458
|
-
wait: "
|
|
465
|
+
wait: "worker-setup",
|
|
459
466
|
});
|
|
460
467
|
try {
|
|
461
468
|
const value = await this.#attempt(
|
|
@@ -545,6 +552,7 @@ export class WorkerBroker {
|
|
|
545
552
|
code:
|
|
546
553
|
| "approval-boundary"
|
|
547
554
|
| "route-not-declared"
|
|
555
|
+
| "endpoint-unavailable"
|
|
548
556
|
| "route-capability-insufficient";
|
|
549
557
|
} {
|
|
550
558
|
const allowed = new Set(["runId", "role", "routeId", "requirements"]);
|
|
@@ -566,12 +574,15 @@ export class WorkerBroker {
|
|
|
566
574
|
routeId: input.routeId,
|
|
567
575
|
});
|
|
568
576
|
if (!selection.ok) {
|
|
577
|
+
const route = this.#policy.routes[input.routeId];
|
|
569
578
|
return {
|
|
570
579
|
ok: false,
|
|
571
580
|
code:
|
|
572
581
|
selection.code === "route-not-declared"
|
|
573
582
|
? "route-not-declared"
|
|
574
|
-
:
|
|
583
|
+
: route && capable(route, input.role, input.requirements ?? {})
|
|
584
|
+
? "endpoint-unavailable"
|
|
585
|
+
: "route-capability-insufficient",
|
|
575
586
|
};
|
|
576
587
|
}
|
|
577
588
|
return {
|
|
@@ -606,7 +617,8 @@ export class WorkerBroker {
|
|
|
606
617
|
}
|
|
607
618
|
|
|
608
619
|
/**
|
|
609
|
-
* Adds
|
|
620
|
+
* Adds run/role defaults and independent task bindings to route attempts.
|
|
621
|
+
* All bound brokers consult the same health store.
|
|
610
622
|
* Attempt credentials and provider objects remain disposable and are never
|
|
611
623
|
* retained by this facade.
|
|
612
624
|
*/
|
|
@@ -622,16 +634,21 @@ export class RunWorkerBroker {
|
|
|
622
634
|
|
|
623
635
|
constructor(policy: RoutePolicy, options: WorkerBrokerOptions = {}) {
|
|
624
636
|
this.#policy = structuredClone(policy);
|
|
625
|
-
this.#options =
|
|
626
|
-
|
|
637
|
+
this.#options = {
|
|
638
|
+
...options,
|
|
639
|
+
healthStore: options.healthStore ?? new MemoryRouteHealthStore(),
|
|
640
|
+
};
|
|
641
|
+
this.#defaultBroker = new WorkerBroker(this.#policy, this.#options);
|
|
627
642
|
}
|
|
628
643
|
|
|
629
|
-
#bindingKey(runId: string, role: WorkerRole): string {
|
|
630
|
-
return
|
|
644
|
+
#bindingKey(runId: string, role: WorkerRole, taskId?: string): string {
|
|
645
|
+
return JSON.stringify([runId, role, taskId ?? null]);
|
|
631
646
|
}
|
|
632
647
|
|
|
633
|
-
#brokerFor(runId: string, role: WorkerRole): WorkerBroker {
|
|
634
|
-
const binding =
|
|
648
|
+
#brokerFor(runId: string, role: WorkerRole, taskId?: string): WorkerBroker {
|
|
649
|
+
const binding =
|
|
650
|
+
this.#bindings.get(this.#bindingKey(runId, role, taskId)) ??
|
|
651
|
+
this.#bindings.get(this.#bindingKey(runId, role));
|
|
635
652
|
if (!binding) return this.#defaultBroker;
|
|
636
653
|
const routeId = binding.routeId;
|
|
637
654
|
const key = `${role}\0${routeId}`;
|
|
@@ -657,6 +674,8 @@ export class RunWorkerBroker {
|
|
|
657
674
|
if (
|
|
658
675
|
typeof input.runId !== "string" ||
|
|
659
676
|
input.runId.length === 0 ||
|
|
677
|
+
(input.taskId !== undefined &&
|
|
678
|
+
(typeof input.taskId !== "string" || input.taskId.length === 0)) ||
|
|
660
679
|
typeof input.operationId !== "string" ||
|
|
661
680
|
input.operationId.length === 0
|
|
662
681
|
) {
|
|
@@ -667,7 +686,11 @@ export class RunWorkerBroker {
|
|
|
667
686
|
attempts: [],
|
|
668
687
|
};
|
|
669
688
|
}
|
|
670
|
-
const result = await this.#brokerFor(
|
|
689
|
+
const result = await this.#brokerFor(
|
|
690
|
+
input.runId,
|
|
691
|
+
input.role,
|
|
692
|
+
input.taskId,
|
|
693
|
+
).run({
|
|
671
694
|
operationId: input.operationId,
|
|
672
695
|
role: input.role,
|
|
673
696
|
...(input.requirements ? { requirements: input.requirements } : {}),
|
|
@@ -679,39 +702,54 @@ export class RunWorkerBroker {
|
|
|
679
702
|
if (result.ok) {
|
|
680
703
|
const route = this.#policy.routes[result.routeId];
|
|
681
704
|
if (!route) throw new Error("broker-route-result-invalid");
|
|
682
|
-
this.#bindings.set(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
705
|
+
this.#bindings.set(
|
|
706
|
+
this.#bindingKey(input.runId, input.role, input.taskId),
|
|
707
|
+
{
|
|
708
|
+
routeId: route.id,
|
|
709
|
+
fingerprint: route.fingerprint,
|
|
710
|
+
},
|
|
711
|
+
);
|
|
686
712
|
}
|
|
687
713
|
return result;
|
|
688
714
|
}
|
|
689
715
|
|
|
690
716
|
rebind(input: {
|
|
691
717
|
runId: string;
|
|
718
|
+
taskId?: string;
|
|
692
719
|
role: WorkerRole;
|
|
693
720
|
routeId: string;
|
|
694
721
|
requirements?: RouteRequirements;
|
|
695
722
|
}) {
|
|
696
|
-
const
|
|
723
|
+
const { taskId, ...routeInput } = input;
|
|
724
|
+
if (
|
|
725
|
+
taskId !== undefined &&
|
|
726
|
+
(typeof taskId !== "string" || taskId.length === 0)
|
|
727
|
+
) {
|
|
728
|
+
return { ok: false as const, code: "approval-boundary" as const };
|
|
729
|
+
}
|
|
730
|
+
const result = this.#defaultBroker.rebind(routeInput);
|
|
697
731
|
if (result.ok) {
|
|
698
|
-
this.#bindings.set(
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
732
|
+
this.#bindings.set(
|
|
733
|
+
this.#bindingKey(input.runId, input.role, input.taskId),
|
|
734
|
+
{
|
|
735
|
+
routeId: result.route.id,
|
|
736
|
+
fingerprint: result.route.fingerprint,
|
|
737
|
+
},
|
|
738
|
+
);
|
|
702
739
|
}
|
|
703
740
|
return result;
|
|
704
741
|
}
|
|
705
742
|
|
|
706
743
|
resumeBinding(input: {
|
|
707
744
|
runId: string;
|
|
745
|
+
taskId?: string;
|
|
708
746
|
role: WorkerRole;
|
|
709
747
|
routeId: string;
|
|
710
748
|
expectedFingerprint?: string;
|
|
711
749
|
requirements?: RouteRequirements;
|
|
712
750
|
}) {
|
|
713
751
|
const binding = this.#bindings.get(
|
|
714
|
-
this.#bindingKey(input.runId, input.role),
|
|
752
|
+
this.#bindingKey(input.runId, input.role, input.taskId),
|
|
715
753
|
);
|
|
716
754
|
const route = this.#policy.routes[input.routeId];
|
|
717
755
|
if (!binding) {
|
|
@@ -724,6 +762,7 @@ export class RunWorkerBroker {
|
|
|
724
762
|
}
|
|
725
763
|
return this.rebind({
|
|
726
764
|
runId: input.runId,
|
|
765
|
+
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
|
|
727
766
|
role: input.role,
|
|
728
767
|
routeId: input.routeId,
|
|
729
768
|
...(input.requirements ? { requirements: input.requirements } : {}),
|
|
@@ -751,8 +790,12 @@ export class RunWorkerBroker {
|
|
|
751
790
|
this.#boundBrokers.clear();
|
|
752
791
|
}
|
|
753
792
|
|
|
754
|
-
binding(
|
|
755
|
-
|
|
793
|
+
binding(
|
|
794
|
+
runId: string,
|
|
795
|
+
role: WorkerRole,
|
|
796
|
+
taskId?: string,
|
|
797
|
+
): string | undefined {
|
|
798
|
+
return this.#bindings.get(this.#bindingKey(runId, role, taskId))?.routeId;
|
|
756
799
|
}
|
|
757
800
|
|
|
758
801
|
status(): Record<string, unknown> {
|