@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.
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"dag_runner.d.ts","sourceRoot":"","sources":["../src/dag_runner.ts"],"names":[],"mappings":"AAIA,OAAO,EAEN,KAAK,QAAQ,EAMb,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,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIrD,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;AAuBF,qBAAa,SAAS;;gBAepB,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,CAAA;KAAO;IAiVpD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAW,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,SAAS,CAAC;CAkLrG"}
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"}
@@ -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, executionSpecFromDict, fsEventLogFromRepoRoot, getStorePaths, newRunId, runContext, } from "@femtomc/mu-core/node";
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
- let cli = this.#fallbackCli;
40
- let model = this.#fallbackModel;
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 #renderPrompt(issue, promptPath, rootId) {
85
- let rendered;
86
- if (promptPath && existsSync(promptPath)) {
87
- rendered = await renderPromptTemplate(promptPath, issue, { repoRoot: this.#repoRoot });
48
+ async #renderUserPrompt(issue, rootId, step) {
49
+ let rendered = issue.title ?? "";
50
+ if (issue.body) {
51
+ rendered += `\n\n${issue.body}`;
88
52
  }
89
- else {
90
- rendered = issue.title;
91
- if (issue.body) {
92
- rendered += `\n\n${issue.body}`;
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.#renderPrompt(issue, cfg.promptPath, rootId);
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
- #hasReviewer() {
144
- return existsSync(join(this.#repoRoot, ".mu", "roles", "reviewer.md"));
145
- }
146
- async #maybeReview(issue, rootId, step) {
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.#events.emit("dag.review.start", {
156
- source: "dag_runner",
157
- issueId,
158
- payload: { root_id: rootId, step },
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 review = opts.review ?? true;
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, review },
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 { exitCode, elapsedS } = await this.#executeBackend(repairIssue, cfg, rootId, {
375
- logSuffix: "unstick",
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 { exitCode, elapsedS } = await this.#executeBackend(issue, cfg, rootId);
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;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,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"}
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,4 @@
1
+ export type MuRole = "orchestrator" | "worker";
2
+ export declare function parseMuRole(role: string | null | undefined): MuRole;
3
+ export declare function systemPromptForRole(role: MuRole): string;
4
+ //# sourceMappingURL=mu_roles.d.ts.map
@@ -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"}
@@ -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
+ }
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"pi_backend.d.ts","sourceRoot":"","sources":["../src/pi_backend.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,cAAc,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,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,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,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;CA0DvD"}
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"}
@@ -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
- // Preserve Python's JSONL tee behavior: write one line per line.
66
- await teeFh.write(`${trimmed}\n`);
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
  })();
@@ -1,4 +1,5 @@
1
- import type { BackendRunOpts, BackendRunner } from "./pi_backend.js";
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":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAgBrE;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,aAAa;IAC3C,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;CAsEhD"}
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"}
@@ -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
- * by searching across all known providers.
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 undefined;
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 model = resolveModel(opts.model);
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: createCodingTools(opts.cwd),
65
+ tools,
36
66
  sessionManager: SessionManager.inMemory(opts.cwd),
37
- settingsManager: SettingsManager.inMemory(),
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
- teeFh.write(`${line}\n`).catch(() => { });
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.14",
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.14",
16
- "@femtomc/mu-forum": "26.2.14",
17
- "@femtomc/mu-issue": "26.2.14",
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"