@femtomc/mu-orchestrator 26.2.70 → 26.2.71
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/dist/dag_reconcile.d.ts +62 -0
- package/dist/dag_reconcile.d.ts.map +1 -0
- package/dist/dag_reconcile.js +210 -0
- package/dist/dag_runner.d.ts +41 -0
- package/dist/dag_runner.d.ts.map +1 -1
- package/dist/dag_runner.js +266 -58
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/inter_root_queue_reconcile.d.ts +45 -0
- package/dist/inter_root_queue_reconcile.d.ts.map +1 -0
- package/dist/inter_root_queue_reconcile.js +111 -0
- package/package.json +5 -5
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Issue, type ValidationResult } from "@femtomc/mu-core";
|
|
2
|
+
export type DagRootPhase = "active" | "waiting_review" | "refining" | "done";
|
|
3
|
+
export declare const DAG_ROOT_PHASE_TAG_PREFIX = "loop:";
|
|
4
|
+
export type DagReconcileDecision = {
|
|
5
|
+
kind: "root_final";
|
|
6
|
+
validation: ValidationResult;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "reopen_retryable";
|
|
9
|
+
issue: Issue;
|
|
10
|
+
reason: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "dispatch_leaf";
|
|
13
|
+
issue: Issue;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "dispatch_reviewer";
|
|
16
|
+
issue: Issue;
|
|
17
|
+
round: number;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "enter_waiting_review";
|
|
20
|
+
reason: string;
|
|
21
|
+
round: number;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "review_accept";
|
|
24
|
+
issue: Issue;
|
|
25
|
+
round: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "review_refine";
|
|
28
|
+
issue: Issue;
|
|
29
|
+
round: number;
|
|
30
|
+
reason: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "review_budget_exhausted";
|
|
33
|
+
issue: Issue;
|
|
34
|
+
round: number;
|
|
35
|
+
max_rounds: number;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "resume_active";
|
|
38
|
+
reason: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "repair_deadlock";
|
|
41
|
+
reason: string;
|
|
42
|
+
};
|
|
43
|
+
export type DagReconcileOpts = {
|
|
44
|
+
issues: readonly Issue[];
|
|
45
|
+
rootId: string;
|
|
46
|
+
attemptsByIssueId?: ReadonlyMap<string, number>;
|
|
47
|
+
maxReorchestrationAttempts?: number;
|
|
48
|
+
maxRefineRoundsPerRoot?: number;
|
|
49
|
+
dispatchTags?: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic intra-root reconcile selector.
|
|
53
|
+
*
|
|
54
|
+
* This function is intentionally side-effect free: it inspects one DAG snapshot and computes the
|
|
55
|
+
* next orchestrator action. The caller is responsible for applying effects and then reconciling
|
|
56
|
+
* again against a refreshed snapshot.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DAG_RECONCILE_ENGINE_INVARIANTS: readonly ["ORCH-DAG-RECON-001: one snapshot in -> one deterministic next action out.", "ORCH-DAG-RECON-002: retryable reopen decisions are budget-aware and derived from DAG primitives.", "ORCH-DAG-RECON-003: root finality is checked before dispatch in non-retry paths.", "ORCH-DAG-RECON-004: leaf dispatch selection is delegated to deterministic core ready-leaf primitives.", "ORCH-DAG-RECON-005: review loop is explicit: active -> waiting_review -> (done | refining).", "ORCH-DAG-RECON-006: refine loops are deterministically budgeted per root."];
|
|
59
|
+
export declare function rootPhaseFromTags(tags: readonly string[]): DagRootPhase;
|
|
60
|
+
export declare function withRootPhaseTag(tags: readonly string[], phase: DagRootPhase): string[];
|
|
61
|
+
export declare function reconcileDagTurn(opts: DagReconcileOpts): DagReconcileDecision;
|
|
62
|
+
//# sourceMappingURL=dag_reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dag_reconcile.d.ts","sourceRoot":"","sources":["../src/dag_reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,KAAK,EAAE,KAAK,gBAAgB,EAAe,MAAM,kBAAkB,CAAC;AAEvH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,gBAAgB,GAAG,UAAU,GAAG,MAAM,CAAC;AAE7E,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAUjD,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,yBAAyB,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACpF;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,qiBAOlC,CAAC;AAqEX,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,CAWvE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,EAAE,CAGvF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,oBAAoB,CA0H7E"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readyLeaves, retryableDagCandidates, validateDag } from "@femtomc/mu-core";
|
|
2
|
+
export const DAG_ROOT_PHASE_TAG_PREFIX = "loop:";
|
|
3
|
+
const ROOT_PHASES = new Set(["active", "waiting_review", "refining", "done"]);
|
|
4
|
+
const REVIEWER_ROLE_TAG = "role:reviewer";
|
|
5
|
+
const REVIEW_ROUND_TAG_PREFIX = "review:round:";
|
|
6
|
+
const ACCEPT_OUTCOMES = new Set(["accept", "success"]);
|
|
7
|
+
const REFINE_OUTCOMES = new Set(["failure", "needs_work", "refine"]);
|
|
8
|
+
const DEFAULT_DISPATCH_TAGS = ["node:agent"];
|
|
9
|
+
const DEFAULT_MAX_REFINE_ROUNDS = 3;
|
|
10
|
+
/**
|
|
11
|
+
* Deterministic intra-root reconcile selector.
|
|
12
|
+
*
|
|
13
|
+
* This function is intentionally side-effect free: it inspects one DAG snapshot and computes the
|
|
14
|
+
* next orchestrator action. The caller is responsible for applying effects and then reconciling
|
|
15
|
+
* again against a refreshed snapshot.
|
|
16
|
+
*/
|
|
17
|
+
export const DAG_RECONCILE_ENGINE_INVARIANTS = [
|
|
18
|
+
"ORCH-DAG-RECON-001: one snapshot in -> one deterministic next action out.",
|
|
19
|
+
"ORCH-DAG-RECON-002: retryable reopen decisions are budget-aware and derived from DAG primitives.",
|
|
20
|
+
"ORCH-DAG-RECON-003: root finality is checked before dispatch in non-retry paths.",
|
|
21
|
+
"ORCH-DAG-RECON-004: leaf dispatch selection is delegated to deterministic core ready-leaf primitives.",
|
|
22
|
+
"ORCH-DAG-RECON-005: review loop is explicit: active -> waiting_review -> (done | refining).",
|
|
23
|
+
"ORCH-DAG-RECON-006: refine loops are deterministically budgeted per root.",
|
|
24
|
+
];
|
|
25
|
+
function normalizeOutcome(outcome) {
|
|
26
|
+
if (typeof outcome !== "string") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const normalized = outcome.trim().toLowerCase();
|
|
30
|
+
return normalized.length > 0 ? normalized : null;
|
|
31
|
+
}
|
|
32
|
+
function compareByCreatedThenId(a, b) {
|
|
33
|
+
if (a.created_at !== b.created_at) {
|
|
34
|
+
return a.created_at - b.created_at;
|
|
35
|
+
}
|
|
36
|
+
return a.id.localeCompare(b.id);
|
|
37
|
+
}
|
|
38
|
+
function reviewRound(issue) {
|
|
39
|
+
for (const tag of issue.tags) {
|
|
40
|
+
if (!tag.startsWith(REVIEW_ROUND_TAG_PREFIX)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const raw = tag.slice(REVIEW_ROUND_TAG_PREFIX.length).trim();
|
|
44
|
+
if (!/^\d+$/.test(raw)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return Math.max(1, Number.parseInt(raw, 10));
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function compareReviewerIssues(a, b) {
|
|
52
|
+
const ar = reviewRound(a);
|
|
53
|
+
const br = reviewRound(b);
|
|
54
|
+
if (ar != null || br != null) {
|
|
55
|
+
if (ar == null) {
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
if (br == null) {
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if (ar !== br) {
|
|
62
|
+
return ar - br;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return compareByCreatedThenId(a, b);
|
|
66
|
+
}
|
|
67
|
+
function hasParentDep(issue, parentId) {
|
|
68
|
+
return issue.deps.some((dep) => dep.type === "parent" && dep.target === parentId);
|
|
69
|
+
}
|
|
70
|
+
function isReviewerIssue(issue) {
|
|
71
|
+
return issue.tags.includes(REVIEWER_ROLE_TAG);
|
|
72
|
+
}
|
|
73
|
+
function reviewerIssuesForRoot(issues, rootId) {
|
|
74
|
+
return issues
|
|
75
|
+
.filter((issue) => isReviewerIssue(issue) && hasParentDep(issue, rootId))
|
|
76
|
+
.sort(compareReviewerIssues);
|
|
77
|
+
}
|
|
78
|
+
function normalizeMaxRefineRounds(value) {
|
|
79
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
80
|
+
return Math.max(1, Math.trunc(value));
|
|
81
|
+
}
|
|
82
|
+
return DEFAULT_MAX_REFINE_ROUNDS;
|
|
83
|
+
}
|
|
84
|
+
export function rootPhaseFromTags(tags) {
|
|
85
|
+
for (const tag of tags) {
|
|
86
|
+
if (!tag.startsWith(DAG_ROOT_PHASE_TAG_PREFIX)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const candidate = tag.slice(DAG_ROOT_PHASE_TAG_PREFIX.length);
|
|
90
|
+
if (ROOT_PHASES.has(candidate)) {
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return "active";
|
|
95
|
+
}
|
|
96
|
+
export function withRootPhaseTag(tags, phase) {
|
|
97
|
+
const withoutPhase = tags.filter((tag) => !tag.startsWith(DAG_ROOT_PHASE_TAG_PREFIX));
|
|
98
|
+
return [...withoutPhase, `${DAG_ROOT_PHASE_TAG_PREFIX}${phase}`];
|
|
99
|
+
}
|
|
100
|
+
export function reconcileDagTurn(opts) {
|
|
101
|
+
const root = opts.issues.find((issue) => issue.id === opts.rootId) ?? null;
|
|
102
|
+
const rootPhase = rootPhaseFromTags(root?.tags ?? []);
|
|
103
|
+
const reviewerIssues = reviewerIssuesForRoot(opts.issues, opts.rootId);
|
|
104
|
+
const latestReviewer = reviewerIssues.length > 0 ? reviewerIssues[reviewerIssues.length - 1] : null;
|
|
105
|
+
const latestReviewRound = latestReviewer ? (reviewRound(latestReviewer) ?? reviewerIssues.length) : 0;
|
|
106
|
+
const nextReviewRound = latestReviewRound > 0 ? latestReviewRound + 1 : 1;
|
|
107
|
+
const maxRefineRounds = normalizeMaxRefineRounds(opts.maxRefineRoundsPerRoot);
|
|
108
|
+
const refineRoundsUsed = reviewerIssues.filter((issue) => {
|
|
109
|
+
if (issue.status !== "closed") {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const outcome = normalizeOutcome(issue.outcome);
|
|
113
|
+
return outcome != null && REFINE_OUTCOMES.has(outcome);
|
|
114
|
+
}).length;
|
|
115
|
+
if (rootPhase === "refining") {
|
|
116
|
+
return {
|
|
117
|
+
kind: "resume_active",
|
|
118
|
+
reason: "review_requested_refinement",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (rootPhase === "waiting_review") {
|
|
122
|
+
if (!latestReviewer) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "enter_waiting_review",
|
|
125
|
+
reason: "reviewer_missing",
|
|
126
|
+
round: nextReviewRound,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (latestReviewer.status === "open") {
|
|
130
|
+
return {
|
|
131
|
+
kind: "dispatch_reviewer",
|
|
132
|
+
issue: latestReviewer,
|
|
133
|
+
round: latestReviewRound,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (latestReviewer.status === "in_progress") {
|
|
137
|
+
return {
|
|
138
|
+
kind: "repair_deadlock",
|
|
139
|
+
reason: `reviewer in_progress: ${latestReviewer.id}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const outcome = normalizeOutcome(latestReviewer.outcome);
|
|
143
|
+
if (outcome != null && ACCEPT_OUTCOMES.has(outcome)) {
|
|
144
|
+
return {
|
|
145
|
+
kind: "review_accept",
|
|
146
|
+
issue: latestReviewer,
|
|
147
|
+
round: latestReviewRound,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (refineRoundsUsed > maxRefineRounds) {
|
|
151
|
+
return {
|
|
152
|
+
kind: "review_budget_exhausted",
|
|
153
|
+
issue: latestReviewer,
|
|
154
|
+
round: latestReviewRound,
|
|
155
|
+
max_rounds: maxRefineRounds,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
kind: "review_refine",
|
|
160
|
+
issue: latestReviewer,
|
|
161
|
+
round: latestReviewRound,
|
|
162
|
+
reason: `outcome=${outcome ?? "null"}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (rootPhase === "done") {
|
|
166
|
+
const validation = validateDag(opts.issues, opts.rootId);
|
|
167
|
+
if (validation.is_final) {
|
|
168
|
+
return { kind: "root_final", validation };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
kind: "resume_active",
|
|
172
|
+
reason: "done_phase_not_final",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const retryable = retryableDagCandidates(opts.issues, {
|
|
176
|
+
root_id: opts.rootId,
|
|
177
|
+
attempts_by_issue_id: opts.attemptsByIssueId,
|
|
178
|
+
max_attempts: opts.maxReorchestrationAttempts,
|
|
179
|
+
}).filter((entry) => !isReviewerIssue(entry.issue));
|
|
180
|
+
if (retryable.length > 0) {
|
|
181
|
+
const target = retryable[0];
|
|
182
|
+
return {
|
|
183
|
+
kind: "reopen_retryable",
|
|
184
|
+
issue: target.issue,
|
|
185
|
+
reason: target.reason,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const validation = validateDag(opts.issues, opts.rootId);
|
|
189
|
+
if (validation.is_final) {
|
|
190
|
+
return {
|
|
191
|
+
kind: "enter_waiting_review",
|
|
192
|
+
reason: validation.reason,
|
|
193
|
+
round: nextReviewRound,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const ready = readyLeaves(opts.issues, {
|
|
197
|
+
root_id: opts.rootId,
|
|
198
|
+
tags: opts.dispatchTags ?? DEFAULT_DISPATCH_TAGS,
|
|
199
|
+
}).filter((issue) => !isReviewerIssue(issue));
|
|
200
|
+
if (ready.length === 0) {
|
|
201
|
+
return {
|
|
202
|
+
kind: "repair_deadlock",
|
|
203
|
+
reason: validation.reason,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
kind: "dispatch_leaf",
|
|
208
|
+
issue: ready[0],
|
|
209
|
+
};
|
|
210
|
+
}
|
package/dist/dag_runner.d.ts
CHANGED
|
@@ -3,6 +3,31 @@ import { type EventLog } from "@femtomc/mu-core/node";
|
|
|
3
3
|
import type { ForumStore } from "@femtomc/mu-forum";
|
|
4
4
|
import type { IssueStore } from "@femtomc/mu-issue";
|
|
5
5
|
import type { ModelOverrides } from "./model_resolution.js";
|
|
6
|
+
/**
|
|
7
|
+
* Default-on orchestration reconcile contract for `DagRunner`.
|
|
8
|
+
*
|
|
9
|
+
* The orchestration redesign is the default behavior (no feature-flag fork). Compatibility adapters
|
|
10
|
+
* are allowed only if they preserve the same durable state transitions and emitted events.
|
|
11
|
+
*/
|
|
12
|
+
export type DagRunnerReconcilePhase = "turn_start" | "unstick_retryable" | "validate_root" | "select_leaf" | "dispatch_issue" | "repair_deadlock" | "postcondition_reconcile" | "requeue_retryable" | "turn_end";
|
|
13
|
+
/** Review/refinement control loop that reconcile implementations must honor. */
|
|
14
|
+
export type DagRunnerReviewLoopPhase = "plan" | "execute" | "review" | "accept" | "refine";
|
|
15
|
+
/**
|
|
16
|
+
* Enumerated invariants (kept implementation-facing so tests can assert these explicitly).
|
|
17
|
+
*/
|
|
18
|
+
export declare const DAG_RUNNER_CONTRACT_INVARIANTS: readonly ["ORCH-RECON-001: Reconcile is default-on and must not branch behind rollout flags.", "ORCH-RECON-002: At most one issue is claimed+dispatched per reconcile step for a root.", "ORCH-RECON-003: A dispatched issue must end closed; otherwise the runner force-closes with outcome=failure.", "ORCH-RECON-004: failure/needs_work outcomes may be retried only up to the per-issue attempt budget (currently 3).", "ORCH-RECON-005: Root finality check (`validate(root).is_final`) runs before dispatch each step.", "ORCH-RECON-006: Review loop semantics are plan -> execute -> review -> (accept | refine).", "ORCH-RECON-007: Refine loops are budgeted (default max_refine_rounds_per_root=3); exhaustion is terminal.", "ORCH-RECON-008: Compatibility adapters may exist in-place, but must preserve this state machine and events."];
|
|
19
|
+
/** Default refine-loop budget for upcoming reviewer integration modules. */
|
|
20
|
+
export declare const DEFAULT_MAX_REFINE_ROUNDS_PER_ROOT = 3;
|
|
21
|
+
/**
|
|
22
|
+
* Reviewer semantics contract:
|
|
23
|
+
* - accept => review step closes with `success`; root can move to terminal validation
|
|
24
|
+
* - refine => review step closes with `refine` (or legacy `needs_work`), then orchestrator schedules follow-up work
|
|
25
|
+
*/
|
|
26
|
+
export declare const REVIEW_DECISION_TO_OUTCOME: {
|
|
27
|
+
readonly accept: "success";
|
|
28
|
+
readonly refine: "refine";
|
|
29
|
+
};
|
|
30
|
+
export declare const DAG_RUNNER_BUDGET_INVARIANTS: readonly ["ORCH-BUDGET-001: max_steps is a hard upper bound on reconcile turns per run invocation.", "ORCH-BUDGET-002: per-issue re-orchestration attempts are capped at 3.", "ORCH-BUDGET-003: reviewer refine rounds are capped per root (default 3)."];
|
|
6
31
|
export type DagResult = {
|
|
7
32
|
status: "root_final" | "no_executable_leaf" | "max_steps_exhausted" | "error";
|
|
8
33
|
steps: number;
|
|
@@ -49,7 +74,23 @@ export declare class DagRunner {
|
|
|
49
74
|
backend?: BackendRunner;
|
|
50
75
|
events?: EventLog;
|
|
51
76
|
modelOverrides?: ModelOverrides;
|
|
77
|
+
maxRefineRoundsPerRoot?: number;
|
|
52
78
|
});
|
|
79
|
+
/**
|
|
80
|
+
* Reconcile entrypoint for one root DAG.
|
|
81
|
+
*
|
|
82
|
+
* Turn state machine (default path, no rollout flag branch):
|
|
83
|
+
* turn_start
|
|
84
|
+
* -> unstick_retryable
|
|
85
|
+
* -> validate_root
|
|
86
|
+
* -> select_leaf
|
|
87
|
+
* -> (none) repair_deadlock -> turn_end
|
|
88
|
+
* -> (leaf) dispatch_issue -> postcondition_reconcile -> requeue_retryable -> turn_end
|
|
89
|
+
*
|
|
90
|
+
* Reviewer/refinement loop contract:
|
|
91
|
+
* plan -> execute -> review -> (accept | refine)
|
|
92
|
+
* refine -> execute (after orchestrator persists follow-up executable work)
|
|
93
|
+
*/
|
|
53
94
|
run(rootId: string, maxSteps?: number, opts?: DagRunnerRunOpts): Promise<DagResult>;
|
|
54
95
|
}
|
|
55
96
|
export {};
|
package/dist/dag_runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dag_runner.d.ts","sourceRoot":"","sources":["../src/dag_runner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAA8D,MAAM,mBAAmB,CAAC;AAEnH,OAAO,EAEN,KAAK,QAAQ,EAKb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"dag_runner.d.ts","sourceRoot":"","sources":["../src/dag_runner.ts"],"names":[],"mappings":"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"}
|
package/dist/dag_runner.js
CHANGED
|
@@ -2,7 +2,42 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { SdkBackend, roleFromTags, systemPromptForRole } from "@femtomc/mu-agent";
|
|
4
4
|
import { currentRunId, fsEventLogFromRepoRoot, getStorePaths, newRunId, runContext, } from "@femtomc/mu-core/node";
|
|
5
|
+
import { reconcileDagTurn, withRootPhaseTag } from "./dag_reconcile.js";
|
|
5
6
|
import { resolveModelConfig } from "./model_resolution.js";
|
|
7
|
+
/**
|
|
8
|
+
* Enumerated invariants (kept implementation-facing so tests can assert these explicitly).
|
|
9
|
+
*/
|
|
10
|
+
export const DAG_RUNNER_CONTRACT_INVARIANTS = [
|
|
11
|
+
"ORCH-RECON-001: Reconcile is default-on and must not branch behind rollout flags.",
|
|
12
|
+
"ORCH-RECON-002: At most one issue is claimed+dispatched per reconcile step for a root.",
|
|
13
|
+
"ORCH-RECON-003: A dispatched issue must end closed; otherwise the runner force-closes with outcome=failure.",
|
|
14
|
+
"ORCH-RECON-004: failure/needs_work outcomes may be retried only up to the per-issue attempt budget (currently 3).",
|
|
15
|
+
"ORCH-RECON-005: Root finality check (`validate(root).is_final`) runs before dispatch each step.",
|
|
16
|
+
"ORCH-RECON-006: Review loop semantics are plan -> execute -> review -> (accept | refine).",
|
|
17
|
+
"ORCH-RECON-007: Refine loops are budgeted (default max_refine_rounds_per_root=3); exhaustion is terminal.",
|
|
18
|
+
"ORCH-RECON-008: Compatibility adapters may exist in-place, but must preserve this state machine and events.",
|
|
19
|
+
];
|
|
20
|
+
/** Default refine-loop budget for upcoming reviewer integration modules. */
|
|
21
|
+
export const DEFAULT_MAX_REFINE_ROUNDS_PER_ROOT = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Reviewer semantics contract:
|
|
24
|
+
* - accept => review step closes with `success`; root can move to terminal validation
|
|
25
|
+
* - refine => review step closes with `refine` (or legacy `needs_work`), then orchestrator schedules follow-up work
|
|
26
|
+
*/
|
|
27
|
+
export const REVIEW_DECISION_TO_OUTCOME = {
|
|
28
|
+
accept: "success",
|
|
29
|
+
refine: "refine",
|
|
30
|
+
};
|
|
31
|
+
export const DAG_RUNNER_BUDGET_INVARIANTS = [
|
|
32
|
+
"ORCH-BUDGET-001: max_steps is a hard upper bound on reconcile turns per run invocation.",
|
|
33
|
+
"ORCH-BUDGET-002: per-issue re-orchestration attempts are capped at 3.",
|
|
34
|
+
"ORCH-BUDGET-003: reviewer refine rounds are capped per root (default 3).",
|
|
35
|
+
];
|
|
36
|
+
const MAX_REORCHESTRATION_ATTEMPTS = 3;
|
|
37
|
+
const REVIEWER_ROLE_TAG = "role:reviewer";
|
|
38
|
+
const ORCHESTRATOR_ROLE_TAG = "role:orchestrator";
|
|
39
|
+
const REVIEWER_ACCEPT_OUTCOMES = new Set(["accept", "success"]);
|
|
40
|
+
const REVIEWER_REFINE_OUTCOMES = new Set(["failure", "needs_work", "refine"]);
|
|
6
41
|
function roundTo(n, digits) {
|
|
7
42
|
const f = 10 ** digits;
|
|
8
43
|
return Math.round(n * f) / f;
|
|
@@ -23,6 +58,24 @@ function normalizeSelfMetadataValue(value) {
|
|
|
23
58
|
const trimmed = value.trim();
|
|
24
59
|
return trimmed.length > 0 ? trimmed : "unknown";
|
|
25
60
|
}
|
|
61
|
+
function normalizeOutcome(value) {
|
|
62
|
+
if (typeof value !== "string") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const normalized = value.trim().toLowerCase();
|
|
66
|
+
return normalized.length > 0 ? normalized : null;
|
|
67
|
+
}
|
|
68
|
+
function sameTags(a, b) {
|
|
69
|
+
if (a.length !== b.length) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
73
|
+
if (a[i] !== b[i]) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
26
79
|
export function renderAgentSelfMetadata(meta) {
|
|
27
80
|
const model = normalizeSelfMetadataValue(meta.model);
|
|
28
81
|
const thinkingLevel = normalizeSelfMetadataValue(meta.thinkingLevel);
|
|
@@ -35,6 +88,7 @@ export class DagRunner {
|
|
|
35
88
|
#events;
|
|
36
89
|
#backend;
|
|
37
90
|
#modelOverrides;
|
|
91
|
+
#maxRefineRoundsPerRoot;
|
|
38
92
|
#reorchestrateOutcomes = new Set(["failure", "needs_work"]);
|
|
39
93
|
#attempts = new Map();
|
|
40
94
|
constructor(store, forum, repoRoot, opts = {}) {
|
|
@@ -44,6 +98,7 @@ export class DagRunner {
|
|
|
44
98
|
this.#events = opts.events ?? fsEventLogFromRepoRoot(repoRoot);
|
|
45
99
|
this.#backend = opts.backend ?? new SdkBackend();
|
|
46
100
|
this.#modelOverrides = opts.modelOverrides ?? {};
|
|
101
|
+
this.#maxRefineRoundsPerRoot = Math.max(1, Math.trunc(opts.maxRefineRoundsPerRoot ?? DEFAULT_MAX_REFINE_ROUNDS_PER_ROOT));
|
|
47
102
|
}
|
|
48
103
|
async #resolveConfig() {
|
|
49
104
|
return resolveModelConfig(this.#modelOverrides);
|
|
@@ -121,10 +176,14 @@ export class DagRunner {
|
|
|
121
176
|
if (!before) {
|
|
122
177
|
return;
|
|
123
178
|
}
|
|
179
|
+
const tagsWithoutRole = before.tags.filter((t) => !t.startsWith("role:"));
|
|
180
|
+
const tagsWithAgent = tagsWithoutRole.includes("node:agent")
|
|
181
|
+
? tagsWithoutRole
|
|
182
|
+
: [...tagsWithoutRole, "node:agent"];
|
|
124
183
|
const reopened = await this.#store.update(issueId, {
|
|
125
184
|
status: "open",
|
|
126
185
|
outcome: null,
|
|
127
|
-
tags: [...
|
|
186
|
+
tags: [...tagsWithAgent, ORCHESTRATOR_ROLE_TAG],
|
|
128
187
|
});
|
|
129
188
|
await this.#events.emit("dag.unstick.reopen", {
|
|
130
189
|
source: "dag_runner",
|
|
@@ -139,49 +198,72 @@ export class DagRunner {
|
|
|
139
198
|
reason: opts.reason,
|
|
140
199
|
}), "orchestrator");
|
|
141
200
|
}
|
|
142
|
-
async #
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const childrenOf = new Map();
|
|
147
|
-
for (const row of rows) {
|
|
148
|
-
for (const dep of row.deps ?? []) {
|
|
149
|
-
if (dep.type !== "parent")
|
|
150
|
-
continue;
|
|
151
|
-
const list = childrenOf.get(dep.target) ?? [];
|
|
152
|
-
list.push(row);
|
|
153
|
-
childrenOf.set(dep.target, list);
|
|
154
|
-
}
|
|
201
|
+
async #setRootPhase(rootId, phase) {
|
|
202
|
+
const root = await this.#store.get(rootId);
|
|
203
|
+
if (!root) {
|
|
204
|
+
return null;
|
|
155
205
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
continue;
|
|
170
|
-
candidates.push(row);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (outcome === "expanded" && (childrenOf.get(row.id)?.length ?? 0) === 0) {
|
|
174
|
-
candidates.push(row);
|
|
206
|
+
const nextTags = withRootPhaseTag(root.tags, phase);
|
|
207
|
+
if (sameTags(root.tags, nextTags)) {
|
|
208
|
+
return root;
|
|
209
|
+
}
|
|
210
|
+
return await this.#store.update(rootId, { tags: nextTags });
|
|
211
|
+
}
|
|
212
|
+
async #ensureReviewerIssue(rootId, round, step, rows) {
|
|
213
|
+
const hasParent = (issue) => issue.deps.some((dep) => dep.type === "parent" && dep.target === rootId);
|
|
214
|
+
const pending = rows
|
|
215
|
+
.filter((issue) => issue.tags.includes(REVIEWER_ROLE_TAG) && hasParent(issue) && issue.status !== "closed")
|
|
216
|
+
.sort((a, b) => {
|
|
217
|
+
if (a.created_at !== b.created_at) {
|
|
218
|
+
return a.created_at - b.created_at;
|
|
175
219
|
}
|
|
220
|
+
return a.id.localeCompare(b.id);
|
|
221
|
+
})
|
|
222
|
+
.pop();
|
|
223
|
+
if (pending) {
|
|
224
|
+
return pending;
|
|
176
225
|
}
|
|
177
|
-
|
|
178
|
-
|
|
226
|
+
const root = rows.find((row) => row.id === rootId) ?? null;
|
|
227
|
+
if (!root) {
|
|
228
|
+
return null;
|
|
179
229
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
const created = await this.#store.create(`Review round ${round}: ${root.title}`, {
|
|
231
|
+
body: `Review the completed execution for root ${rootId}.\n\n` +
|
|
232
|
+
`Close with outcome=success to accept, or outcome=refine/needs_work to request follow-up worker work.\n` +
|
|
233
|
+
`Do not create child issues; refinement scheduling is orchestrator-owned.`,
|
|
234
|
+
tags: ["node:agent", REVIEWER_ROLE_TAG, `review:round:${round}`],
|
|
235
|
+
priority: Math.max(1, (root.priority ?? 3) - 1),
|
|
236
|
+
});
|
|
237
|
+
await this.#store.add_dep(created.id, "parent", rootId);
|
|
238
|
+
await this.#events.emit("dag.review.issue_created", {
|
|
239
|
+
source: "dag_runner",
|
|
240
|
+
issueId: rootId,
|
|
241
|
+
payload: { root_id: rootId, step, round, review_issue_id: created.id },
|
|
242
|
+
});
|
|
243
|
+
await this.#forum.post(`issue:${rootId}`, JSON.stringify({
|
|
244
|
+
step,
|
|
245
|
+
issue_id: rootId,
|
|
246
|
+
type: "review_requested",
|
|
247
|
+
round,
|
|
248
|
+
review_issue_id: created.id,
|
|
249
|
+
}), "orchestrator");
|
|
250
|
+
return created;
|
|
184
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Reconcile entrypoint for one root DAG.
|
|
254
|
+
*
|
|
255
|
+
* Turn state machine (default path, no rollout flag branch):
|
|
256
|
+
* turn_start
|
|
257
|
+
* -> unstick_retryable
|
|
258
|
+
* -> validate_root
|
|
259
|
+
* -> select_leaf
|
|
260
|
+
* -> (none) repair_deadlock -> turn_end
|
|
261
|
+
* -> (leaf) dispatch_issue -> postcondition_reconcile -> requeue_retryable -> turn_end
|
|
262
|
+
*
|
|
263
|
+
* Reviewer/refinement loop contract:
|
|
264
|
+
* plan -> execute -> review -> (accept | refine)
|
|
265
|
+
* refine -> execute (after orchestrator persists follow-up executable work)
|
|
266
|
+
*/
|
|
185
267
|
async run(rootId, maxSteps = 20, opts = {}) {
|
|
186
268
|
const hooks = opts.hooks;
|
|
187
269
|
const runId = currentRunId() ?? newRunId();
|
|
@@ -195,31 +277,144 @@ export class DagRunner {
|
|
|
195
277
|
try {
|
|
196
278
|
for (let i = 0; i < maxSteps; i++) {
|
|
197
279
|
const step = i + 1;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
280
|
+
const rows = await this.#store.list();
|
|
281
|
+
const decision = reconcileDagTurn({
|
|
282
|
+
issues: rows,
|
|
283
|
+
rootId,
|
|
284
|
+
attemptsByIssueId: this.#attempts,
|
|
285
|
+
maxReorchestrationAttempts: MAX_REORCHESTRATION_ATTEMPTS,
|
|
286
|
+
maxRefineRoundsPerRoot: this.#maxRefineRoundsPerRoot,
|
|
287
|
+
dispatchTags: ["node:agent"],
|
|
288
|
+
});
|
|
289
|
+
if (decision.kind === "reopen_retryable") {
|
|
290
|
+
await this.#reopenForOrchestration(decision.issue.id, {
|
|
291
|
+
reason: decision.reason,
|
|
292
|
+
step,
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (decision.kind === "enter_waiting_review") {
|
|
297
|
+
const phased = await this.#setRootPhase(rootId, "waiting_review");
|
|
298
|
+
if (!phased) {
|
|
299
|
+
final = { status: "error", steps: step, error: "root vanished" };
|
|
300
|
+
return final;
|
|
301
|
+
}
|
|
302
|
+
const reviewIssue = await this.#ensureReviewerIssue(rootId, decision.round, step, rows);
|
|
303
|
+
if (!reviewIssue) {
|
|
304
|
+
final = { status: "error", steps: step, error: "review_issue_create_failed" };
|
|
305
|
+
return final;
|
|
306
|
+
}
|
|
307
|
+
await this.#events.emit("dag.review.waiting", {
|
|
308
|
+
source: "dag_runner",
|
|
309
|
+
issueId: rootId,
|
|
310
|
+
payload: {
|
|
311
|
+
root_id: rootId,
|
|
312
|
+
step,
|
|
313
|
+
round: decision.round,
|
|
314
|
+
review_issue_id: reviewIssue.id,
|
|
315
|
+
reason: decision.reason,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (decision.kind === "review_accept") {
|
|
321
|
+
await this.#setRootPhase(rootId, "done");
|
|
322
|
+
await this.#store.update(rootId, { status: "closed", outcome: "success" });
|
|
323
|
+
await this.#events.emit("dag.review.accept", {
|
|
324
|
+
source: "dag_runner",
|
|
325
|
+
issueId: rootId,
|
|
326
|
+
payload: { root_id: rootId, step, round: decision.round, review_issue_id: decision.issue.id },
|
|
327
|
+
});
|
|
328
|
+
await this.#forum.post(`issue:${rootId}`, JSON.stringify({
|
|
329
|
+
step,
|
|
330
|
+
issue_id: rootId,
|
|
331
|
+
type: "review_accept",
|
|
332
|
+
round: decision.round,
|
|
333
|
+
review_issue_id: decision.issue.id,
|
|
334
|
+
}), "orchestrator");
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (decision.kind === "review_refine") {
|
|
338
|
+
await this.#setRootPhase(rootId, "refining");
|
|
339
|
+
const normalized = normalizeOutcome(decision.issue.outcome);
|
|
340
|
+
if (normalized !== REVIEW_DECISION_TO_OUTCOME.refine) {
|
|
341
|
+
await this.#store.update(decision.issue.id, { outcome: REVIEW_DECISION_TO_OUTCOME.refine });
|
|
342
|
+
}
|
|
343
|
+
await this.#events.emit("dag.review.refine", {
|
|
344
|
+
source: "dag_runner",
|
|
345
|
+
issueId: rootId,
|
|
346
|
+
payload: {
|
|
347
|
+
root_id: rootId,
|
|
348
|
+
step,
|
|
349
|
+
round: decision.round,
|
|
350
|
+
review_issue_id: decision.issue.id,
|
|
351
|
+
reason: decision.reason,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
await this.#forum.post(`issue:${rootId}`, JSON.stringify({
|
|
355
|
+
step,
|
|
356
|
+
issue_id: rootId,
|
|
357
|
+
type: "review_refine",
|
|
358
|
+
round: decision.round,
|
|
359
|
+
review_issue_id: decision.issue.id,
|
|
360
|
+
reason: decision.reason,
|
|
361
|
+
}), "orchestrator");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (decision.kind === "review_budget_exhausted") {
|
|
365
|
+
await this.#setRootPhase(rootId, "done");
|
|
366
|
+
await this.#store.update(rootId, { status: "closed", outcome: "budget_exhausted" });
|
|
367
|
+
await this.#events.emit("dag.review.budget_exhausted", {
|
|
368
|
+
source: "dag_runner",
|
|
369
|
+
issueId: rootId,
|
|
370
|
+
payload: {
|
|
371
|
+
root_id: rootId,
|
|
372
|
+
step,
|
|
373
|
+
round: decision.round,
|
|
374
|
+
max_rounds: decision.max_rounds,
|
|
375
|
+
review_issue_id: decision.issue.id,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
final = {
|
|
379
|
+
status: "error",
|
|
380
|
+
steps: step,
|
|
381
|
+
error: `review_budget_exhausted:${decision.max_rounds}`,
|
|
382
|
+
};
|
|
383
|
+
return final;
|
|
384
|
+
}
|
|
385
|
+
if (decision.kind === "resume_active") {
|
|
386
|
+
await this.#setRootPhase(rootId, "active");
|
|
387
|
+
await this.#reopenForOrchestration(rootId, {
|
|
388
|
+
reason: decision.reason,
|
|
389
|
+
step,
|
|
390
|
+
});
|
|
391
|
+
await this.#events.emit("dag.review.resume_active", {
|
|
392
|
+
source: "dag_runner",
|
|
393
|
+
issueId: rootId,
|
|
394
|
+
payload: { root_id: rootId, step, reason: decision.reason },
|
|
395
|
+
});
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (decision.kind === "root_final") {
|
|
203
399
|
final = { status: "root_final", steps: i, error: "" };
|
|
204
400
|
return final;
|
|
205
401
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (candidates.length === 0) {
|
|
209
|
-
// Repair pass on the root to resolve deadlocks / bad expansions.
|
|
402
|
+
if (decision.kind === "repair_deadlock") {
|
|
403
|
+
// repair_deadlock phase: run orchestrator on root to create executable leaf work.
|
|
210
404
|
await this.#events.emit("dag.unstick.start", {
|
|
211
405
|
source: "dag_runner",
|
|
212
406
|
issueId: rootId,
|
|
213
407
|
payload: { root_id: rootId, step },
|
|
214
408
|
});
|
|
215
|
-
const rootIssue =
|
|
409
|
+
const rootIssue = rows.find((row) => row.id === rootId) ?? null;
|
|
216
410
|
if (!rootIssue) {
|
|
217
411
|
final = { status: "error", steps: i, error: "root vanished" };
|
|
218
412
|
return final;
|
|
219
413
|
}
|
|
220
414
|
const idsInScope = new Set(await this.#store.subtree_ids(rootId));
|
|
221
|
-
const openIssues =
|
|
415
|
+
const openIssues = rows.filter((row) => idsInScope.has(row.id) && row.status === "open");
|
|
222
416
|
const diag = `- open_issues: ${openIssues.length}\n` +
|
|
417
|
+
`- reconcile_reason: ${decision.reason}\n` +
|
|
223
418
|
`- action: diagnose deadlocks or missing expansions and create executable leaf work\n` +
|
|
224
419
|
`- hint: run \`mu issues ready --root ${rootId}\` and \`mu issues list --root ${rootId}\`\n`;
|
|
225
420
|
const repairIssue = {
|
|
@@ -251,7 +446,7 @@ export class DagRunner {
|
|
|
251
446
|
});
|
|
252
447
|
continue;
|
|
253
448
|
}
|
|
254
|
-
const issue =
|
|
449
|
+
const issue = decision.issue;
|
|
255
450
|
const issueId = issue.id;
|
|
256
451
|
const role = roleFromTags(issue.tags);
|
|
257
452
|
await this.#events.emit("dag.step.start", {
|
|
@@ -262,7 +457,7 @@ export class DagRunner {
|
|
|
262
457
|
if (hooks?.onStepStart) {
|
|
263
458
|
await hooks.onStepStart({ rootId, step, issueId, role, title: issue.title ?? "" });
|
|
264
459
|
}
|
|
265
|
-
//
|
|
460
|
+
// dispatch_issue: claim leaf
|
|
266
461
|
await this.#events.emit("dag.claim", {
|
|
267
462
|
source: "dag_runner",
|
|
268
463
|
issueId,
|
|
@@ -272,7 +467,7 @@ export class DagRunner {
|
|
|
272
467
|
// Track attempt count for circuit breaker.
|
|
273
468
|
const attempt = (this.#attempts.get(issueId) ?? 0) + 1;
|
|
274
469
|
this.#attempts.set(issueId, attempt);
|
|
275
|
-
//
|
|
470
|
+
// dispatch_issue: resolve model + execute backend
|
|
276
471
|
const cfg = await this.#resolveConfig();
|
|
277
472
|
const logSuffix = attempt > 1 ? `attempt-${attempt}` : "";
|
|
278
473
|
const onBackendLine = hooks?.onBackendLine;
|
|
@@ -283,7 +478,7 @@ export class DagRunner {
|
|
|
283
478
|
? (line) => onBackendLine({ rootId, step, issueId, logSuffix, line })
|
|
284
479
|
: undefined,
|
|
285
480
|
});
|
|
286
|
-
//
|
|
481
|
+
// postcondition_reconcile
|
|
287
482
|
let updated = await this.#store.get(issueId);
|
|
288
483
|
if (!updated) {
|
|
289
484
|
final = { status: "error", steps: step, error: "issue vanished" };
|
|
@@ -297,7 +492,20 @@ export class DagRunner {
|
|
|
297
492
|
});
|
|
298
493
|
updated = await this.#store.close(issueId, "failure");
|
|
299
494
|
}
|
|
300
|
-
|
|
495
|
+
if (role === "reviewer") {
|
|
496
|
+
const normalized = normalizeOutcome(updated.outcome);
|
|
497
|
+
let canonicalOutcome = null;
|
|
498
|
+
if (normalized && REVIEWER_ACCEPT_OUTCOMES.has(normalized)) {
|
|
499
|
+
canonicalOutcome = REVIEW_DECISION_TO_OUTCOME.accept;
|
|
500
|
+
}
|
|
501
|
+
else if (normalized && REVIEWER_REFINE_OUTCOMES.has(normalized)) {
|
|
502
|
+
canonicalOutcome = REVIEW_DECISION_TO_OUTCOME.refine;
|
|
503
|
+
}
|
|
504
|
+
if (canonicalOutcome && updated.outcome !== canonicalOutcome) {
|
|
505
|
+
updated = await this.#store.update(issueId, { outcome: canonicalOutcome });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// turn_end: persist execution record
|
|
301
509
|
await this.#forum.post(`issue:${issueId}`, JSON.stringify({
|
|
302
510
|
step,
|
|
303
511
|
issue_id: issueId,
|
|
@@ -327,9 +535,9 @@ export class DagRunner {
|
|
|
327
535
|
outcome: updated.outcome,
|
|
328
536
|
},
|
|
329
537
|
});
|
|
330
|
-
//
|
|
331
|
-
if (updated.outcome && this.#reorchestrateOutcomes.has(updated.outcome)) {
|
|
332
|
-
if (attempt <
|
|
538
|
+
// requeue_retryable (bounded by attempt budget)
|
|
539
|
+
if (role !== "reviewer" && updated.outcome && this.#reorchestrateOutcomes.has(updated.outcome)) {
|
|
540
|
+
if (attempt < MAX_REORCHESTRATION_ATTEMPTS) {
|
|
333
541
|
await this.#reopenForOrchestration(issueId, { reason: `outcome=${updated.outcome}`, step });
|
|
334
542
|
}
|
|
335
543
|
else {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export type { DagResult, DagRunnerBackendLineEvent, DagRunnerHooks, DagRunnerRunOpts, DagRunnerStepEndEvent, DagRunnerStepStartEvent, } from "./dag_runner.js";
|
|
2
|
-
export { DagRunner } from "./dag_runner.js";
|
|
1
|
+
export type { DagResult, DagRunnerBackendLineEvent, DagRunnerHooks, DagRunnerReconcilePhase, DagRunnerReviewLoopPhase, DagRunnerRunOpts, DagRunnerStepEndEvent, DagRunnerStepStartEvent, } from "./dag_runner.js";
|
|
2
|
+
export { DAG_RUNNER_BUDGET_INVARIANTS, DAG_RUNNER_CONTRACT_INVARIANTS, DEFAULT_MAX_REFINE_ROUNDS_PER_ROOT, DagRunner, REVIEW_DECISION_TO_OUTCOME, } from "./dag_runner.js";
|
|
3
|
+
export type { DagReconcileDecision, DagReconcileOpts, DagRootPhase } from "./dag_reconcile.js";
|
|
4
|
+
export { DAG_RECONCILE_ENGINE_INVARIANTS, reconcileDagTurn } from "./dag_reconcile.js";
|
|
5
|
+
export type { InterRootQueuePolicy, InterRootQueueReconcilePlan, InterRootQueueSnapshot, OrchestrationQueueState, } from "./inter_root_queue_reconcile.js";
|
|
6
|
+
export { DEFAULT_INTER_ROOT_QUEUE_POLICY, INTER_ROOT_QUEUE_RECONCILE_INVARIANTS, normalizeInterRootQueuePolicy, ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS, ORCHESTRATION_QUEUE_INVARIANTS, reconcileInterRootQueue, } from "./inter_root_queue_reconcile.js";
|
|
3
7
|
export type { ModelOverrides, ResolvedModelConfig } from "./model_resolution.js";
|
|
4
8
|
export { resolveModelConfig } from "./model_resolution.js";
|
|
5
9
|
export type { PiStreamRendererOpts } from "./pi_stream_renderer.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,SAAS,EACT,yBAAyB,EACzB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,SAAS,EACT,yBAAyB,EACzB,cAAc,EACd,uBAAuB,EACvB,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,4BAA4B,EAC5B,8BAA8B,EAC9B,kCAAkC,EAClC,SAAS,EACT,0BAA0B,GAC1B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,EAAE,+BAA+B,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACvF,YAAY,EACX,oBAAoB,EACpB,2BAA2B,EAC3B,sBAAsB,EACtB,uBAAuB,GACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACN,+BAA+B,EAC/B,qCAAqC,EACrC,6BAA6B,EAC7B,uCAAuC,EACvC,8BAA8B,EAC9B,uBAAuB,GACvB,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export { DagRunner } from "./dag_runner.js";
|
|
1
|
+
export { DAG_RUNNER_BUDGET_INVARIANTS, DAG_RUNNER_CONTRACT_INVARIANTS, DEFAULT_MAX_REFINE_ROUNDS_PER_ROOT, DagRunner, REVIEW_DECISION_TO_OUTCOME, } from "./dag_runner.js";
|
|
2
|
+
export { DAG_RECONCILE_ENGINE_INVARIANTS, reconcileDagTurn } from "./dag_reconcile.js";
|
|
3
|
+
export { DEFAULT_INTER_ROOT_QUEUE_POLICY, INTER_ROOT_QUEUE_RECONCILE_INVARIANTS, normalizeInterRootQueuePolicy, ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS, ORCHESTRATION_QUEUE_INVARIANTS, reconcileInterRootQueue, } from "./inter_root_queue_reconcile.js";
|
|
2
4
|
export { resolveModelConfig } from "./model_resolution.js";
|
|
3
5
|
export { PiStreamRenderer } from "./pi_stream_renderer.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type OrchestrationQueueState = "queued" | "active" | "waiting_review" | "refining" | "done" | "failed" | "cancelled";
|
|
2
|
+
/**
|
|
3
|
+
* Inter-root scheduler policy knob for durable queue drain.
|
|
4
|
+
* - sequential: exactly one active root (`max_active_roots=1`)
|
|
5
|
+
* - parallel: bounded fanout (`max_active_roots>=1`)
|
|
6
|
+
*/
|
|
7
|
+
export type InterRootQueuePolicy = {
|
|
8
|
+
mode: "sequential";
|
|
9
|
+
max_active_roots: 1;
|
|
10
|
+
} | {
|
|
11
|
+
mode: "parallel";
|
|
12
|
+
max_active_roots: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const DEFAULT_INTER_ROOT_QUEUE_POLICY: InterRootQueuePolicy;
|
|
15
|
+
export declare function normalizeInterRootQueuePolicy(policy: InterRootQueuePolicy | null | undefined): InterRootQueuePolicy;
|
|
16
|
+
/**
|
|
17
|
+
* Allowed queue transitions. Downstream queue tests should enforce this table exactly.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS: Record<OrchestrationQueueState, readonly OrchestrationQueueState[]>;
|
|
20
|
+
export declare const ORCHESTRATION_QUEUE_INVARIANTS: readonly ["ORCH-QUEUE-001: Queue writes are durable before acknowledging enqueue/start/resume requests.", "ORCH-QUEUE-002: Queue dispatch must claim exactly one active item at a time per root slot.", "ORCH-QUEUE-003: Terminal states (done|failed|cancelled) are immutable.", "ORCH-QUEUE-004: Review path is active -> waiting_review -> (done | refining).", "ORCH-QUEUE-005: Refinement re-enters execution only via refining -> queued.", "ORCH-QUEUE-006: sequential policy permits <=1 active root; parallel permits <=max_active_roots active roots."];
|
|
21
|
+
export type InterRootQueueSnapshot = {
|
|
22
|
+
queue_id: string;
|
|
23
|
+
root_issue_id: string | null;
|
|
24
|
+
state: OrchestrationQueueState;
|
|
25
|
+
job_id: string | null;
|
|
26
|
+
created_at_ms: number;
|
|
27
|
+
};
|
|
28
|
+
export type InterRootQueueReconcilePlan = {
|
|
29
|
+
policy: InterRootQueuePolicy;
|
|
30
|
+
max_active_roots: number;
|
|
31
|
+
active_root_count: number;
|
|
32
|
+
available_root_slots: number;
|
|
33
|
+
activate_queue_ids: string[];
|
|
34
|
+
launch_queue_ids: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare const INTER_ROOT_QUEUE_RECONCILE_INVARIANTS: readonly ["ORCH-INTER-ROOT-RECON-001: one queue snapshot + policy yields one deterministic activation/launch plan.", "ORCH-INTER-ROOT-RECON-002: activation order is FIFO (`created_at_ms`, then `queue_id`) with per-root slot dedupe.", "ORCH-INTER-ROOT-RECON-003: sequential policy admits <=1 occupied root; parallel admits <=max_active_roots roots.", "ORCH-INTER-ROOT-RECON-004: launch candidates are active rows without bound job ids, one launch per root slot."];
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic inter-root queue reconcile primitive.
|
|
39
|
+
*
|
|
40
|
+
* Computes queue activation/launch intentions from durable queue state and policy. The caller is
|
|
41
|
+
* responsible for performing side effects (claim, launch, bind, transition) and reconciling again
|
|
42
|
+
* against refreshed queue/runtime snapshots.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reconcileInterRootQueue<Row extends InterRootQueueSnapshot>(rows: readonly Row[], policy: InterRootQueuePolicy): InterRootQueueReconcilePlan;
|
|
45
|
+
//# sourceMappingURL=inter_root_queue_reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inter_root_queue_reconcile.d.ts","sourceRoot":"","sources":["../src/inter_root_queue_reconcile.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAChC,QAAQ,GACR,QAAQ,GACR,gBAAgB,GAChB,UAAU,GACV,MAAM,GACN,QAAQ,GACR,WAAW,CAAC;AAEf;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,gBAAgB,EAAE,CAAC,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,eAAO,MAAM,+BAA+B,EAAE,oBAG7C,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,GAAG,oBAAoB,CAWnH;AAED;;GAEG;AACH,eAAO,MAAM,uCAAuC,EAAE,MAAM,CAC3D,uBAAuB,EACvB,SAAS,uBAAuB,EAAE,CASlC,CAAC;AAEF,eAAO,MAAM,8BAA8B,miBAOjC,CAAC;AAIX,MAAM,MAAM,sBAAsB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,uBAAuB,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACzC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,qCAAqC,gdAKxC,CAAC;AAoBX;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,SAAS,sBAAsB,EACzE,IAAI,EAAE,SAAS,GAAG,EAAE,EACpB,MAAM,EAAE,oBAAoB,GAC1B,2BAA2B,CAgD7B"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const DEFAULT_INTER_ROOT_QUEUE_POLICY = {
|
|
2
|
+
mode: "sequential",
|
|
3
|
+
max_active_roots: 1,
|
|
4
|
+
};
|
|
5
|
+
export function normalizeInterRootQueuePolicy(policy) {
|
|
6
|
+
if (!policy) {
|
|
7
|
+
return DEFAULT_INTER_ROOT_QUEUE_POLICY;
|
|
8
|
+
}
|
|
9
|
+
if (policy.mode === "parallel") {
|
|
10
|
+
return {
|
|
11
|
+
mode: "parallel",
|
|
12
|
+
max_active_roots: Math.max(1, Math.trunc(policy.max_active_roots)),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return DEFAULT_INTER_ROOT_QUEUE_POLICY;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Allowed queue transitions. Downstream queue tests should enforce this table exactly.
|
|
19
|
+
*/
|
|
20
|
+
export const ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS = {
|
|
21
|
+
queued: ["active", "cancelled"],
|
|
22
|
+
active: ["waiting_review", "done", "failed", "cancelled"],
|
|
23
|
+
waiting_review: ["refining", "done", "failed", "cancelled"],
|
|
24
|
+
refining: ["queued", "failed", "cancelled"],
|
|
25
|
+
done: [],
|
|
26
|
+
failed: [],
|
|
27
|
+
cancelled: [],
|
|
28
|
+
};
|
|
29
|
+
export const ORCHESTRATION_QUEUE_INVARIANTS = [
|
|
30
|
+
"ORCH-QUEUE-001: Queue writes are durable before acknowledging enqueue/start/resume requests.",
|
|
31
|
+
"ORCH-QUEUE-002: Queue dispatch must claim exactly one active item at a time per root slot.",
|
|
32
|
+
"ORCH-QUEUE-003: Terminal states (done|failed|cancelled) are immutable.",
|
|
33
|
+
"ORCH-QUEUE-004: Review path is active -> waiting_review -> (done | refining).",
|
|
34
|
+
"ORCH-QUEUE-005: Refinement re-enters execution only via refining -> queued.",
|
|
35
|
+
"ORCH-QUEUE-006: sequential policy permits <=1 active root; parallel permits <=max_active_roots active roots.",
|
|
36
|
+
];
|
|
37
|
+
const INTER_ROOT_OCCUPIED_STATES = new Set(["active", "waiting_review", "refining"]);
|
|
38
|
+
export const INTER_ROOT_QUEUE_RECONCILE_INVARIANTS = [
|
|
39
|
+
"ORCH-INTER-ROOT-RECON-001: one queue snapshot + policy yields one deterministic activation/launch plan.",
|
|
40
|
+
"ORCH-INTER-ROOT-RECON-002: activation order is FIFO (`created_at_ms`, then `queue_id`) with per-root slot dedupe.",
|
|
41
|
+
"ORCH-INTER-ROOT-RECON-003: sequential policy admits <=1 occupied root; parallel admits <=max_active_roots roots.",
|
|
42
|
+
"ORCH-INTER-ROOT-RECON-004: launch candidates are active rows without bound job ids, one launch per root slot.",
|
|
43
|
+
];
|
|
44
|
+
function stableCompare(a, b) {
|
|
45
|
+
if (a.created_at_ms !== b.created_at_ms) {
|
|
46
|
+
return a.created_at_ms - b.created_at_ms;
|
|
47
|
+
}
|
|
48
|
+
return a.queue_id.localeCompare(b.queue_id);
|
|
49
|
+
}
|
|
50
|
+
function normalizeMaxActiveRoots(policy) {
|
|
51
|
+
if (policy.mode === "parallel") {
|
|
52
|
+
return Math.max(1, Math.trunc(policy.max_active_roots));
|
|
53
|
+
}
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
function queueRootSlotKey(row) {
|
|
57
|
+
return row.root_issue_id ? `root:${row.root_issue_id}` : `queue:${row.queue_id}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Deterministic inter-root queue reconcile primitive.
|
|
61
|
+
*
|
|
62
|
+
* Computes queue activation/launch intentions from durable queue state and policy. The caller is
|
|
63
|
+
* responsible for performing side effects (claim, launch, bind, transition) and reconciling again
|
|
64
|
+
* against refreshed queue/runtime snapshots.
|
|
65
|
+
*/
|
|
66
|
+
export function reconcileInterRootQueue(rows, policy) {
|
|
67
|
+
const sorted = [...rows].sort(stableCompare);
|
|
68
|
+
const maxActiveRoots = normalizeMaxActiveRoots(policy);
|
|
69
|
+
const occupiedRoots = new Set();
|
|
70
|
+
const launchRoots = new Set();
|
|
71
|
+
const launchQueueIds = [];
|
|
72
|
+
for (const row of sorted) {
|
|
73
|
+
if (!INTER_ROOT_OCCUPIED_STATES.has(row.state)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const slotKey = queueRootSlotKey(row);
|
|
77
|
+
occupiedRoots.add(slotKey);
|
|
78
|
+
if (row.state !== "active" || row.job_id != null || launchRoots.has(slotKey)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
launchRoots.add(slotKey);
|
|
82
|
+
launchQueueIds.push(row.queue_id);
|
|
83
|
+
}
|
|
84
|
+
const availableRootSlots = Math.max(0, maxActiveRoots - occupiedRoots.size);
|
|
85
|
+
const claimedRoots = new Set();
|
|
86
|
+
const activateQueueIds = [];
|
|
87
|
+
if (availableRootSlots > 0) {
|
|
88
|
+
for (const row of sorted) {
|
|
89
|
+
if (row.state !== "queued") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const slotKey = queueRootSlotKey(row);
|
|
93
|
+
if (occupiedRoots.has(slotKey) || claimedRoots.has(slotKey)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
claimedRoots.add(slotKey);
|
|
97
|
+
activateQueueIds.push(row.queue_id);
|
|
98
|
+
if (activateQueueIds.length >= availableRootSlots) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
policy,
|
|
105
|
+
max_active_roots: maxActiveRoots,
|
|
106
|
+
active_root_count: occupiedRoots.size,
|
|
107
|
+
available_root_slots: availableRootSlots,
|
|
108
|
+
activate_queue_ids: activateQueueIds,
|
|
109
|
+
launch_queue_ids: launchQueueIds,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-orchestrator",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.71",
|
|
4
4
|
"description": "Long-running execution engine for mu work plans.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"dist/**"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@femtomc/mu-agent": "
|
|
25
|
-
"@femtomc/mu-core": "
|
|
26
|
-
"@femtomc/mu-forum": "
|
|
27
|
-
"@femtomc/mu-issue": "
|
|
24
|
+
"@femtomc/mu-agent": "workspace:*",
|
|
25
|
+
"@femtomc/mu-core": "workspace:*",
|
|
26
|
+
"@femtomc/mu-forum": "workspace:*",
|
|
27
|
+
"@femtomc/mu-issue": "workspace:*",
|
|
28
28
|
"@mariozechner/pi-coding-agent": "^0.53.0",
|
|
29
29
|
"@mariozechner/pi-ai": "^0.53.0"
|
|
30
30
|
}
|