@femtomc/mu-orchestrator 26.2.14 → 26.2.16
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_runner.d.ts +33 -3
- package/dist/dag_runner.d.ts.map +1 -1
- package/dist/dag_runner.js +71 -203
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/model_resolution.d.ts +21 -0
- package/dist/model_resolution.d.ts.map +1 -0
- package/dist/model_resolution.js +88 -0
- package/dist/mu_roles.d.ts +4 -0
- package/dist/mu_roles.d.ts.map +1 -0
- package/dist/mu_roles.js +39 -0
- package/dist/pi_backend.d.ts +3 -1
- package/dist/pi_backend.d.ts.map +1 -1
- package/dist/pi_backend.js +16 -2
- package/dist/pi_sdk_backend.d.ts +10 -1
- package/dist/pi_sdk_backend.d.ts.map +1 -1
- package/dist/pi_sdk_backend.js +74 -15
- package/dist/pi_stream_renderer.d.ts +31 -0
- package/dist/pi_stream_renderer.d.ts.map +1 -0
- package/dist/pi_stream_renderer.js +97 -0
- package/package.json +4 -4
package/dist/dag_runner.d.ts
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
import { type EventLog } from "@femtomc/mu-core/node";
|
|
2
2
|
import type { ForumStore } from "@femtomc/mu-forum";
|
|
3
3
|
import type { IssueStore } from "@femtomc/mu-issue";
|
|
4
|
+
import type { ModelOverrides } from "./model_resolution.js";
|
|
4
5
|
import type { BackendRunner } from "./pi_backend.js";
|
|
5
6
|
export type DagResult = {
|
|
6
7
|
status: "root_final" | "no_executable_leaf" | "max_steps_exhausted" | "error";
|
|
7
8
|
steps: number;
|
|
8
9
|
error: string;
|
|
9
10
|
};
|
|
11
|
+
export type DagRunnerStepStartEvent = {
|
|
12
|
+
rootId: string;
|
|
13
|
+
step: number;
|
|
14
|
+
issueId: string;
|
|
15
|
+
role: string | null;
|
|
16
|
+
title: string;
|
|
17
|
+
};
|
|
18
|
+
export type DagRunnerStepEndEvent = {
|
|
19
|
+
rootId: string;
|
|
20
|
+
step: number;
|
|
21
|
+
issueId: string;
|
|
22
|
+
exitCode: number;
|
|
23
|
+
elapsedS: number;
|
|
24
|
+
outcome: string | null;
|
|
25
|
+
};
|
|
26
|
+
export type DagRunnerBackendLineEvent = {
|
|
27
|
+
rootId: string;
|
|
28
|
+
step: number;
|
|
29
|
+
issueId: string;
|
|
30
|
+
logSuffix: string;
|
|
31
|
+
line: string;
|
|
32
|
+
};
|
|
33
|
+
export type DagRunnerHooks = {
|
|
34
|
+
onStepStart?: (ev: DagRunnerStepStartEvent) => void | Promise<void>;
|
|
35
|
+
onStepEnd?: (ev: DagRunnerStepEndEvent) => void | Promise<void>;
|
|
36
|
+
onBackendLine?: (ev: DagRunnerBackendLineEvent) => void;
|
|
37
|
+
};
|
|
38
|
+
export type DagRunnerRunOpts = {
|
|
39
|
+
hooks?: DagRunnerHooks;
|
|
40
|
+
};
|
|
10
41
|
export declare class DagRunner {
|
|
11
42
|
#private;
|
|
12
43
|
constructor(store: IssueStore, forum: ForumStore, repoRoot: string, opts?: {
|
|
13
44
|
backend?: BackendRunner;
|
|
14
45
|
events?: EventLog;
|
|
46
|
+
modelOverrides?: ModelOverrides;
|
|
15
47
|
});
|
|
16
|
-
run(rootId: string, maxSteps?: number, opts?:
|
|
17
|
-
review?: boolean;
|
|
18
|
-
}): Promise<DagResult>;
|
|
48
|
+
run(rootId: string, maxSteps?: number, opts?: DagRunnerRunOpts): Promise<DagResult>;
|
|
19
49
|
}
|
|
20
50
|
//# sourceMappingURL=dag_runner.d.ts.map
|
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":"
|
|
1
|
+
{"version":3,"file":"dag_runner.d.ts","sourceRoot":"","sources":["../src/dag_runner.ts"],"names":[],"mappings":"AAGA,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;AACpD,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,uBAAuB,CAAC;AAGjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,MAAM,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,YAAY,GAAG,oBAAoB,GAAG,qBAAqB,GAAG,OAAO,CAAC;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,yBAAyB,KAAK,IAAI,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AA2BF,qBAAa,SAAS;;gBAWpB,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,aAAa,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;QAAC,cAAc,CAAC,EAAE,cAAc,CAAA;KAAO;IAsKrF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAW,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC;CAiMjG"}
|
package/dist/dag_runner.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { mkdir } from "node:fs/promises";
|
|
3
2
|
import { join, relative } from "node:path";
|
|
4
|
-
import { currentRunId,
|
|
3
|
+
import { currentRunId, fsEventLogFromRepoRoot, getStorePaths, newRunId, runContext, } from "@femtomc/mu-core/node";
|
|
4
|
+
import { resolveModelConfig } from "./model_resolution.js";
|
|
5
|
+
import { parseMuRole, systemPromptForRole } from "./mu_roles.js";
|
|
5
6
|
import { PiSdkBackend } from "./pi_sdk_backend.js";
|
|
6
|
-
import { readPromptMeta, renderPromptTemplate } from "./prompt.js";
|
|
7
7
|
function roundTo(n, digits) {
|
|
8
8
|
const f = 10 ** digits;
|
|
9
9
|
return Math.round(n * f) / f;
|
|
10
10
|
}
|
|
11
|
+
function specRoleFromExecutionSpec(execution_spec) {
|
|
12
|
+
const role = execution_spec?.role;
|
|
13
|
+
if (typeof role !== "string") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const trimmed = role.trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
+
}
|
|
11
19
|
function relPath(repoRoot, path) {
|
|
12
20
|
try {
|
|
13
21
|
const rel = relative(repoRoot, path);
|
|
@@ -18,15 +26,12 @@ function relPath(repoRoot, path) {
|
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
export class DagRunner {
|
|
21
|
-
// Hardcoded fallbacks if neither execution_spec nor orchestrator.md provide config.
|
|
22
|
-
#fallbackCli = "pi";
|
|
23
|
-
#fallbackModel = "gpt-5.3-codex";
|
|
24
|
-
#fallbackReasoning = "xhigh";
|
|
25
29
|
#store;
|
|
26
30
|
#forum;
|
|
27
31
|
#repoRoot;
|
|
28
32
|
#events;
|
|
29
33
|
#backend;
|
|
34
|
+
#modelOverrides;
|
|
30
35
|
#reorchestrateOutcomes = new Set(["failure", "needs_work"]);
|
|
31
36
|
constructor(store, forum, repoRoot, opts = {}) {
|
|
32
37
|
this.#store = store;
|
|
@@ -34,70 +39,29 @@ export class DagRunner {
|
|
|
34
39
|
this.#repoRoot = repoRoot;
|
|
35
40
|
this.#events = opts.events ?? fsEventLogFromRepoRoot(repoRoot);
|
|
36
41
|
this.#backend = opts.backend ?? new PiSdkBackend();
|
|
42
|
+
this.#modelOverrides = opts.modelOverrides ?? {};
|
|
37
43
|
}
|
|
38
44
|
async #resolveConfig(issue) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let reasoning = this.#fallbackReasoning;
|
|
42
|
-
let promptPath = null;
|
|
43
|
-
// Tier 1: orchestrator.md frontmatter (global defaults).
|
|
44
|
-
const { orchestratorPath } = getStorePaths(this.#repoRoot);
|
|
45
|
-
if (existsSync(orchestratorPath)) {
|
|
46
|
-
const meta = await readPromptMeta(orchestratorPath);
|
|
47
|
-
if (typeof meta.cli === "string")
|
|
48
|
-
cli = meta.cli;
|
|
49
|
-
if (typeof meta.model === "string")
|
|
50
|
-
model = meta.model;
|
|
51
|
-
if (typeof meta.reasoning === "string")
|
|
52
|
-
reasoning = meta.reasoning;
|
|
53
|
-
promptPath = orchestratorPath;
|
|
54
|
-
}
|
|
55
|
-
// Parse execution spec (may set role + explicit fields).
|
|
56
|
-
const specDict = issue.execution_spec ?? null;
|
|
57
|
-
const spec = specDict ? executionSpecFromDict(specDict, this.#repoRoot) : null;
|
|
58
|
-
// Tier 2: role file frontmatter (role-specific defaults).
|
|
59
|
-
if (spec?.role) {
|
|
60
|
-
const rolePath = join(this.#repoRoot, ".mu", "roles", `${spec.role}.md`);
|
|
61
|
-
if (existsSync(rolePath)) {
|
|
62
|
-
const roleMeta = await readPromptMeta(rolePath);
|
|
63
|
-
if (typeof roleMeta.cli === "string")
|
|
64
|
-
cli = roleMeta.cli;
|
|
65
|
-
if (typeof roleMeta.model === "string")
|
|
66
|
-
model = roleMeta.model;
|
|
67
|
-
if (typeof roleMeta.reasoning === "string")
|
|
68
|
-
reasoning = roleMeta.reasoning;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Tier 3: execution_spec explicit fields (highest priority).
|
|
72
|
-
if (spec) {
|
|
73
|
-
if (spec.cli != null)
|
|
74
|
-
cli = spec.cli;
|
|
75
|
-
if (spec.model != null)
|
|
76
|
-
model = spec.model;
|
|
77
|
-
if (spec.reasoning != null)
|
|
78
|
-
reasoning = spec.reasoning;
|
|
79
|
-
if (spec.prompt_path != null)
|
|
80
|
-
promptPath = spec.prompt_path;
|
|
81
|
-
}
|
|
82
|
-
return { cli, model, reasoning, promptPath };
|
|
45
|
+
void issue;
|
|
46
|
+
return resolveModelConfig(this.#modelOverrides);
|
|
83
47
|
}
|
|
84
|
-
async #
|
|
85
|
-
let rendered;
|
|
86
|
-
if (
|
|
87
|
-
rendered
|
|
48
|
+
async #renderUserPrompt(issue, rootId, step) {
|
|
49
|
+
let rendered = issue.title ?? "";
|
|
50
|
+
if (issue.body) {
|
|
51
|
+
rendered += `\n\n${issue.body}`;
|
|
88
52
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
53
|
+
const runId = currentRunId();
|
|
54
|
+
rendered += `\n\n## Mu Run Context\nRoot: ${rootId}\nAssigned issue: ${issue.id}\nStep: ${step}\n`;
|
|
55
|
+
if (runId) {
|
|
56
|
+
rendered += `Run: ${runId}\n`;
|
|
94
57
|
}
|
|
95
|
-
rendered += `\n\n## Inshallah Context\nRoot: ${rootId}\nAssigned issue: ${issue.id}\n`;
|
|
96
58
|
return rendered;
|
|
97
59
|
}
|
|
98
|
-
async #executeBackend(issue, cfg, rootId, opts = {}) {
|
|
60
|
+
async #executeBackend(issue, cfg, rootId, step, opts = {}) {
|
|
61
|
+
const role = parseMuRole(specRoleFromExecutionSpec(issue.execution_spec));
|
|
99
62
|
const logSuffix = opts.logSuffix ?? "";
|
|
100
|
-
const rendered = await this.#
|
|
63
|
+
const rendered = await this.#renderUserPrompt(issue, rootId, step);
|
|
64
|
+
const systemPrompt = systemPromptForRole(role);
|
|
101
65
|
const { logsDir } = getStorePaths(this.#repoRoot);
|
|
102
66
|
await mkdir(logsDir, { recursive: true });
|
|
103
67
|
const suffix = logSuffix ? `.${logSuffix}` : "";
|
|
@@ -106,10 +70,10 @@ export class DagRunner {
|
|
|
106
70
|
source: "backend",
|
|
107
71
|
issueId: issue.id,
|
|
108
72
|
payload: {
|
|
73
|
+
role,
|
|
109
74
|
cli: cfg.cli,
|
|
110
75
|
model: cfg.model,
|
|
111
76
|
reasoning: cfg.reasoning,
|
|
112
|
-
prompt_path: cfg.promptPath,
|
|
113
77
|
tee_path: relPath(this.#repoRoot, teePath),
|
|
114
78
|
log_suffix: logSuffix,
|
|
115
79
|
},
|
|
@@ -117,14 +81,16 @@ export class DagRunner {
|
|
|
117
81
|
const t0 = Date.now();
|
|
118
82
|
const exitCode = await this.#backend.run({
|
|
119
83
|
issueId: issue.id,
|
|
84
|
+
role,
|
|
85
|
+
systemPrompt,
|
|
120
86
|
prompt: rendered,
|
|
121
87
|
model: cfg.model,
|
|
122
88
|
thinking: cfg.reasoning,
|
|
123
89
|
cwd: this.#repoRoot,
|
|
124
90
|
cli: cfg.cli,
|
|
125
|
-
promptPath: cfg.promptPath,
|
|
126
91
|
logSuffix,
|
|
127
92
|
teePath,
|
|
93
|
+
onLine: opts.onLine,
|
|
128
94
|
});
|
|
129
95
|
const elapsedS = (Date.now() - t0) / 1000;
|
|
130
96
|
await this.#events.emit("backend.run.end", {
|
|
@@ -140,44 +106,16 @@ export class DagRunner {
|
|
|
140
106
|
});
|
|
141
107
|
return { exitCode, elapsedS };
|
|
142
108
|
}
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const issueId = issue.id;
|
|
148
|
-
// Guards.
|
|
149
|
-
if (issue.outcome !== "success") {
|
|
150
|
-
return issue;
|
|
151
|
-
}
|
|
152
|
-
if (!this.#hasReviewer()) {
|
|
153
|
-
return issue;
|
|
109
|
+
async #reopenForOrchestration(issueId, opts) {
|
|
110
|
+
const before = await this.#store.get(issueId);
|
|
111
|
+
if (!before) {
|
|
112
|
+
return;
|
|
154
113
|
}
|
|
155
|
-
await this.#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
114
|
+
const reopened = await this.#store.update(issueId, {
|
|
115
|
+
status: "open",
|
|
116
|
+
outcome: null,
|
|
117
|
+
execution_spec: { role: "orchestrator" },
|
|
159
118
|
});
|
|
160
|
-
const reviewIssue = { ...issue, execution_spec: { role: "reviewer" } };
|
|
161
|
-
const cfg = await this.#resolveConfig(reviewIssue);
|
|
162
|
-
const { exitCode, elapsedS } = await this.#executeBackend(reviewIssue, cfg, rootId, { logSuffix: "review" });
|
|
163
|
-
await this.#forum.post(`issue:${issueId}`, JSON.stringify({
|
|
164
|
-
step,
|
|
165
|
-
issue_id: issueId,
|
|
166
|
-
title: issue.title,
|
|
167
|
-
exit_code: exitCode,
|
|
168
|
-
elapsed_s: roundTo(elapsedS, 1),
|
|
169
|
-
type: "review",
|
|
170
|
-
}), "reviewer");
|
|
171
|
-
const updated = (await this.#store.get(issueId)) ?? issue;
|
|
172
|
-
await this.#events.emit("dag.review.end", {
|
|
173
|
-
source: "dag_runner",
|
|
174
|
-
issueId,
|
|
175
|
-
payload: { root_id: rootId, step, outcome: updated.outcome },
|
|
176
|
-
});
|
|
177
|
-
return updated;
|
|
178
|
-
}
|
|
179
|
-
async #reopenForOrchestration(issueId, opts) {
|
|
180
|
-
const reopened = await this.#store.update(issueId, { status: "open", outcome: null, execution_spec: null });
|
|
181
119
|
await this.#events.emit("dag.unstick.reopen", {
|
|
182
120
|
source: "dag_runner",
|
|
183
121
|
issueId,
|
|
@@ -231,99 +169,14 @@ export class DagRunner {
|
|
|
231
169
|
await this.#reopenForOrchestration(target.id, { reason: `was outcome=${target.outcome}`, step });
|
|
232
170
|
return true;
|
|
233
171
|
}
|
|
234
|
-
async #collapseReview(issue, rootId, step) {
|
|
235
|
-
const issueId = issue.id;
|
|
236
|
-
await this.#events.emit("dag.collapse_review.start", {
|
|
237
|
-
source: "dag_runner",
|
|
238
|
-
issueId,
|
|
239
|
-
payload: { root_id: rootId, step },
|
|
240
|
-
});
|
|
241
|
-
const kids = await this.#store.children(issueId);
|
|
242
|
-
const lines = [];
|
|
243
|
-
for (const kid of kids) {
|
|
244
|
-
lines.push(`- [${kid.outcome ?? "?"}] ${kid.id}: ${kid.title}`);
|
|
245
|
-
}
|
|
246
|
-
const childrenSummary = lines.join("\n");
|
|
247
|
-
const originalBody = issue.body || "";
|
|
248
|
-
const collapsePrompt = `# Collapse Review\n\n` +
|
|
249
|
-
`## Original Specification\n\n` +
|
|
250
|
-
`**${issue.title}**\n\n` +
|
|
251
|
-
`${originalBody}\n\n` +
|
|
252
|
-
`## Children Outcomes\n\n` +
|
|
253
|
-
`${childrenSummary}\n\n` +
|
|
254
|
-
`## Instructions\n\n` +
|
|
255
|
-
`All children of this issue have completed. Review whether their aggregate work satisfies the original specification above.\n\n` +
|
|
256
|
-
`If satisfied: no action needed (the issue will be marked successful).\n\n` +
|
|
257
|
-
`If NOT satisfied: mark the parent as needing work by running:\n\n` +
|
|
258
|
-
` \`mu issues update ${issueId} --outcome needs_work\`\n\n` +
|
|
259
|
-
`Then explain the gaps in the forum topic (issue:${issueId}).\n\n` +
|
|
260
|
-
`Do NOT create child issues yourself; the orchestrator will re-expand the issue into remediation children.\n`;
|
|
261
|
-
const reviewIssue = {
|
|
262
|
-
...issue,
|
|
263
|
-
title: `Collapse review: ${issue.title}`,
|
|
264
|
-
body: collapsePrompt,
|
|
265
|
-
execution_spec: { role: "reviewer" },
|
|
266
|
-
};
|
|
267
|
-
const cfg = await this.#resolveConfig(reviewIssue);
|
|
268
|
-
const { exitCode, elapsedS } = await this.#executeBackend(reviewIssue, { ...cfg, promptPath: null }, rootId, {
|
|
269
|
-
logSuffix: "collapse-review",
|
|
270
|
-
});
|
|
271
|
-
await this.#forum.post(`issue:${issueId}`, JSON.stringify({
|
|
272
|
-
step,
|
|
273
|
-
issue_id: issueId,
|
|
274
|
-
title: issue.title,
|
|
275
|
-
exit_code: exitCode,
|
|
276
|
-
elapsed_s: roundTo(elapsedS, 1),
|
|
277
|
-
type: "collapse-review",
|
|
278
|
-
}), "reviewer");
|
|
279
|
-
const newKids = await this.#store.children(issueId);
|
|
280
|
-
const openKids = newKids.filter((k) => k.status !== "closed");
|
|
281
|
-
const updated = (await this.#store.get(issueId)) ?? issue;
|
|
282
|
-
if (updated.status !== "closed") {
|
|
283
|
-
await this.#events.emit("dag.collapse_review.end", {
|
|
284
|
-
source: "dag_runner",
|
|
285
|
-
issueId,
|
|
286
|
-
payload: { root_id: rootId, step, status: updated.status, outcome: updated.outcome },
|
|
287
|
-
});
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (updated.outcome && this.#reorchestrateOutcomes.has(updated.outcome)) {
|
|
291
|
-
await this.#events.emit("dag.collapse_review.end", {
|
|
292
|
-
source: "dag_runner",
|
|
293
|
-
issueId,
|
|
294
|
-
payload: { root_id: rootId, step, status: updated.status, outcome: updated.outcome },
|
|
295
|
-
});
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (openKids.length > 0) {
|
|
299
|
-
await this.#events.emit("dag.collapse_review.end", {
|
|
300
|
-
source: "dag_runner",
|
|
301
|
-
issueId,
|
|
302
|
-
payload: {
|
|
303
|
-
root_id: rootId,
|
|
304
|
-
step,
|
|
305
|
-
status: updated.status,
|
|
306
|
-
outcome: updated.outcome,
|
|
307
|
-
open_kids: openKids.length,
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
await this.#store.update(issueId, { outcome: "success" });
|
|
313
|
-
await this.#events.emit("dag.collapse_review.end", {
|
|
314
|
-
source: "dag_runner",
|
|
315
|
-
issueId,
|
|
316
|
-
payload: { root_id: rootId, step, outcome: "success" },
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
172
|
async run(rootId, maxSteps = 20, opts = {}) {
|
|
320
|
-
const
|
|
173
|
+
const hooks = opts.hooks;
|
|
321
174
|
const runId = currentRunId() ?? newRunId();
|
|
322
175
|
return await runContext({ runId }, async () => {
|
|
323
176
|
await this.#events.emit("dag.run.start", {
|
|
324
177
|
source: "dag_runner",
|
|
325
178
|
issueId: rootId,
|
|
326
|
-
payload: { root_id: rootId, max_steps: maxSteps
|
|
179
|
+
payload: { root_id: rootId, max_steps: maxSteps },
|
|
327
180
|
});
|
|
328
181
|
let final = null;
|
|
329
182
|
try {
|
|
@@ -331,14 +184,6 @@ export class DagRunner {
|
|
|
331
184
|
const step = i + 1;
|
|
332
185
|
// 0. Unstick: failures / needs_work trigger re-orchestration.
|
|
333
186
|
await this.#maybeUnstick(rootId, step);
|
|
334
|
-
// 1. Collapse review (before termination check).
|
|
335
|
-
if (review && this.#hasReviewer()) {
|
|
336
|
-
const collapsible = await this.#store.collapsible(rootId);
|
|
337
|
-
if (collapsible.length > 0) {
|
|
338
|
-
await this.#collapseReview(collapsible[0], rootId, step);
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
187
|
// 2. Check termination.
|
|
343
188
|
const v = await this.#store.validate(rootId);
|
|
344
189
|
if (v.is_final) {
|
|
@@ -371,8 +216,13 @@ export class DagRunner {
|
|
|
371
216
|
execution_spec: null,
|
|
372
217
|
};
|
|
373
218
|
const cfg = await this.#resolveConfig(repairIssue);
|
|
374
|
-
const
|
|
375
|
-
|
|
219
|
+
const logSuffix = "unstick";
|
|
220
|
+
const onBackendLine = hooks?.onBackendLine;
|
|
221
|
+
const { exitCode, elapsedS } = await this.#executeBackend(repairIssue, cfg, rootId, step, {
|
|
222
|
+
logSuffix,
|
|
223
|
+
onLine: onBackendLine
|
|
224
|
+
? (line) => onBackendLine({ rootId, step, issueId: rootId, logSuffix, line })
|
|
225
|
+
: undefined,
|
|
376
226
|
});
|
|
377
227
|
await this.#forum.post(`issue:${rootId}`, JSON.stringify({
|
|
378
228
|
step,
|
|
@@ -391,11 +241,16 @@ export class DagRunner {
|
|
|
391
241
|
}
|
|
392
242
|
const issue = candidates[0];
|
|
393
243
|
const issueId = issue.id;
|
|
244
|
+
// Validate role early so we don't claim/work an unsupported leaf.
|
|
245
|
+
const role = parseMuRole(specRoleFromExecutionSpec(issue.execution_spec));
|
|
394
246
|
await this.#events.emit("dag.step.start", {
|
|
395
247
|
source: "dag_runner",
|
|
396
248
|
issueId,
|
|
397
249
|
payload: { root_id: rootId, step, title: issue.title ?? "" },
|
|
398
250
|
});
|
|
251
|
+
if (hooks?.onStepStart) {
|
|
252
|
+
await hooks.onStepStart({ rootId, step, issueId, role, title: issue.title ?? "" });
|
|
253
|
+
}
|
|
399
254
|
// 3. Claim.
|
|
400
255
|
await this.#events.emit("dag.claim", {
|
|
401
256
|
source: "dag_runner",
|
|
@@ -405,7 +260,14 @@ export class DagRunner {
|
|
|
405
260
|
await this.#store.claim(issueId);
|
|
406
261
|
// 4. Route + 5. Render + 6. Execute.
|
|
407
262
|
const cfg = await this.#resolveConfig(issue);
|
|
408
|
-
const
|
|
263
|
+
const logSuffix = "";
|
|
264
|
+
const onBackendLine = hooks?.onBackendLine;
|
|
265
|
+
const { exitCode, elapsedS } = await this.#executeBackend(issue, cfg, rootId, step, {
|
|
266
|
+
logSuffix,
|
|
267
|
+
onLine: onBackendLine
|
|
268
|
+
? (line) => onBackendLine({ rootId, step, issueId, logSuffix, line })
|
|
269
|
+
: undefined,
|
|
270
|
+
});
|
|
409
271
|
// 7. Check postconditions.
|
|
410
272
|
let updated = await this.#store.get(issueId);
|
|
411
273
|
if (!updated) {
|
|
@@ -415,10 +277,6 @@ export class DagRunner {
|
|
|
415
277
|
if (updated.status !== "closed") {
|
|
416
278
|
updated = await this.#store.close(issueId, "failure");
|
|
417
279
|
}
|
|
418
|
-
// 7b. Review phase.
|
|
419
|
-
if (review && updated.status === "closed") {
|
|
420
|
-
updated = await this.#maybeReview(updated, rootId, step);
|
|
421
|
-
}
|
|
422
280
|
// 8. Log to forum.
|
|
423
281
|
await this.#forum.post(`issue:${issueId}`, JSON.stringify({
|
|
424
282
|
step,
|
|
@@ -428,6 +286,16 @@ export class DagRunner {
|
|
|
428
286
|
outcome: updated.outcome,
|
|
429
287
|
elapsed_s: roundTo(elapsedS, 1),
|
|
430
288
|
}), "orchestrator");
|
|
289
|
+
if (hooks?.onStepEnd) {
|
|
290
|
+
await hooks.onStepEnd({
|
|
291
|
+
rootId,
|
|
292
|
+
step,
|
|
293
|
+
issueId,
|
|
294
|
+
exitCode,
|
|
295
|
+
elapsedS: roundTo(elapsedS, 3),
|
|
296
|
+
outcome: updated.outcome ?? null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
431
299
|
await this.#events.emit("dag.step.end", {
|
|
432
300
|
source: "dag_runner",
|
|
433
301
|
issueId,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
export type { DagResult } from "./dag_runner.js";
|
|
1
|
+
export type { DagResult, DagRunnerBackendLineEvent, DagRunnerHooks, DagRunnerRunOpts, DagRunnerStepEndEvent, DagRunnerStepStartEvent, } from "./dag_runner.js";
|
|
2
2
|
export { DagRunner } from "./dag_runner.js";
|
|
3
|
+
export type { ModelOverrides, ResolvedModelConfig } from "./model_resolution.js";
|
|
4
|
+
export { resolveModelConfig } from "./model_resolution.js";
|
|
3
5
|
export type { BackendRunner, BackendRunOpts } from "./pi_backend.js";
|
|
4
6
|
export { PiCliBackend, piStreamHasError } from "./pi_backend.js";
|
|
5
|
-
export { PiSdkBackend } from "./pi_sdk_backend.js";
|
|
7
|
+
export { createMuResourceLoader, PiSdkBackend } from "./pi_sdk_backend.js";
|
|
8
|
+
export type { PiStreamRendererOpts } from "./pi_stream_renderer.js";
|
|
9
|
+
export { PiStreamRenderer } from "./pi_stream_renderer.js";
|
|
6
10
|
export type { PromptMeta } from "./prompt.js";
|
|
7
11
|
export { buildRoleCatalog, extractDescription, readPromptMeta, renderPromptTemplate, splitFrontmatter, } from "./prompt.js";
|
|
8
12
|
export declare function orchestratorHello(): string;
|
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,
|
|
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,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC3E,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,gBAAgB,GAChB,MAAM,aAAa,CAAC;AAGrB,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { DagRunner } from "./dag_runner.js";
|
|
2
|
+
export { resolveModelConfig } from "./model_resolution.js";
|
|
2
3
|
export { PiCliBackend, piStreamHasError } from "./pi_backend.js";
|
|
3
|
-
export { PiSdkBackend } from "./pi_sdk_backend.js";
|
|
4
|
+
export { createMuResourceLoader, PiSdkBackend } from "./pi_sdk_backend.js";
|
|
5
|
+
export { PiStreamRenderer } from "./pi_stream_renderer.js";
|
|
4
6
|
export { buildRoleCatalog, extractDescription, readPromptMeta, renderPromptTemplate, splitFrontmatter, } from "./prompt.js";
|
|
5
7
|
// Back-compat placeholder API used by other packages/tests.
|
|
6
8
|
export function orchestratorHello() {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
export type ModelOverrides = {
|
|
3
|
+
model?: string;
|
|
4
|
+
provider?: string;
|
|
5
|
+
reasoning?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ResolvedModelConfig = {
|
|
8
|
+
cli: string;
|
|
9
|
+
model: string;
|
|
10
|
+
reasoning: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve model configuration from overrides and authenticated providers.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. Explicit --model: find across providers (prefer auth'd)
|
|
17
|
+
* 2. Explicit --provider only: pick best model from that provider
|
|
18
|
+
* 3. Auto-detect: best model from any auth'd provider
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveModelConfig(overrides: ModelOverrides, authStorage?: AuthStorage): ResolvedModelConfig;
|
|
21
|
+
//# sourceMappingURL=model_resolution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model_resolution.d.ts","sourceRoot":"","sources":["../src/model_resolution.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CAClB,CAAC;AAmBF;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,cAAc,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,mBAAmB,CAY5G"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getModels, getProviders, supportsXhigh } from "@mariozechner/pi-ai";
|
|
2
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
/**
|
|
4
|
+
* Rank models by capability: reasoning > output cost (proxy for tier) > context window.
|
|
5
|
+
*/
|
|
6
|
+
function rankModel(m) {
|
|
7
|
+
const reasoningScore = m.reasoning ? 1_000_000 : 0;
|
|
8
|
+
const costScore = (m.cost?.output ?? 0) * 1_000;
|
|
9
|
+
const ctxScore = (m.contextWindow ?? 0) / 1_000_000;
|
|
10
|
+
return reasoningScore + costScore + ctxScore;
|
|
11
|
+
}
|
|
12
|
+
function pickReasoning(model, explicit) {
|
|
13
|
+
if (explicit)
|
|
14
|
+
return explicit;
|
|
15
|
+
if (supportsXhigh(model))
|
|
16
|
+
return "xhigh";
|
|
17
|
+
if (model.reasoning)
|
|
18
|
+
return "high";
|
|
19
|
+
return "high";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve model configuration from overrides and authenticated providers.
|
|
23
|
+
*
|
|
24
|
+
* Resolution order:
|
|
25
|
+
* 1. Explicit --model: find across providers (prefer auth'd)
|
|
26
|
+
* 2. Explicit --provider only: pick best model from that provider
|
|
27
|
+
* 3. Auto-detect: best model from any auth'd provider
|
|
28
|
+
*/
|
|
29
|
+
export function resolveModelConfig(overrides, authStorage) {
|
|
30
|
+
const auth = authStorage ?? new AuthStorage();
|
|
31
|
+
if (overrides.model) {
|
|
32
|
+
return resolveExplicitModel(overrides.model, overrides.provider, overrides.reasoning, auth);
|
|
33
|
+
}
|
|
34
|
+
if (overrides.provider) {
|
|
35
|
+
return resolveFromProvider(overrides.provider, overrides.reasoning, auth);
|
|
36
|
+
}
|
|
37
|
+
return autoDetect(overrides.reasoning, auth);
|
|
38
|
+
}
|
|
39
|
+
function resolveExplicitModel(modelId, providerConstraint, reasoningOverride, auth) {
|
|
40
|
+
let fallback;
|
|
41
|
+
for (const provider of getProviders()) {
|
|
42
|
+
if (providerConstraint && provider !== providerConstraint)
|
|
43
|
+
continue;
|
|
44
|
+
const models = getModels(provider);
|
|
45
|
+
const match = models.find((m) => m.id === modelId);
|
|
46
|
+
if (!match)
|
|
47
|
+
continue;
|
|
48
|
+
if (auth.hasAuth(provider)) {
|
|
49
|
+
return { cli: "pi", model: match.id, reasoning: pickReasoning(match, reasoningOverride) };
|
|
50
|
+
}
|
|
51
|
+
if (!fallback) {
|
|
52
|
+
fallback = match;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (fallback) {
|
|
56
|
+
return { cli: "pi", model: fallback.id, reasoning: pickReasoning(fallback, reasoningOverride) };
|
|
57
|
+
}
|
|
58
|
+
const scope = providerConstraint ? ` in provider "${providerConstraint}"` : "";
|
|
59
|
+
throw new Error(`Model "${modelId}" not found${scope}. Run \`mu login --list\` to see available providers.`);
|
|
60
|
+
}
|
|
61
|
+
function resolveFromProvider(providerId, reasoningOverride, auth) {
|
|
62
|
+
const providers = getProviders();
|
|
63
|
+
if (!providers.includes(providerId)) {
|
|
64
|
+
throw new Error(`Unknown provider "${providerId}". Available: ${providers.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
if (!auth.hasAuth(providerId)) {
|
|
67
|
+
throw new Error(`No auth for provider "${providerId}". Run \`mu login ${providerId}\``);
|
|
68
|
+
}
|
|
69
|
+
const models = getModels(providerId);
|
|
70
|
+
if (models.length === 0) {
|
|
71
|
+
throw new Error(`No models available for provider "${providerId}".`);
|
|
72
|
+
}
|
|
73
|
+
const best = [...models].sort((a, b) => rankModel(b) - rankModel(a))[0];
|
|
74
|
+
return { cli: "pi", model: best.id, reasoning: pickReasoning(best, reasoningOverride) };
|
|
75
|
+
}
|
|
76
|
+
function autoDetect(reasoningOverride, auth) {
|
|
77
|
+
const authedModels = [];
|
|
78
|
+
for (const provider of getProviders()) {
|
|
79
|
+
if (!auth.hasAuth(provider))
|
|
80
|
+
continue;
|
|
81
|
+
authedModels.push(...getModels(provider));
|
|
82
|
+
}
|
|
83
|
+
if (authedModels.length === 0) {
|
|
84
|
+
throw new Error("No authenticated providers. Run `mu login` to authenticate.");
|
|
85
|
+
}
|
|
86
|
+
const best = authedModels.sort((a, b) => rankModel(b) - rankModel(a))[0];
|
|
87
|
+
return { cli: "pi", model: best.id, reasoning: pickReasoning(best, reasoningOverride) };
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mu_roles.d.ts","sourceRoot":"","sources":["../src/mu_roles.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE/C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAanE;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BxD"}
|
package/dist/mu_roles.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function parseMuRole(role) {
|
|
2
|
+
if (role == null) {
|
|
3
|
+
return "orchestrator";
|
|
4
|
+
}
|
|
5
|
+
const trimmed = role.trim();
|
|
6
|
+
if (trimmed === "orchestrator" || trimmed === "worker") {
|
|
7
|
+
return trimmed;
|
|
8
|
+
}
|
|
9
|
+
throw new Error(`unsupported execution_spec.role=${JSON.stringify(trimmed)} (only "orchestrator" and "worker" are supported)`);
|
|
10
|
+
}
|
|
11
|
+
export function systemPromptForRole(role) {
|
|
12
|
+
if (role === "orchestrator") {
|
|
13
|
+
return [
|
|
14
|
+
"You are mu's orchestrator.",
|
|
15
|
+
"",
|
|
16
|
+
"Responsibilities:",
|
|
17
|
+
"- Decompose the assigned goal into small, concrete child issues.",
|
|
18
|
+
"- Decide ordering using dependencies (e.g. blocks) and keep work items atomic.",
|
|
19
|
+
"- Do not implement code changes directly; delegate execution to the worker role.",
|
|
20
|
+
"- Keep plans deterministic and minimal.",
|
|
21
|
+
"",
|
|
22
|
+
"Role rules:",
|
|
23
|
+
"- Use only the roles: orchestrator, worker.",
|
|
24
|
+
"- Assign executable leaves to role=worker.",
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
return [
|
|
28
|
+
"You are mu's worker.",
|
|
29
|
+
"",
|
|
30
|
+
"Responsibilities:",
|
|
31
|
+
"- Execute exactly one atomic issue end-to-end.",
|
|
32
|
+
"- Keep scope tight to the issue specification.",
|
|
33
|
+
"- Verify results (tests/typecheck/etc) and report what changed.",
|
|
34
|
+
"- Close with a terminal outcome: success, failure, or skipped.",
|
|
35
|
+
"",
|
|
36
|
+
"Role rules:",
|
|
37
|
+
"- Use only the roles: orchestrator, worker.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
package/dist/pi_backend.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { MuRole } from "./mu_roles.js";
|
|
1
2
|
export type BackendRunOpts = {
|
|
2
3
|
issueId: string;
|
|
4
|
+
role: MuRole;
|
|
5
|
+
systemPrompt: string;
|
|
3
6
|
prompt: string;
|
|
4
7
|
model: string;
|
|
5
8
|
thinking: string;
|
|
6
9
|
cwd: string;
|
|
7
10
|
cli: string;
|
|
8
|
-
promptPath: string | null;
|
|
9
11
|
logSuffix: string;
|
|
10
12
|
onLine?: (line: string) => void;
|
|
11
13
|
teePath?: string;
|
package/dist/pi_backend.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pi_backend.d.ts","sourceRoot":"","sources":["../src/pi_backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pi_backend.d.ts","sourceRoot":"","sources":["../src/pi_backend.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,MAAM,cAAc,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,WAAW,aAAa;IAC7B,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA4BtD;AAED,qBAAa,YAAa,YAAW,aAAa;;IAKpC,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;CAuEvD"}
|
package/dist/pi_backend.js
CHANGED
|
@@ -53,6 +53,7 @@ export class PiCliBackend {
|
|
|
53
53
|
proc.stdout?.pipe(merged);
|
|
54
54
|
proc.stderr?.pipe(merged);
|
|
55
55
|
let sawAssistantError = false;
|
|
56
|
+
const DELTA_TYPES = /^(?:thinking_delta|toolcall_delta|text_delta)$/;
|
|
56
57
|
const rl = createInterface({ input: merged, crlfDelay: Number.POSITIVE_INFINITY });
|
|
57
58
|
const readLoop = (async () => {
|
|
58
59
|
for await (const line of rl) {
|
|
@@ -62,8 +63,21 @@ export class PiCliBackend {
|
|
|
62
63
|
}
|
|
63
64
|
opts.onLine?.(trimmed);
|
|
64
65
|
if (teeFh) {
|
|
65
|
-
//
|
|
66
|
-
|
|
66
|
+
// Skip streaming deltas from the log — they carry the full
|
|
67
|
+
// accumulated message state on every token, causing quadratic
|
|
68
|
+
// log growth. Structural events are preserved.
|
|
69
|
+
let skip = false;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(trimmed);
|
|
72
|
+
const aType = parsed?.assistantMessageEvent?.type;
|
|
73
|
+
if (typeof aType === "string" && DELTA_TYPES.test(aType)) {
|
|
74
|
+
skip = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
if (!skip) {
|
|
79
|
+
await teeFh.write(`${trimmed}\n`);
|
|
80
|
+
}
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
83
|
})();
|
package/dist/pi_sdk_backend.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DefaultResourceLoader, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { BackendRunner, BackendRunOpts } from "./pi_backend.js";
|
|
2
3
|
/**
|
|
3
4
|
* In-process backend using the pi SDK.
|
|
4
5
|
*
|
|
@@ -8,4 +9,12 @@ import type { BackendRunOpts, BackendRunner } from "./pi_backend.js";
|
|
|
8
9
|
export declare class PiSdkBackend implements BackendRunner {
|
|
9
10
|
run(opts: BackendRunOpts): Promise<number>;
|
|
10
11
|
}
|
|
12
|
+
export type CreateMuResourceLoaderOpts = {
|
|
13
|
+
cwd: string;
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
agentDir?: string;
|
|
16
|
+
settingsManager?: SettingsManager;
|
|
17
|
+
additionalSkillPaths?: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function createMuResourceLoader(opts: CreateMuResourceLoaderOpts): DefaultResourceLoader;
|
|
11
20
|
//# sourceMappingURL=pi_sdk_backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pi_sdk_backend.d.ts","sourceRoot":"","sources":["../src/pi_sdk_backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pi_sdk_backend.d.ts","sourceRoot":"","sources":["../src/pi_sdk_backend.ts"],"names":[],"mappings":"AAMA,OAAO,EAWN,qBAAqB,EAErB,eAAe,EACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AA8BrE;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,aAAa;IAC3C,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;CAsGhD;AAED,MAAM,MAAM,0BAA0B,GAAG;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,0BAA0B,GAAG,qBAAqB,CAsB9F"}
|
package/dist/pi_sdk_backend.js
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { mkdir, open } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { SessionManager, SettingsManager, createAgentSession, createCodingTools, } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { getModels, getProviders } from "@mariozechner/pi-ai";
|
|
5
|
+
import { AuthStorage, createAgentSession, createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, DefaultResourceLoader, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
5
6
|
import { piStreamHasError } from "./pi_backend.js";
|
|
6
7
|
/**
|
|
7
|
-
* Resolve a bare model ID (e.g. "gpt-5.3-codex") to a pi-ai Model object
|
|
8
|
-
*
|
|
8
|
+
* Resolve a bare model ID (e.g. "gpt-5.3-codex") to a pi-ai Model object.
|
|
9
|
+
*
|
|
10
|
+
* When multiple providers offer the same model ID, prefer providers that
|
|
11
|
+
* have auth configured (env var, OAuth, or stored API key).
|
|
9
12
|
*/
|
|
10
|
-
function resolveModel(modelId) {
|
|
13
|
+
function resolveModel(modelId, authStorage) {
|
|
14
|
+
let fallback;
|
|
11
15
|
for (const provider of getProviders()) {
|
|
12
16
|
const models = getModels(provider);
|
|
13
17
|
const match = models.find((m) => m.id === modelId);
|
|
14
|
-
if (match)
|
|
18
|
+
if (!match)
|
|
19
|
+
continue;
|
|
20
|
+
// Prefer providers that have auth configured.
|
|
21
|
+
if (authStorage.hasAuth(provider)) {
|
|
15
22
|
return match;
|
|
23
|
+
}
|
|
24
|
+
// Keep first match as fallback.
|
|
25
|
+
if (!fallback) {
|
|
26
|
+
fallback = match;
|
|
27
|
+
}
|
|
16
28
|
}
|
|
17
|
-
return
|
|
29
|
+
return fallback;
|
|
18
30
|
}
|
|
19
31
|
/**
|
|
20
32
|
* In-process backend using the pi SDK.
|
|
@@ -24,17 +36,37 @@ function resolveModel(modelId) {
|
|
|
24
36
|
*/
|
|
25
37
|
export class PiSdkBackend {
|
|
26
38
|
async run(opts) {
|
|
27
|
-
const
|
|
39
|
+
const authStorage = new AuthStorage();
|
|
40
|
+
const model = resolveModel(opts.model, authStorage);
|
|
28
41
|
if (!model) {
|
|
29
|
-
throw new Error(`Model "${opts.model}" not found in pi-ai registry`);
|
|
42
|
+
throw new Error(`Model "${opts.model}" not found in pi-ai registry. ` + `Available providers: ${getProviders().join(", ")}`);
|
|
30
43
|
}
|
|
44
|
+
const settingsManager = SettingsManager.inMemory();
|
|
45
|
+
const resourceLoader = createMuResourceLoader({
|
|
46
|
+
cwd: opts.cwd,
|
|
47
|
+
systemPrompt: opts.systemPrompt,
|
|
48
|
+
settingsManager,
|
|
49
|
+
});
|
|
50
|
+
await resourceLoader.reload();
|
|
51
|
+
const tools = [
|
|
52
|
+
// Mu expects these built-in tools to exist at least for role=orchestrator.
|
|
53
|
+
createReadTool(opts.cwd),
|
|
54
|
+
createBashTool(opts.cwd),
|
|
55
|
+
createEditTool(opts.cwd),
|
|
56
|
+
createWriteTool(opts.cwd),
|
|
57
|
+
createGrepTool(opts.cwd),
|
|
58
|
+
createFindTool(opts.cwd),
|
|
59
|
+
createLsTool(opts.cwd),
|
|
60
|
+
];
|
|
31
61
|
const sessionOpts = {
|
|
32
62
|
cwd: opts.cwd,
|
|
33
63
|
model,
|
|
34
64
|
thinkingLevel: opts.thinking,
|
|
35
|
-
tools
|
|
65
|
+
tools,
|
|
36
66
|
sessionManager: SessionManager.inMemory(opts.cwd),
|
|
37
|
-
settingsManager
|
|
67
|
+
settingsManager,
|
|
68
|
+
resourceLoader,
|
|
69
|
+
authStorage,
|
|
38
70
|
};
|
|
39
71
|
const { session } = await createAgentSession(sessionOpts);
|
|
40
72
|
let teeFh = null;
|
|
@@ -56,23 +88,29 @@ export class PiSdkBackend {
|
|
|
56
88
|
onError: () => { },
|
|
57
89
|
});
|
|
58
90
|
let sawError = false;
|
|
91
|
+
const DELTA_TYPES = new Set(["thinking_delta", "toolcall_delta", "text_delta"]);
|
|
59
92
|
// Subscribe to events — serialize to JSONL for tee and error detection.
|
|
60
93
|
const unsub = session.subscribe((event) => {
|
|
61
94
|
const line = JSON.stringify(event);
|
|
62
95
|
if (piStreamHasError(line)) {
|
|
63
96
|
sawError = true;
|
|
64
97
|
}
|
|
98
|
+
// onLine gets everything (CLI needs deltas for live rendering).
|
|
65
99
|
opts.onLine?.(line);
|
|
100
|
+
// Tee file: skip streaming deltas (they carry the full accumulated
|
|
101
|
+
// message state on every token, causing quadratic log growth).
|
|
102
|
+
// Structural events (message_start/end, turn_start/end, tool_execution_*,
|
|
103
|
+
// thinking_start/end, toolcall_start/end) are kept.
|
|
66
104
|
if (teeFh) {
|
|
67
|
-
|
|
105
|
+
const aType = event?.assistantMessageEvent?.type;
|
|
106
|
+
if (!DELTA_TYPES.has(aType)) {
|
|
107
|
+
teeFh.write(`${line}\n`).catch(() => { });
|
|
108
|
+
}
|
|
68
109
|
}
|
|
69
110
|
});
|
|
70
111
|
try {
|
|
71
112
|
await session.prompt(opts.prompt, { expandPromptTemplates: false });
|
|
72
113
|
}
|
|
73
|
-
catch {
|
|
74
|
-
return 1;
|
|
75
|
-
}
|
|
76
114
|
finally {
|
|
77
115
|
unsub();
|
|
78
116
|
}
|
|
@@ -86,3 +124,24 @@ export class PiSdkBackend {
|
|
|
86
124
|
}
|
|
87
125
|
}
|
|
88
126
|
}
|
|
127
|
+
export function createMuResourceLoader(opts) {
|
|
128
|
+
const skillPaths = new Set();
|
|
129
|
+
for (const p of opts.additionalSkillPaths ?? []) {
|
|
130
|
+
skillPaths.add(p);
|
|
131
|
+
}
|
|
132
|
+
// If a repo has a top-level `skills/` dir (like workshop/), load it.
|
|
133
|
+
const repoSkills = join(opts.cwd, "skills");
|
|
134
|
+
if (existsSync(repoSkills)) {
|
|
135
|
+
skillPaths.add(repoSkills);
|
|
136
|
+
}
|
|
137
|
+
return new DefaultResourceLoader({
|
|
138
|
+
cwd: opts.cwd,
|
|
139
|
+
agentDir: opts.agentDir,
|
|
140
|
+
settingsManager: opts.settingsManager ?? SettingsManager.inMemory(),
|
|
141
|
+
additionalSkillPaths: [...skillPaths],
|
|
142
|
+
systemPromptOverride: (_base) => opts.systemPrompt,
|
|
143
|
+
agentsFilesOverride: (base) => ({
|
|
144
|
+
agentsFiles: base.agentsFiles.filter((f) => basename(f.path) === "AGENTS.md"),
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PiStreamRendererOpts = {
|
|
2
|
+
/**
|
|
3
|
+
* Render tool execution lifecycle events (start/end).
|
|
4
|
+
* Default: false (assistant text only).
|
|
5
|
+
*/
|
|
6
|
+
showToolEvents?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Render model "thinking" deltas (if present).
|
|
9
|
+
* Default: false.
|
|
10
|
+
*/
|
|
11
|
+
showThinking?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Minimal renderer for pi JSONL event streams (pi CLI `--mode json` and pi SDK AgentEvent JSON).
|
|
15
|
+
*
|
|
16
|
+
* Intended usage:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const r = new PiStreamRenderer();
|
|
20
|
+
* onLine: (line) => {
|
|
21
|
+
* const out = r.renderLine(line);
|
|
22
|
+
* if (out) process.stdout.write(out);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class PiStreamRenderer {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(opts?: PiStreamRendererOpts);
|
|
29
|
+
renderLine(line: string): string | null;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=pi_stream_renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi_stream_renderer.d.ts","sourceRoot":"","sources":["../src/pi_stream_renderer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG;IAClC;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAgB;;gBAKhB,IAAI,GAAE,oBAAyB;IAK3C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAkFvC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal renderer for pi JSONL event streams (pi CLI `--mode json` and pi SDK AgentEvent JSON).
|
|
3
|
+
*
|
|
4
|
+
* Intended usage:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const r = new PiStreamRenderer();
|
|
8
|
+
* onLine: (line) => {
|
|
9
|
+
* const out = r.renderLine(line);
|
|
10
|
+
* if (out) process.stdout.write(out);
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export class PiStreamRenderer {
|
|
15
|
+
#showToolEvents;
|
|
16
|
+
#showThinking;
|
|
17
|
+
#needsNewline = false;
|
|
18
|
+
constructor(opts = {}) {
|
|
19
|
+
this.#showToolEvents = opts.showToolEvents ?? false;
|
|
20
|
+
this.#showThinking = opts.showThinking ?? false;
|
|
21
|
+
}
|
|
22
|
+
renderLine(line) {
|
|
23
|
+
let event;
|
|
24
|
+
try {
|
|
25
|
+
event = JSON.parse(line);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Mixed stdout/stderr can include plain-text warnings; surface them.
|
|
29
|
+
if (!line)
|
|
30
|
+
return null;
|
|
31
|
+
this.#needsNewline = false;
|
|
32
|
+
return line.endsWith("\n") ? line : `${line}\n`;
|
|
33
|
+
}
|
|
34
|
+
const type = event?.type;
|
|
35
|
+
if (type === "message_update") {
|
|
36
|
+
const assistantEvent = event?.assistantMessageEvent;
|
|
37
|
+
if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
|
|
38
|
+
const delta = assistantEvent.delta;
|
|
39
|
+
if (!delta)
|
|
40
|
+
return null;
|
|
41
|
+
this.#needsNewline = !delta.endsWith("\n");
|
|
42
|
+
return delta;
|
|
43
|
+
}
|
|
44
|
+
if (this.#showThinking &&
|
|
45
|
+
assistantEvent?.type === "thinking_delta" &&
|
|
46
|
+
typeof assistantEvent.delta === "string") {
|
|
47
|
+
const delta = assistantEvent.delta;
|
|
48
|
+
if (!delta)
|
|
49
|
+
return null;
|
|
50
|
+
this.#needsNewline = !delta.endsWith("\n");
|
|
51
|
+
return delta;
|
|
52
|
+
}
|
|
53
|
+
if (assistantEvent?.type === "error") {
|
|
54
|
+
let out = "";
|
|
55
|
+
if (this.#needsNewline) {
|
|
56
|
+
out += "\n";
|
|
57
|
+
this.#needsNewline = false;
|
|
58
|
+
}
|
|
59
|
+
const reason = typeof assistantEvent.reason === "string" ? assistantEvent.reason : "error";
|
|
60
|
+
out += `[assistant:${reason}]\n`;
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (type === "message_end") {
|
|
66
|
+
const msg = event?.message;
|
|
67
|
+
if (msg && typeof msg === "object" && msg.role === "assistant") {
|
|
68
|
+
if (this.#needsNewline) {
|
|
69
|
+
this.#needsNewline = false;
|
|
70
|
+
return "\n";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (this.#showToolEvents && type === "tool_execution_start") {
|
|
76
|
+
const toolName = typeof event?.toolName === "string" ? event.toolName : "tool";
|
|
77
|
+
let out = "";
|
|
78
|
+
if (this.#needsNewline) {
|
|
79
|
+
out += "\n";
|
|
80
|
+
this.#needsNewline = false;
|
|
81
|
+
}
|
|
82
|
+
out += `[tool] ${toolName}\n`;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
if (this.#showToolEvents && type === "tool_execution_end" && event?.isError === true) {
|
|
86
|
+
const toolName = typeof event?.toolName === "string" ? event.toolName : "tool";
|
|
87
|
+
let out = "";
|
|
88
|
+
if (this.#needsNewline) {
|
|
89
|
+
out += "\n";
|
|
90
|
+
this.#needsNewline = false;
|
|
91
|
+
}
|
|
92
|
+
out += `[tool:error] ${toolName}\n`;
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-orchestrator",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": ["dist/**"],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@femtomc/mu-core": "26.2.
|
|
16
|
-
"@femtomc/mu-forum": "26.2.
|
|
17
|
-
"@femtomc/mu-issue": "26.2.
|
|
15
|
+
"@femtomc/mu-core": "26.2.16",
|
|
16
|
+
"@femtomc/mu-forum": "26.2.16",
|
|
17
|
+
"@femtomc/mu-issue": "26.2.16",
|
|
18
18
|
"@mariozechner/pi-agent-core": "^0.52.12",
|
|
19
19
|
"@mariozechner/pi-coding-agent": "^0.52.12",
|
|
20
20
|
"@mariozechner/pi-ai": "^0.52.12"
|