programmatic subagents
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(pi-mill): break recursive boot cycle between pi and mill

pi → mill discovery → pi --list-models → pi (with extensions) → mill discovery → ∞

Remove spawnSync("mill", ["discovery"]) from extension boot path. Remove
pi --list-models subprocess from driver-pi model catalog. Both caused
infinite process spawning when pi loaded the pi-mill extension.

+83 -133
+1 -24
packages/driver-pi/src/public/index.api.ts
··· 1 - import * as Command from "@effect/platform/Command"; 2 - import * as BunContext from "@effect/platform-bun/BunContext"; 3 1 import { Effect } from "effect"; 4 2 import type { DriverCodec, DriverProcessConfig, DriverRegistration } from "@mill/core"; 5 - import { decodePiModelCatalogOutput } from "../pi.codec"; 6 3 import { makePiProcessDriver } from "../process-driver.effect"; 7 4 8 5 export interface CreatePiDriverRegistrationInput { ··· 10 7 readonly models?: ReadonlyArray<string>; 11 8 } 12 9 13 - const defaultModelCatalog = ( 14 - process: DriverProcessConfig, 15 - ): Effect.Effect<ReadonlyArray<string>, never> => { 16 - if (process.command !== "pi") { 17 - return Effect.succeed([]); 18 - } 19 - 20 - const listModelsEffect = Effect.provide( 21 - Command.string(Command.make(process.command, "--list-models")), 22 - BunContext.layer, 23 - ); 24 - 25 - return Effect.catchAll( 26 - Effect.flatMap(listModelsEffect, decodePiModelCatalogOutput), 27 - () => Effect.succeed([]), 28 - ); 29 - }; 30 - 31 10 export const createPiCodec = (input?: { 32 11 readonly process?: DriverProcessConfig; 33 12 readonly models?: ReadonlyArray<string>; ··· 38 17 }; 39 18 } 40 19 41 - const process = input?.process ?? createPiDriverConfig(); 42 - 43 20 return { 44 - modelCatalog: defaultModelCatalog(process), 21 + modelCatalog: Effect.succeed([]), 45 22 }; 46 23 }; 47 24
+81 -108
packages/pi-mill/index.ts
··· 1 - import { spawnSync } from "node:child_process"; 2 1 import * as fs from "node:fs"; 3 2 import * as os from "node:os"; 4 3 import * as path from "node:path"; ··· 82 81 prompt: string; 83 82 } 84 83 85 - const parseJsonObjectFromText = (text: string): Record<string, unknown> | undefined => { 86 - const candidates = text 87 - .split("\n") 88 - .map((line) => line.trim()) 89 - .filter((line) => line.length > 0) 90 - .reverse(); 91 84 92 - for (const candidate of candidates) { 93 - try { 94 - const parsed = JSON.parse(candidate) as unknown; 95 - if (typeof parsed === "object" && parsed !== null) { 96 - return parsed as Record<string, unknown>; 97 - } 98 - } catch { 99 - continue; 100 - } 101 - } 102 - 103 - return undefined; 104 - }; 105 - 106 - function readModelsFromMill(config: ExtensionConfig): string[] { 107 - const args = [...config.millArgs, "discovery", "--json"]; 108 - if (config.millRunsDir && config.millRunsDir.trim().length > 0) { 109 - args.push("--runs-dir", config.millRunsDir); 110 - } 111 - 112 - const result = spawnSync(config.millCommand, args, { 113 - encoding: "utf-8", 114 - shell: false, 115 - }); 116 - 117 - if (result.status !== 0) { 118 - return []; 119 - } 120 - 121 - const payload = parseJsonObjectFromText(result.stdout); 122 - if (!payload) { 123 - return []; 124 - } 125 - 126 - const drivers = payload.drivers; 127 - if (typeof drivers !== "object" || drivers === null) { 128 - return []; 129 - } 130 - 131 - const models = new Set<string>(); 132 - for (const entry of Object.values(drivers)) { 133 - if (typeof entry !== "object" || entry === null) { 134 - continue; 135 - } 136 - const values = (entry as { models?: unknown }).models; 137 - if (!Array.isArray(values)) { 138 - continue; 139 - } 140 - for (const value of values) { 141 - if (typeof value === "string" && value.trim().length > 0) { 142 - models.add(value); 143 - } 144 - } 145 - } 146 - 147 - return [...models]; 148 - } 149 85 150 86 function readEnabledModelsFallback(): string[] { 151 87 try { ··· 344 280 const observability = new ObservabilityStore(); 345 281 const registry = new RunRegistry(); 346 282 const widget = new FactoryWidget(); 347 - const modelsFromMill = readModelsFromMill(config); 348 - const fallbackModels = readEnabledModelsFallback(); 349 - const enabledModels = modelsFromMill.length > 0 ? modelsFromMill : fallbackModels; 350 - const modelsText = enabledModels.length > 0 ? enabledModels.join(", ") : "(none detected)"; 283 + // Model discovery is deferred to avoid a boot cycle: 284 + // pi → mill discovery → pi --list-models → pi (with extensions) → mill discovery → … 285 + const enabledModels = readEnabledModelsFallback(); 286 + const modelsText = enabledModels.length > 0 ? enabledModels.join(", ") : "(use mill discovery to list)"; 351 287 352 288 // Keep a reference to the current context for widget/notification updates 353 289 let currentCtx: ExtensionContext | undefined; ··· 558 494 }); 559 495 } 560 496 561 - // Wire completion: update observability, widget, and notify 497 + // Wire completion: persist state first, then UI updates + notification. 562 498 promise.then( 563 499 (summary) => { 500 + observability.setStatus( 501 + runId, 502 + summary.status === "done" 503 + ? "done" 504 + : summary.status === "cancelled" 505 + ? "cancelled" 506 + : "failed", 507 + ); 508 + 509 + const fullSummary: RunSummary = { 510 + ...summary, 511 + observability: observability.toSummary(runId), 512 + metadata: { 513 + task: params.task, 514 + millCommand: config.millCommand, 515 + millArgs: config.millArgs, 516 + millRunsDir: config.millRunsDir, 517 + }, 518 + }; 519 + 520 + registry.complete(runId, fullSummary); 521 + 564 522 try { 565 - observability.setStatus( 566 - runId, 567 - summary.status === "done" 568 - ? "done" 569 - : summary.status === "cancelled" 570 - ? "cancelled" 571 - : "failed", 572 - ); 573 - const fullSummary = { 574 - ...summary, 575 - observability: observability.toSummary(runId), 576 - metadata: { 577 - task: params.task, 578 - millCommand: config.millCommand, 579 - millArgs: config.millArgs, 580 - millRunsDir: config.millRunsDir, 581 - }, 582 - }; 583 - registry.complete(runId, fullSummary); 523 + writeRunJson(fullSummary); 524 + } catch (error) { 525 + observability.push(runId, "warning", "write_run_json_failed", { error: String(error) }); 526 + } 527 + 528 + try { 584 529 widget.update(registry.getVisible(), ctx); 530 + } catch { 531 + /* ui may be unavailable */ 532 + } 533 + 534 + try { 585 535 notifyCompletion(pi, registry, fullSummary); 586 - writeRunJson(fullSummary); 536 + } catch (error) { 537 + observability.push(runId, "warning", "notify_failed", { error: String(error) }); 538 + } 539 + 540 + try { 587 541 widget.update(registry.getVisible(), ctx); 588 542 } catch { 589 - /* shutting down */ 543 + /* ui may be unavailable */ 590 544 } 591 545 }, 592 546 (err) => { 547 + const details = toErrorDetails(err); 548 + observability.setStatus(runId, details.code === "CANCELLED" ? "cancelled" : "failed"); 549 + 550 + const failedSummary: RunSummary = { 551 + runId, 552 + status: "failed", 553 + results: [], 554 + error: details, 555 + observability: observability.toSummary(runId), 556 + metadata: { 557 + task: params.task, 558 + millCommand: config.millCommand, 559 + millArgs: config.millArgs, 560 + millRunsDir: config.millRunsDir, 561 + }, 562 + }; 563 + 564 + registry.fail(runId, details); 565 + 593 566 try { 594 - const details = toErrorDetails(err); 595 - observability.setStatus(runId, details.code === "CANCELLED" ? "cancelled" : "failed"); 596 - const failedSummary: RunSummary = { 597 - runId, 598 - status: "failed", 599 - results: [], 600 - error: details, 601 - observability: observability.toSummary(runId), 602 - metadata: { 603 - task: params.task, 604 - millCommand: config.millCommand, 605 - millArgs: config.millArgs, 606 - millRunsDir: config.millRunsDir, 607 - }, 608 - }; 609 - registry.fail(runId, details); 567 + writeRunJson(failedSummary); 568 + } catch (error) { 569 + observability.push(runId, "warning", "write_run_json_failed", { error: String(error) }); 570 + } 571 + 572 + try { 573 + widget.update(registry.getVisible(), ctx); 574 + } catch { 575 + /* ui may be unavailable */ 576 + } 577 + 578 + try { 610 579 notifyCompletion(pi, registry, failedSummary); 611 - writeRunJson(failedSummary); 580 + } catch (error) { 581 + observability.push(runId, "warning", "notify_failed", { error: String(error) }); 582 + } 583 + 584 + try { 612 585 widget.update(registry.getVisible(), ctx); 613 586 } catch { 614 - /* shutting down */ 587 + /* ui may be unavailable */ 615 588 } 616 589 }, 617 590 );
+1 -1
packages/pi-mill/package.json
··· 1 1 { 2 2 "name": "pi-mill", 3 - "version": "0.1.0", 3 + "version": "0.1.1", 4 4 "description": "Pi extension package that routes subagent execution through mill", 5 5 "keywords": [ 6 6 "pi-package",