@abelxiaoxing/cadence 1.4.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 +58 -4
- 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 +5 -3
- package/prompts/abel-design.md +93 -44
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +293 -326
- package/src/contracts.ts +184 -76
- package/src/delivery-compiler.ts +103 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -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/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +32 -0
- package/src/worker-broker.ts +37 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-worker.mjs +7 -0
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/subagent-activity.ts
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
PacketActivityEvent,
|
|
14
14
|
PacketActivityObserver,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
} from "./activity-contracts.ts";
|
|
16
|
+
import {
|
|
17
|
+
projectDesignDiagnostic,
|
|
18
|
+
type SafeDesignDiagnostic,
|
|
19
|
+
} from "./design-diagnostics.ts";
|
|
17
20
|
|
|
18
21
|
export const ACTIVITY_DETAILS_KEY = "activityDisplay" as const;
|
|
19
22
|
export const ACTIVITY_WIDGET_KEY = "abel-subagents" as const;
|
|
@@ -22,51 +25,20 @@ export const ACTIVITY_REFRESH_MS = 100;
|
|
|
22
25
|
export const ACTIVITY_WIDGET_MAX_LINES = 12;
|
|
23
26
|
export const ACTIVITY_WIDGET_MIN_WIDTH = 3;
|
|
24
27
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"rejected",
|
|
40
|
-
"completed",
|
|
41
|
-
] as const;
|
|
42
|
-
|
|
43
|
-
export type WorkflowActivityState = (typeof WORKFLOW_ACTIVITY_STATES)[number];
|
|
44
|
-
export type ActivityState = PacketActivityState | WorkflowActivityState;
|
|
45
|
-
export type TerminalActivityState =
|
|
46
|
-
| Exclude<
|
|
47
|
-
PacketActivityState,
|
|
48
|
-
| "queued"
|
|
49
|
-
| "connecting"
|
|
50
|
-
| "waiting-first-response"
|
|
51
|
-
| "running"
|
|
52
|
-
| "retrying"
|
|
53
|
-
>
|
|
54
|
-
| WorkflowActivityState;
|
|
55
|
-
|
|
56
|
-
export interface WorkflowActivityUpdate {
|
|
57
|
-
state: WorkflowActivityState;
|
|
58
|
-
stage?: "abel-design" | "abel-implement";
|
|
59
|
-
runId?: string;
|
|
60
|
-
change?: string;
|
|
61
|
-
taskId?: string;
|
|
62
|
-
phase?: string;
|
|
63
|
-
objective?: string;
|
|
64
|
-
code?: string;
|
|
65
|
-
attempt?: number;
|
|
66
|
-
maxAttempts?: number;
|
|
67
|
-
wait?: string;
|
|
68
|
-
legalCommands?: string[];
|
|
69
|
-
}
|
|
28
|
+
export type {
|
|
29
|
+
ActivityState,
|
|
30
|
+
TerminalActivityState,
|
|
31
|
+
WorkflowActivityState,
|
|
32
|
+
WorkflowActivityUpdate,
|
|
33
|
+
} from "./activity-contracts.ts";
|
|
34
|
+
export { WORKFLOW_ACTIVITY_STATES } from "./activity-contracts.ts";
|
|
35
|
+
|
|
36
|
+
import type {
|
|
37
|
+
ActivityState,
|
|
38
|
+
TerminalActivityState,
|
|
39
|
+
WorkflowActivityState,
|
|
40
|
+
WorkflowActivityUpdate,
|
|
41
|
+
} from "./activity-contracts.ts";
|
|
70
42
|
|
|
71
43
|
export interface EvidenceActivitySummary {
|
|
72
44
|
kind: "evidence";
|
|
@@ -224,7 +196,7 @@ export function sanitizeFailureReason(value: unknown, maxLength = 240): string {
|
|
|
224
196
|
function safeDesignFailure(value: unknown): {
|
|
225
197
|
operation: string;
|
|
226
198
|
code: string;
|
|
227
|
-
diagnostics:
|
|
199
|
+
diagnostics: SafeDesignDiagnostic[];
|
|
228
200
|
} | null {
|
|
229
201
|
const failure = asRecord(value);
|
|
230
202
|
if (
|
|
@@ -237,23 +209,9 @@ function safeDesignFailure(value: unknown): {
|
|
|
237
209
|
) {
|
|
238
210
|
return null;
|
|
239
211
|
}
|
|
240
|
-
const diagnostics = failure.diagnostics.flatMap((candidate) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
!record ||
|
|
244
|
-
typeof record.code !== "string" ||
|
|
245
|
-
!SAFE_DESIGN_DETAIL_VALUE.test(record.code)
|
|
246
|
-
) {
|
|
247
|
-
return [];
|
|
248
|
-
}
|
|
249
|
-
return [
|
|
250
|
-
Object.fromEntries(
|
|
251
|
-
Object.entries(record).filter(
|
|
252
|
-
([, entry]) =>
|
|
253
|
-
typeof entry === "string" && SAFE_DESIGN_DETAIL_VALUE.test(entry),
|
|
254
|
-
),
|
|
255
|
-
) as Record<string, string>,
|
|
256
|
-
];
|
|
212
|
+
const diagnostics = failure.diagnostics.slice(0, 64).flatMap((candidate) => {
|
|
213
|
+
const diagnostic = projectDesignDiagnostic(candidate);
|
|
214
|
+
return diagnostic ? [diagnostic] : [];
|
|
257
215
|
});
|
|
258
216
|
return {
|
|
259
217
|
operation: failure.operation,
|
|
@@ -286,7 +244,10 @@ function renderDesignFailure(
|
|
|
286
244
|
const lines = failure.diagnostics.map((diagnostic) => {
|
|
287
245
|
const details = Object.entries(diagnostic)
|
|
288
246
|
.filter(([key]) => key !== "code")
|
|
289
|
-
.map(
|
|
247
|
+
.map(
|
|
248
|
+
([key, value]) =>
|
|
249
|
+
`${key}=${Array.isArray(value) ? JSON.stringify(value) : value}`,
|
|
250
|
+
)
|
|
290
251
|
.join(" · ");
|
|
291
252
|
return ` - ${diagnostic.code}${details ? ` · ${details}` : ""}`;
|
|
292
253
|
});
|
|
@@ -315,6 +276,7 @@ function stateGlyph(state: ActivityState, spinnerFrame = 0): string {
|
|
|
315
276
|
switch (state) {
|
|
316
277
|
case "queued":
|
|
317
278
|
return "…";
|
|
279
|
+
case "preparing":
|
|
318
280
|
case "connecting":
|
|
319
281
|
case "waiting-first-response":
|
|
320
282
|
case "running":
|
|
@@ -563,6 +525,8 @@ function workflowStateOf(
|
|
|
563
525
|
case "ready":
|
|
564
526
|
case "queued":
|
|
565
527
|
return "queued";
|
|
528
|
+
case "preparing":
|
|
529
|
+
return "preparing";
|
|
566
530
|
case "connecting":
|
|
567
531
|
return "connecting";
|
|
568
532
|
case "waiting-first-response":
|
|
@@ -681,6 +645,7 @@ function workflowWait(
|
|
|
681
645
|
) {
|
|
682
646
|
return approval.gate;
|
|
683
647
|
}
|
|
648
|
+
if (state === "preparing") return "worker-setup";
|
|
684
649
|
if (state === "connecting") return "connection";
|
|
685
650
|
if (state === "waiting-first-response") return "first-response";
|
|
686
651
|
if (state === "retrying") return "bounded-policy";
|
|
@@ -867,6 +832,7 @@ function terminalState(
|
|
|
867
832
|
} {
|
|
868
833
|
return ![
|
|
869
834
|
"queued",
|
|
835
|
+
"preparing",
|
|
870
836
|
"connecting",
|
|
871
837
|
"waiting-first-response",
|
|
872
838
|
"running",
|
package/src/submit-tool.ts
CHANGED
|
@@ -17,65 +17,16 @@ import {
|
|
|
17
17
|
validateDiffResult,
|
|
18
18
|
validateEvidenceResult,
|
|
19
19
|
} from "./contracts.ts";
|
|
20
|
+
import {
|
|
21
|
+
completeEvidenceDraft,
|
|
22
|
+
evidenceDraftSchema,
|
|
23
|
+
} from "./evidence-draft.ts";
|
|
20
24
|
import {
|
|
21
25
|
type BeginCandidateInput,
|
|
22
26
|
TASK_LEDGER_LIMITS,
|
|
23
27
|
type TaskLedger,
|
|
24
28
|
} from "./task-ledger.ts";
|
|
25
29
|
|
|
26
|
-
const compactEvidenceSchema = Type.Object({
|
|
27
|
-
id: Type.String(),
|
|
28
|
-
role: Type.String(),
|
|
29
|
-
kind: Type.Literal("evidence"),
|
|
30
|
-
conclusions: Type.Array(Type.String()),
|
|
31
|
-
citations: Type.Array(
|
|
32
|
-
Type.Object({ path: Type.String(), lines: Type.String() }),
|
|
33
|
-
),
|
|
34
|
-
constraints: Type.Array(Type.String()),
|
|
35
|
-
dependencies: Type.Array(Type.String()),
|
|
36
|
-
risks: Type.Array(Type.String()),
|
|
37
|
-
blockingQuestions: Type.Array(Type.String()),
|
|
38
|
-
hints: Type.Object({
|
|
39
|
-
writeSet: Type.Array(Type.String()),
|
|
40
|
-
verification: Type.String(),
|
|
41
|
-
agentsImpact: Type.String(),
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const designEvidenceSchema = Type.Object(
|
|
46
|
-
{
|
|
47
|
-
id: Type.String(),
|
|
48
|
-
role: Type.Literal("design-explorer"),
|
|
49
|
-
kind: Type.Literal("evidence"),
|
|
50
|
-
packet_id: Type.String(),
|
|
51
|
-
module_name: Type.String(),
|
|
52
|
-
scope: Type.Array(Type.String()),
|
|
53
|
-
files_read: Type.Array(Type.String()),
|
|
54
|
-
evidence: Type.Array(
|
|
55
|
-
Type.Object(
|
|
56
|
-
{
|
|
57
|
-
claim: Type.String(),
|
|
58
|
-
path: Type.String(),
|
|
59
|
-
line_start: Type.Integer({ minimum: 1 }),
|
|
60
|
-
line_end: Type.Integer({ minimum: 1 }),
|
|
61
|
-
},
|
|
62
|
-
{ additionalProperties: false },
|
|
63
|
-
),
|
|
64
|
-
),
|
|
65
|
-
existing_structures: Type.Array(Type.String()),
|
|
66
|
-
existing_conventions: Type.Array(Type.String()),
|
|
67
|
-
constraints_discovered: Type.Array(Type.String()),
|
|
68
|
-
open_questions: Type.Array(Type.String()),
|
|
69
|
-
dependencies: Type.Array(Type.String()),
|
|
70
|
-
write_set_hints: Type.Array(Type.String()),
|
|
71
|
-
validation_hints: Type.Array(Type.String()),
|
|
72
|
-
agents_impact_hints: Type.Array(Type.String()),
|
|
73
|
-
risks: Type.Array(Type.String()),
|
|
74
|
-
success_criteria_hints: Type.Array(Type.String()),
|
|
75
|
-
},
|
|
76
|
-
{ additionalProperties: false },
|
|
77
|
-
);
|
|
78
|
-
|
|
79
30
|
const diffSchema = Type.Object({
|
|
80
31
|
id: Type.String(),
|
|
81
32
|
role: Type.String(),
|
|
@@ -881,13 +832,11 @@ export function createSubmitTool(input: {
|
|
|
881
832
|
name: "abel_submit_result",
|
|
882
833
|
label: "Submit Abel Result",
|
|
883
834
|
description:
|
|
884
|
-
"Submit
|
|
835
|
+
"Submit one accepted final result. Evidence identity and omitted advisory fields are completed by this tool; keep citations, constraints, risks, and open questions explicit. After a rejection, correct once in this same session. Brief accompanying text is harmless.",
|
|
885
836
|
executionMode: "sequential",
|
|
886
837
|
parameters:
|
|
887
838
|
input.output === "evidence"
|
|
888
|
-
? input.role
|
|
889
|
-
? designEvidenceSchema
|
|
890
|
-
: compactEvidenceSchema
|
|
839
|
+
? evidenceDraftSchema(input.requestId, input.role)
|
|
891
840
|
: diffSchema,
|
|
892
841
|
async execute(_toolCallId, params) {
|
|
893
842
|
attempts++;
|
|
@@ -916,7 +865,11 @@ export function createSubmitTool(input: {
|
|
|
916
865
|
phase: true,
|
|
917
866
|
};
|
|
918
867
|
failure = undefined;
|
|
919
|
-
const value =
|
|
868
|
+
const value = (
|
|
869
|
+
input.output === "evidence"
|
|
870
|
+
? completeEvidenceDraft(params, input.requestId, input.role)
|
|
871
|
+
: params
|
|
872
|
+
) as Record<string, unknown>;
|
|
920
873
|
if (value === null || typeof value !== "object") {
|
|
921
874
|
identity.request = false;
|
|
922
875
|
identity.role = false;
|
|
@@ -963,7 +916,15 @@ export function createSubmitTool(input: {
|
|
|
963
916
|
stage: "structural-submit",
|
|
964
917
|
});
|
|
965
918
|
throw new Error(
|
|
966
|
-
|
|
919
|
+
JSON.stringify({
|
|
920
|
+
code: "code" in failure ? failure.code : failure.kind,
|
|
921
|
+
reason:
|
|
922
|
+
validation.reason ?? "submitted result identity does not match",
|
|
923
|
+
identityMismatch: Object.entries(identity)
|
|
924
|
+
.filter(([, matches]) => !matches)
|
|
925
|
+
.map(([field]) => field),
|
|
926
|
+
remainingCorrections: Math.max(0, 2 - attempts),
|
|
927
|
+
}),
|
|
967
928
|
);
|
|
968
929
|
}
|
|
969
930
|
submitted = structuredClone(value) as unknown as
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Request budgets describe observable model progress, never TCP/TLS events. */
|
|
2
|
+
export const REQUEST_BOUNDS = Object.freeze({
|
|
3
|
+
firstProgressMs: 90_000,
|
|
4
|
+
streamIdleMs: 180_000,
|
|
5
|
+
cancellationDrainMs: 250,
|
|
6
|
+
});
|
|
7
|
+
export type TransportTimeoutCode =
|
|
8
|
+
| "first-progress-timeout"
|
|
9
|
+
| "stream-idle-timeout"
|
|
10
|
+
| "attempt-timeout";
|
|
11
|
+
export class TransportTimeout extends Error {
|
|
12
|
+
readonly scope: "request" | "attempt";
|
|
13
|
+
constructor(readonly code: TransportTimeoutCode) {
|
|
14
|
+
super(code);
|
|
15
|
+
this.name = "TransportTimeout";
|
|
16
|
+
this.scope = code === "attempt-timeout" ? "attempt" : "request";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isTransportTimeoutCode(
|
|
20
|
+
code: string,
|
|
21
|
+
): code is TransportTimeoutCode {
|
|
22
|
+
return [
|
|
23
|
+
"first-progress-timeout",
|
|
24
|
+
"stream-idle-timeout",
|
|
25
|
+
"attempt-timeout",
|
|
26
|
+
].includes(code);
|
|
27
|
+
}
|
|
28
|
+
export function transportFailureError(code: string): Error {
|
|
29
|
+
return isTransportTimeoutCode(code)
|
|
30
|
+
? new TransportTimeout(code)
|
|
31
|
+
: new Error("transport-failure");
|
|
32
|
+
}
|
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,
|
|
@@ -71,6 +69,7 @@ export interface RunWorkerAttemptInput<T> {
|
|
|
71
69
|
export interface RouteAttemptContext {
|
|
72
70
|
route: WorkerRoutePolicy;
|
|
73
71
|
signal: AbortSignal;
|
|
72
|
+
onRequestStart(): void;
|
|
74
73
|
onHeaders(): void;
|
|
75
74
|
onProgress(): void;
|
|
76
75
|
}
|
|
@@ -81,7 +80,7 @@ export interface BrokerAttemptEvidence {
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
export interface BrokerActivityUpdate {
|
|
84
|
-
state: "
|
|
83
|
+
state: "preparing" | "waiting-first-response" | "running" | "retrying";
|
|
85
84
|
attempt: number;
|
|
86
85
|
maxAttempts: number;
|
|
87
86
|
code?: string;
|
|
@@ -100,12 +99,7 @@ function emitBrokerActivity(
|
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
class RouteAttemptStop extends Error {
|
|
103
|
-
readonly code:
|
|
104
|
-
| "connect-timeout"
|
|
105
|
-
| "first-response-timeout"
|
|
106
|
-
| "idle-timeout"
|
|
107
|
-
| "phase-timeout"
|
|
108
|
-
| "cancelled";
|
|
102
|
+
readonly code: "cancelled";
|
|
109
103
|
|
|
110
104
|
constructor(code: RouteAttemptStop["code"]) {
|
|
111
105
|
super(code);
|
|
@@ -182,7 +176,8 @@ function capable(
|
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
function safeFailureCode(error: unknown): string {
|
|
185
|
-
if (error instanceof RouteAttemptStop
|
|
179
|
+
if (error instanceof RouteAttemptStop || error instanceof TransportTimeout)
|
|
180
|
+
return error.code;
|
|
186
181
|
return "transport-failure";
|
|
187
182
|
}
|
|
188
183
|
|
|
@@ -331,59 +326,39 @@ export class WorkerBroker {
|
|
|
331
326
|
onActivity?: (state: "waiting-first-response" | "running") => void,
|
|
332
327
|
): Promise<T> {
|
|
333
328
|
const controller = new AbortController();
|
|
334
|
-
|
|
335
|
-
let
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const stop = (code: RouteAttemptStop["code"]) => {
|
|
341
|
-
if (!controller.signal.aborted)
|
|
342
|
-
controller.abort(new RouteAttemptStop(code));
|
|
343
|
-
};
|
|
344
|
-
const schedule = (code: RouteAttemptStop["code"], milliseconds: number) => {
|
|
345
|
-
const timer = setTimeout(() => stop(code), milliseconds);
|
|
346
|
-
timers.add(timer);
|
|
347
|
-
return timer;
|
|
348
|
-
};
|
|
349
|
-
const clear = (timer: ReturnType<typeof setTimeout> | undefined) => {
|
|
350
|
-
if (timer !== undefined) {
|
|
351
|
-
clearTimeout(timer);
|
|
352
|
-
timers.delete(timer);
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
const onParentAbort = () => stop("cancelled");
|
|
356
|
-
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();
|
|
357
334
|
else parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
335
|
+
const timer = setTimeout(
|
|
336
|
+
() => controller.abort(new TransportTimeout("attempt-timeout")),
|
|
337
|
+
ROUTE_ATTEMPT_BOUNDS.totalMs,
|
|
338
|
+
);
|
|
361
339
|
let removeAbortRace: () => void = () => {};
|
|
362
340
|
try {
|
|
363
|
-
const execution = Promise.resolve().then(() =>
|
|
364
|
-
|
|
341
|
+
const execution = Promise.resolve().then(() => {
|
|
342
|
+
controller.signal.throwIfAborted();
|
|
343
|
+
return execute({
|
|
365
344
|
route: structuredClone(route),
|
|
366
345
|
signal: controller.signal,
|
|
346
|
+
onRequestStart: () => {
|
|
347
|
+
if (!accepting || controller.signal.aborted) return;
|
|
348
|
+
running = false;
|
|
349
|
+
onActivity?.("waiting-first-response");
|
|
350
|
+
},
|
|
367
351
|
onHeaders: () => {
|
|
368
|
-
if (controller.signal.aborted ||
|
|
369
|
-
headersSeen = true;
|
|
370
|
-
clear(connectTimer);
|
|
352
|
+
if (!accepting || controller.signal.aborted || running) return;
|
|
371
353
|
onActivity?.("waiting-first-response");
|
|
372
|
-
firstResponseTimer = schedule(
|
|
373
|
-
"first-response-timeout",
|
|
374
|
-
ROUTE_ATTEMPT_BOUNDS.firstResponseMs,
|
|
375
|
-
);
|
|
376
354
|
},
|
|
377
355
|
onProgress: () => {
|
|
378
|
-
if (controller.signal.aborted) return;
|
|
379
|
-
|
|
380
|
-
clear(firstResponseTimer);
|
|
381
|
-
clear(idleTimer);
|
|
382
|
-
idleTimer = schedule("idle-timeout", ROUTE_ATTEMPT_BOUNDS.idleMs);
|
|
356
|
+
if (!accepting || controller.signal.aborted) return;
|
|
357
|
+
running = true;
|
|
383
358
|
onActivity?.("running");
|
|
384
359
|
},
|
|
385
|
-
})
|
|
386
|
-
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
387
362
|
// A timed-out Provider may ignore its AbortSignal and settle later. The
|
|
388
363
|
// broker owns the bounded result, so absorb that late settlement.
|
|
389
364
|
void execution.catch(() => undefined);
|
|
@@ -403,7 +378,8 @@ export class WorkerBroker {
|
|
|
403
378
|
return await Promise.race([execution, aborted]);
|
|
404
379
|
} finally {
|
|
405
380
|
removeAbortRace();
|
|
406
|
-
|
|
381
|
+
accepting = false;
|
|
382
|
+
clearTimeout(timer);
|
|
407
383
|
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
408
384
|
}
|
|
409
385
|
}
|
|
@@ -483,10 +459,10 @@ export class WorkerBroker {
|
|
|
483
459
|
).length;
|
|
484
460
|
const attempt = evidence.length + 1;
|
|
485
461
|
emitBrokerActivity(input.onActivity, {
|
|
486
|
-
state: "
|
|
462
|
+
state: "preparing",
|
|
487
463
|
attempt,
|
|
488
464
|
maxAttempts,
|
|
489
|
-
wait: "
|
|
465
|
+
wait: "worker-setup",
|
|
490
466
|
});
|
|
491
467
|
try {
|
|
492
468
|
const value = await this.#attempt(
|
|
@@ -576,6 +552,7 @@ export class WorkerBroker {
|
|
|
576
552
|
code:
|
|
577
553
|
| "approval-boundary"
|
|
578
554
|
| "route-not-declared"
|
|
555
|
+
| "endpoint-unavailable"
|
|
579
556
|
| "route-capability-insufficient";
|
|
580
557
|
} {
|
|
581
558
|
const allowed = new Set(["runId", "role", "routeId", "requirements"]);
|
|
@@ -597,12 +574,15 @@ export class WorkerBroker {
|
|
|
597
574
|
routeId: input.routeId,
|
|
598
575
|
});
|
|
599
576
|
if (!selection.ok) {
|
|
577
|
+
const route = this.#policy.routes[input.routeId];
|
|
600
578
|
return {
|
|
601
579
|
ok: false,
|
|
602
580
|
code:
|
|
603
581
|
selection.code === "route-not-declared"
|
|
604
582
|
? "route-not-declared"
|
|
605
|
-
:
|
|
583
|
+
: route && capable(route, input.role, input.requirements ?? {})
|
|
584
|
+
? "endpoint-unavailable"
|
|
585
|
+
: "route-capability-insufficient",
|
|
606
586
|
};
|
|
607
587
|
}
|
|
608
588
|
return {
|
package/src/workflow-policy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import type { WorkflowActivityUpdate } from "./activity-contracts.ts";
|
|
4
5
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
5
6
|
import {
|
|
6
7
|
type ApprovalBoundaryCode,
|
|
@@ -18,7 +19,6 @@ import type { RoutePolicy } from "./route-policy.ts";
|
|
|
18
19
|
import { canonicalJson } from "./run-state.ts";
|
|
19
20
|
import type { OperationLease } from "./run-store.ts";
|
|
20
21
|
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
21
|
-
import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
|
|
22
22
|
import {
|
|
23
23
|
type CandidateContextBoundary,
|
|
24
24
|
type CandidateContextRef,
|
|
@@ -185,59 +185,20 @@ export interface RedArtifactCorrection {
|
|
|
185
185
|
contextRequest?: WorkflowContextRequest;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export type
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
allowed: false;
|
|
196
|
-
code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
|
|
197
|
-
};
|
|
188
|
+
export type {
|
|
189
|
+
RecoveryActionDecision,
|
|
190
|
+
RecoveryActionRequest,
|
|
191
|
+
WorkflowRecoveryFact,
|
|
192
|
+
WorkflowRecoveryFeedback,
|
|
193
|
+
} from "./workflow-recovery-policy.ts";
|
|
194
|
+
export { decideRecoveryAction } from "./workflow-recovery-policy.ts";
|
|
198
195
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const correction = request.kind === "red-correction";
|
|
206
|
-
const limit =
|
|
207
|
-
authority.additionalAttempt || authority.verificationOnly
|
|
208
|
-
? 1
|
|
209
|
-
: correction
|
|
210
|
-
? plan.verification.artifactCorrection.maxAttempts
|
|
211
|
-
: plan.verification.repair.maxAttempts;
|
|
212
|
-
return Number.isSafeInteger(request.attempt) &&
|
|
213
|
-
request.attempt > 0 &&
|
|
214
|
-
request.attempt <= limit
|
|
215
|
-
? { allowed: true }
|
|
216
|
-
: {
|
|
217
|
-
allowed: false,
|
|
218
|
-
code: correction
|
|
219
|
-
? "artifact-attempts-exhausted"
|
|
220
|
-
: "repair-attempts-exhausted",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export interface WorkflowRecoveryFeedback {
|
|
225
|
-
code: string;
|
|
226
|
-
attempt: number;
|
|
227
|
-
maxAttempts: number;
|
|
228
|
-
strategy:
|
|
229
|
-
| "revise-candidate"
|
|
230
|
-
| "refresh-candidate"
|
|
231
|
-
| "repair-verification"
|
|
232
|
-
| "compact-patch";
|
|
233
|
-
failureIdentities?: string[];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export interface WorkflowRecoveryFact {
|
|
237
|
-
key: string;
|
|
238
|
-
failures: number;
|
|
239
|
-
feedback: WorkflowRecoveryFeedback;
|
|
240
|
-
}
|
|
196
|
+
import type {
|
|
197
|
+
RecoveryActionDecision,
|
|
198
|
+
RecoveryActionRequest,
|
|
199
|
+
WorkflowRecoveryFact,
|
|
200
|
+
WorkflowRecoveryFeedback,
|
|
201
|
+
} from "./workflow-recovery-policy.ts";
|
|
241
202
|
|
|
242
203
|
export const APPROVAL_AUTHORITY_CATEGORIES = [
|
|
243
204
|
"observable-behavior",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ImplementPlan } from "./implement-plan.ts";
|
|
2
|
+
|
|
3
|
+
export type RecoveryActionRequest = {
|
|
4
|
+
kind: "affected-repair" | "cumulative-repair" | "red-correction";
|
|
5
|
+
attempt: number;
|
|
6
|
+
};
|
|
7
|
+
export type RecoveryActionDecision =
|
|
8
|
+
| { allowed: true }
|
|
9
|
+
| {
|
|
10
|
+
allowed: false;
|
|
11
|
+
code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** One policy for every nested recovery action; execution services supply facts only. */
|
|
15
|
+
export function decideRecoveryAction(
|
|
16
|
+
plan: ImplementPlan,
|
|
17
|
+
request: RecoveryActionRequest,
|
|
18
|
+
authority: { additionalAttempt?: boolean; verificationOnly?: boolean } = {},
|
|
19
|
+
): RecoveryActionDecision {
|
|
20
|
+
const correction = request.kind === "red-correction";
|
|
21
|
+
const limit =
|
|
22
|
+
authority.additionalAttempt || authority.verificationOnly
|
|
23
|
+
? 1
|
|
24
|
+
: correction
|
|
25
|
+
? plan.verification.artifactCorrection.maxAttempts
|
|
26
|
+
: plan.verification.repair.maxAttempts;
|
|
27
|
+
return Number.isSafeInteger(request.attempt) &&
|
|
28
|
+
request.attempt > 0 &&
|
|
29
|
+
request.attempt <= limit
|
|
30
|
+
? { allowed: true }
|
|
31
|
+
: {
|
|
32
|
+
allowed: false,
|
|
33
|
+
code: correction
|
|
34
|
+
? "artifact-attempts-exhausted"
|
|
35
|
+
: "repair-attempts-exhausted",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkflowRecoveryFeedback {
|
|
40
|
+
code: string;
|
|
41
|
+
attempt: number;
|
|
42
|
+
maxAttempts: number;
|
|
43
|
+
strategy:
|
|
44
|
+
| "revise-candidate"
|
|
45
|
+
| "refresh-candidate"
|
|
46
|
+
| "repair-verification"
|
|
47
|
+
| "compact-patch";
|
|
48
|
+
failureIdentities?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorkflowRecoveryFact {
|
|
52
|
+
key: string;
|
|
53
|
+
failures: number;
|
|
54
|
+
feedback: WorkflowRecoveryFeedback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function recoveryExhausted(
|
|
58
|
+
incident: WorkflowRecoveryFact | undefined,
|
|
59
|
+
maximum: number,
|
|
60
|
+
granted: boolean,
|
|
61
|
+
): boolean {
|
|
62
|
+
return !granted && incident !== undefined && incident.failures >= maximum;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A grant is a recommendation until the state machine validates and records it in its lease transaction. */
|
|
66
|
+
export function assessRecoveryGrant(input: {
|
|
67
|
+
current: boolean;
|
|
68
|
+
incident?: WorkflowRecoveryFact;
|
|
69
|
+
failureSequence: number;
|
|
70
|
+
requestedSequence: number;
|
|
71
|
+
reason: string;
|
|
72
|
+
previous: { route?: string | null; context?: string | null };
|
|
73
|
+
observed: { route?: string | null; context?: string | null };
|
|
74
|
+
budget?: { used: number; max_work: number };
|
|
75
|
+
}): string | undefined {
|
|
76
|
+
if (
|
|
77
|
+
!input.current ||
|
|
78
|
+
!input.incident ||
|
|
79
|
+
input.incident.failures < input.incident.feedback.maxAttempts ||
|
|
80
|
+
input.failureSequence !== input.requestedSequence
|
|
81
|
+
)
|
|
82
|
+
return "recovery-request-stale";
|
|
83
|
+
if (
|
|
84
|
+
(input.reason === "route-changed" &&
|
|
85
|
+
(!input.previous.route ||
|
|
86
|
+
input.previous.route === input.observed.route)) ||
|
|
87
|
+
(input.reason === "context-extended" &&
|
|
88
|
+
(!input.previous.context ||
|
|
89
|
+
input.previous.context === input.observed.context))
|
|
90
|
+
)
|
|
91
|
+
return "recovery-evidence-unavailable";
|
|
92
|
+
if (!input.budget || input.budget.used >= input.budget.max_work)
|
|
93
|
+
return "change-work-budget-exhausted";
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|