@aeondave/pi-persona 1.15.2
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/AGENTS.md +249 -0
- package/CHANGELOG.md +260 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/agents/balthasar.md +32 -0
- package/agents/casper.md +33 -0
- package/agents/evidence-verifier.md +23 -0
- package/agents/melchior.md +32 -0
- package/agents/operator.md +63 -0
- package/agents/research.md +58 -0
- package/agents/reviewer.md +49 -0
- package/agents/scout.md +23 -0
- package/agents/verifier.md +21 -0
- package/assets/banner.png +0 -0
- package/assets/demo1.gif +0 -0
- package/assets/demo1.mp4 +0 -0
- package/assets/workflow.png +0 -0
- package/contracts/finding.contract.json +9 -0
- package/docs/ARCHITECTURE.md +727 -0
- package/docs/EXPERIENCE.md +96 -0
- package/docs/MONITORS.md +84 -0
- package/docs/README.md +53 -0
- package/docs/REFERENCE.md +408 -0
- package/docs/SPINE.md +152 -0
- package/docs/STRATEGIES.md +215 -0
- package/docs/TELEMETRY.md +9 -0
- package/flows/deep-review.flow.json +8 -0
- package/flows/gated-build.flow.json +9 -0
- package/package.json +106 -0
- package/personas/audit.md +29 -0
- package/personas/dev.md +52 -0
- package/personas/elite.md +256 -0
- package/personas/judge.md +32 -0
- package/personas/magi.md +41 -0
- package/personas/planner.md +37 -0
- package/personas/researcher.md +37 -0
- package/personas/swarm.md +38 -0
- package/personas/verify.md +31 -0
- package/presets/magi-rounds.preset.json +5 -0
- package/prompts/spine.md +11 -0
- package/prompts/spine.worker.md +9 -0
- package/src/agents/agent.ts +78 -0
- package/src/bridge.ts +209 -0
- package/src/bus/broker/client.ts +379 -0
- package/src/bus/broker/framing.ts +43 -0
- package/src/bus/broker/host.ts +407 -0
- package/src/bus/broker/messages.ts +87 -0
- package/src/bus/broker/paths.ts +31 -0
- package/src/bus/contact.ts +145 -0
- package/src/bus/inproc.ts +279 -0
- package/src/bus/peers.ts +114 -0
- package/src/core/brief.ts +356 -0
- package/src/core/capabilities.ts +114 -0
- package/src/core/config.ts +228 -0
- package/src/core/contract.ts +391 -0
- package/src/core/data-root.ts +260 -0
- package/src/core/discovery.ts +33 -0
- package/src/core/display-label.ts +47 -0
- package/src/core/fence.ts +151 -0
- package/src/core/format.ts +13 -0
- package/src/core/frontmatter.ts +202 -0
- package/src/core/models.ts +192 -0
- package/src/core/monitor.ts +415 -0
- package/src/core/naming.ts +42 -0
- package/src/core/nudge.ts +238 -0
- package/src/core/ownership.ts +102 -0
- package/src/core/permissions.ts +56 -0
- package/src/core/pi-compat.ts +50 -0
- package/src/core/seed.ts +770 -0
- package/src/core/session-identity.ts +89 -0
- package/src/core/time.ts +202 -0
- package/src/core/timer.ts +221 -0
- package/src/core/types.ts +28 -0
- package/src/engine/adapter.ts +257 -0
- package/src/engine/async.ts +898 -0
- package/src/engine/change-report.ts +143 -0
- package/src/engine/child.ts +532 -0
- package/src/engine/errors.ts +30 -0
- package/src/engine/fallback.ts +57 -0
- package/src/engine/handles.ts +21 -0
- package/src/engine/inproc.ts +857 -0
- package/src/engine/signals.ts +11 -0
- package/src/engine/spec-preflight.ts +33 -0
- package/src/engine/stream.ts +369 -0
- package/src/engine/worktree.ts +144 -0
- package/src/exocom/activation.ts +31 -0
- package/src/exocom/codes.ts +261 -0
- package/src/exocom/envelope.ts +180 -0
- package/src/exocom/gate.ts +6 -0
- package/src/exocom/guards.ts +45 -0
- package/src/exocom/inbound.ts +141 -0
- package/src/exocom/install.ts +970 -0
- package/src/exocom/ledger.ts +603 -0
- package/src/exocom/limits.ts +20 -0
- package/src/exocom/paths.ts +35 -0
- package/src/exocom/plane.ts +1116 -0
- package/src/exocom/registry.ts +307 -0
- package/src/exocom/scope.ts +42 -0
- package/src/exocom/status.ts +86 -0
- package/src/exocom/untrusted.ts +40 -0
- package/src/exocom/wait.ts +23 -0
- package/src/exocom/write-guard.ts +89 -0
- package/src/extension/broker-host.ts +181 -0
- package/src/extension/clock.ts +14 -0
- package/src/extension/engine.ts +321 -0
- package/src/extension/hooks.ts +796 -0
- package/src/extension/identity.ts +244 -0
- package/src/extension/shared.ts +175 -0
- package/src/extension.ts +2221 -0
- package/src/loader.ts +218 -0
- package/src/monitor/process.ts +116 -0
- package/src/monitor/session.ts +61 -0
- package/src/orchestration/flow-journal.ts +86 -0
- package/src/orchestration/flow-run.ts +301 -0
- package/src/orchestration/flow.ts +125 -0
- package/src/orchestration/judge.ts +78 -0
- package/src/orchestration/model-retry.ts +85 -0
- package/src/orchestration/parallel.ts +107 -0
- package/src/orchestration/params.ts +23 -0
- package/src/orchestration/reducers.ts +123 -0
- package/src/orchestration/render.ts +127 -0
- package/src/orchestration/roster.ts +133 -0
- package/src/orchestration/sdk.ts +326 -0
- package/src/orchestration/strategies/compete.ts +182 -0
- package/src/orchestration/strategies/council-rounds.ts +124 -0
- package/src/orchestration/strategies/critic-loop.ts +168 -0
- package/src/orchestration/strategies/debate.ts +126 -0
- package/src/orchestration/strategies/fanout.ts +20 -0
- package/src/orchestration/strategies/judge.ts +117 -0
- package/src/orchestration/strategies/magi.ts +239 -0
- package/src/orchestration/strategies/map.ts +327 -0
- package/src/orchestration/strategies/pair.ts +72 -0
- package/src/orchestration/strategies/pipeline.ts +91 -0
- package/src/orchestration/strategies/synthesize.ts +99 -0
- package/src/orchestration/strategy.ts +49 -0
- package/src/orchestration/types.ts +31 -0
- package/src/orchestration/voting.ts +126 -0
- package/src/persona/config-store.ts +215 -0
- package/src/persona/controller.ts +173 -0
- package/src/persona/gating.ts +82 -0
- package/src/persona/orchestrate.ts +83 -0
- package/src/persona/persona.ts +294 -0
- package/src/persona/spine.ts +221 -0
- package/src/persona/state.ts +28 -0
- package/src/telemetry/contract.ts +244 -0
- package/src/telemetry/paths.ts +14 -0
- package/src/telemetry/producer.ts +390 -0
- package/src/tools/council.ts +200 -0
- package/src/tools/delegate-tool.ts +637 -0
- package/src/tools/delegate.ts +661 -0
- package/src/tools/exocom-work.ts +328 -0
- package/src/tools/exocom.ts +476 -0
- package/src/tools/flow.ts +117 -0
- package/src/tools/intercom-tool.ts +292 -0
- package/src/tools/intercom.ts +181 -0
- package/src/tools/models.ts +39 -0
- package/src/tools/monitor.ts +66 -0
- package/src/tools/timer.ts +105 -0
- package/src/ui/agent-overlay.ts +526 -0
- package/src/ui/agent-tree.ts +307 -0
- package/src/ui/live-clock.ts +42 -0
- package/src/ui/model-picker.ts +160 -0
- package/src/ui/presentation.ts +170 -0
- package/src/ui/usage.ts +133 -0
- package/teams.yaml +27 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisor-side broker lifecycle (spec B1-B7): the ONE cross-process host every child-engine
|
|
3
|
+
* leg connects to. Started lazily on the first child-engine build; a failed bind is reported to
|
|
4
|
+
* the user ONCE, children built while it is failed spawn without a bus endpoint (so they never
|
|
5
|
+
* burn the connect backoff against a dead socket), and the next build retries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
import type { EngineAdapterBroker } from "../engine/adapter.ts";
|
|
12
|
+
import { type BrokerHost, startBrokerHost } from "../bus/broker/host.ts";
|
|
13
|
+
import type { InProcessBus } from "../bus/inproc.ts";
|
|
14
|
+
|
|
15
|
+
export interface SupervisorBrokerDeps {
|
|
16
|
+
bus: InProcessBus;
|
|
17
|
+
supervisorHandle: string;
|
|
18
|
+
/** Injected in tests. */
|
|
19
|
+
startHost?: typeof startBrokerHost;
|
|
20
|
+
/** User-facing warning (ctx.ui.notify "warning" when a TUI is present, stderr otherwise). */
|
|
21
|
+
warn: (message: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Cross-process `contact_peer` roster (spec B7): scopes `brokerPeers` — the process-wide,
|
|
25
|
+
* pre-spawn registry keyed by handle (populated in `SupervisorBroker.adapterDeps`'s `register`,
|
|
26
|
+
* below) — to the SAME per-engine group as the caller `self`, mirroring `engine/inproc.ts`'s
|
|
27
|
+
* per-engine-instance `peerLabels` map. `self`'s OWN recorded group is the source of truth
|
|
28
|
+
* here, NOT the wire's `group` argument the host would otherwise pass: the child's env
|
|
29
|
+
* carries no group (spec B6, the wire `register` frame stays minimal), so every wire group
|
|
30
|
+
* is "" and scoping by it would either always come back empty (this scope) or leak every
|
|
31
|
+
* concurrent run's peers into one flat list (the host's own default `group=""` scoping).
|
|
32
|
+
* `self` not found (not registered with `peers: true`) ⇒ empty roster, never a leak.
|
|
33
|
+
* Exported for direct unit/integration testing — `extension.ts`'s activation closure itself
|
|
34
|
+
* isn't a testable unit. */
|
|
35
|
+
export function listPeersForGroup(brokerPeers: ReadonlyMap<string, { label: string; group: string }>, self: string): Array<{ handle: string; label: string }> {
|
|
36
|
+
const g = brokerPeers.get(self)?.group;
|
|
37
|
+
if (g === undefined) return [];
|
|
38
|
+
return [...brokerPeers.entries()]
|
|
39
|
+
.filter(([handle, p]) => p.group === g && handle !== self)
|
|
40
|
+
.map(([handle, p]) => ({ handle, label: p.label }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class SupervisorBroker {
|
|
44
|
+
readonly peers = new Map<string, { label: string; group: string }>();
|
|
45
|
+
private hostRef: BrokerHost | undefined;
|
|
46
|
+
private promise: Promise<BrokerHost> | undefined;
|
|
47
|
+
private lastError: string | undefined;
|
|
48
|
+
private warned = false;
|
|
49
|
+
private readonly expected = new Set<string>();
|
|
50
|
+
private readonly preHostSteers = new Map<string, string[]>();
|
|
51
|
+
private readonly deps: SupervisorBrokerDeps;
|
|
52
|
+
|
|
53
|
+
constructor(deps: SupervisorBrokerDeps) {
|
|
54
|
+
this.deps = deps;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get host(): BrokerHost | undefined {
|
|
58
|
+
return this.hostRef;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get error(): string | undefined {
|
|
62
|
+
return this.lastError;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The lifecycle state `adapterDeps`, `steerFrame`, and `doctorLine` each branch on, named
|
|
66
|
+
* once so they can never read it as three different ad hoc conditions. */
|
|
67
|
+
private state(): "idle" | "starting" | "up" | "failed" {
|
|
68
|
+
if (this.hostRef) return "up";
|
|
69
|
+
if (this.lastError !== undefined) return "failed";
|
|
70
|
+
if (this.promise) return "starting";
|
|
71
|
+
return "idle";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Start the host once per endpoint attempt; retried on the next call after a failure. */
|
|
75
|
+
ensure(endpoint: string): void {
|
|
76
|
+
if (this.promise) return;
|
|
77
|
+
if (process.platform !== "win32") {
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(dirname(endpoint), { recursive: true }); // POSIX sockets are filesystem paths
|
|
80
|
+
} catch {
|
|
81
|
+
/* a failed mkdir surfaces as a listen error below */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const start = this.deps.startHost ?? startBrokerHost;
|
|
85
|
+
this.promise = start({
|
|
86
|
+
bus: this.deps.bus,
|
|
87
|
+
supervisorHandle: this.deps.supervisorHandle,
|
|
88
|
+
endpoint,
|
|
89
|
+
listPeersFor: (_group, self) => listPeersForGroup(this.peers, self),
|
|
90
|
+
});
|
|
91
|
+
this.promise.then(
|
|
92
|
+
(h) => {
|
|
93
|
+
this.hostRef = h;
|
|
94
|
+
this.lastError = undefined;
|
|
95
|
+
this.warned = false;
|
|
96
|
+
for (const handle of this.expected) h.expect(handle);
|
|
97
|
+
for (const [handle, texts] of this.preHostSteers) for (const text of texts) h.steer(handle, text);
|
|
98
|
+
this.preHostSteers.clear();
|
|
99
|
+
},
|
|
100
|
+
(err) => {
|
|
101
|
+
this.promise = undefined; // a later build retries
|
|
102
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
103
|
+
const dropped = [...this.preHostSteers.values()].reduce((n, texts) => n + texts.length, 0);
|
|
104
|
+
this.preHostSteers.clear();
|
|
105
|
+
if (!this.warned) {
|
|
106
|
+
this.warned = true;
|
|
107
|
+
this.deps.warn(
|
|
108
|
+
`pi-persona: child-agent bus unavailable — ${this.lastError}; child legs run without live steer/contact until it starts${dropped > 0 ? `; ${dropped} queued steer${dropped === 1 ? "" : "s"} ${dropped === 1 ? "was" : "were"} dropped` : ""}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The adapter-facing deps, or undefined while the last start failed (children spawn bus-less). */
|
|
116
|
+
adapterDeps(endpoint: string): EngineAdapterBroker | undefined {
|
|
117
|
+
const failedBefore = this.state() === "failed";
|
|
118
|
+
this.ensure(endpoint);
|
|
119
|
+
if (failedBefore) return undefined;
|
|
120
|
+
return {
|
|
121
|
+
endpoint,
|
|
122
|
+
register: (info) => {
|
|
123
|
+
this.deps.bus.register(info.handle);
|
|
124
|
+
this.expected.add(info.handle);
|
|
125
|
+
if (info.peers) this.peers.set(info.handle, { label: info.label ?? info.handle, group: info.group ?? "" });
|
|
126
|
+
this.hostRef?.expect(info.handle);
|
|
127
|
+
},
|
|
128
|
+
unregister: (handle) => {
|
|
129
|
+
this.expected.delete(handle);
|
|
130
|
+
this.preHostSteers.delete(handle);
|
|
131
|
+
this.peers.delete(handle);
|
|
132
|
+
this.deps.bus.unregister(handle);
|
|
133
|
+
this.hostRef?.forget(handle);
|
|
134
|
+
},
|
|
135
|
+
steerFrame: (handle, text) => {
|
|
136
|
+
if (this.hostRef) return this.hostRef.steer(handle, text);
|
|
137
|
+
if (this.state() === "failed" || !this.expected.has(handle) || !text.trim()) return false;
|
|
138
|
+
const queued = this.preHostSteers.get(handle) ?? [];
|
|
139
|
+
queued.push(text);
|
|
140
|
+
this.preHostSteers.set(handle, queued);
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
143
|
+
hasPendingAskFrom: (handle) => this.deps.bus.hasPendingAskFrom(handle),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
doctorLine(): string {
|
|
148
|
+
// Failed is reported on its own (spec §4.7: `broker: failed — <reason>`) rather than folded
|
|
149
|
+
// into the "on — …" family: there is no host and no connected-children count to report while
|
|
150
|
+
// down, so "broker: on — failed — …" was a confusing double dash for a state that isn't "on".
|
|
151
|
+
if (this.state() === "failed") return `broker: failed — ${this.lastError}`;
|
|
152
|
+
let status: string;
|
|
153
|
+
switch (this.state()) {
|
|
154
|
+
case "up":
|
|
155
|
+
status = `endpoint ${this.hostRef?.endpoint}`;
|
|
156
|
+
break;
|
|
157
|
+
case "starting":
|
|
158
|
+
status = "(starting…)";
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
status = "(not started — no child-engine build yet)";
|
|
162
|
+
}
|
|
163
|
+
return `broker: on — ${status}, connected children: ${this.hostRef?.connectedHandles().length ?? 0}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async close(): Promise<void> {
|
|
167
|
+
if (this.promise) {
|
|
168
|
+
try {
|
|
169
|
+
const h = await this.promise;
|
|
170
|
+
await h.close();
|
|
171
|
+
} catch {
|
|
172
|
+
/* best-effort — never block shutdown on a broker teardown error */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.hostRef = undefined;
|
|
176
|
+
this.promise = undefined;
|
|
177
|
+
this.peers.clear();
|
|
178
|
+
this.expected.clear();
|
|
179
|
+
this.preHostSteers.clear();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Resolve host-local facts outside the pure clock formatter. */
|
|
2
|
+
import { buildClockSnapshot } from "../core/time.ts";
|
|
3
|
+
|
|
4
|
+
export function readClockSnapshot(now: number): string | undefined {
|
|
5
|
+
const date = new Date(now);
|
|
6
|
+
if (!Number.isFinite(date.getTime())) return undefined;
|
|
7
|
+
let timeZone = "local timezone";
|
|
8
|
+
try {
|
|
9
|
+
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || timeZone;
|
|
10
|
+
} catch {
|
|
11
|
+
// The numeric offset still identifies local time when an IANA name is unavailable.
|
|
12
|
+
}
|
|
13
|
+
return buildClockSnapshot(now, { timeZone, offsetMinutes: -date.getTimezoneOffset() });
|
|
14
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine construction: in-process vs child, worktree isolation, MCP-child routing, provider fallback.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import type { AgentConfig } from "../agents/agent.ts";
|
|
7
|
+
import { type ContractDef, DEFAULT_CONTRACT, installedContractNames } from "../core/contract.ts";
|
|
8
|
+
import type { RunLimits } from "../core/capabilities.ts";
|
|
9
|
+
import type { PiPersonaConfig } from "../core/config.ts";
|
|
10
|
+
import { isThinkingLevel } from "../core/types.ts";
|
|
11
|
+
import { personaModels, type PersonaConfigStore } from "../persona/config-store.ts";
|
|
12
|
+
import type { PersonaController, PersonaHost } from "../persona/controller.ts";
|
|
13
|
+
import { type EngineAdapterBroker, type EngineAdapterDeps, makeEngine } from "../engine/adapter.ts";
|
|
14
|
+
import { captureStatus, diffStatus, findGitRoot, renderChangeReport } from "../engine/change-report.ts";
|
|
15
|
+
import { withModelFallback } from "../engine/fallback.ts";
|
|
16
|
+
import { captureWorktreeArtifact, defaultGitExec, type GitExec, withWorktree, worktreePreflight } from "../engine/worktree.ts";
|
|
17
|
+
import { type InProcessDeps, makeInProcessEngine } from "../engine/inproc.ts";
|
|
18
|
+
import { emptyUsage, type ProgressSnapshot } from "../engine/stream.ts";
|
|
19
|
+
import type { AgentRunSpec, StrategyEngine } from "../orchestration/sdk.ts";
|
|
20
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
21
|
+
import type { InProcessBus } from "../bus/inproc.ts";
|
|
22
|
+
|
|
23
|
+
export interface EngineFactories {
|
|
24
|
+
makeEngine: typeof makeEngine;
|
|
25
|
+
makeInProcessEngine: typeof makeInProcessEngine;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_ENGINE_FACTORIES: EngineFactories = Object.freeze({ makeEngine, makeInProcessEngine });
|
|
29
|
+
|
|
30
|
+
export interface BuildEngineDeps {
|
|
31
|
+
agents: AgentConfig[];
|
|
32
|
+
contractDefs: Record<string, ContractDef>;
|
|
33
|
+
controller: PersonaController;
|
|
34
|
+
host: PersonaHost;
|
|
35
|
+
config: PiPersonaConfig;
|
|
36
|
+
personaConfigs: PersonaConfigStore;
|
|
37
|
+
lastCtx: ExtensionContext | undefined;
|
|
38
|
+
workerSpineText: string;
|
|
39
|
+
engineFactories: EngineFactories;
|
|
40
|
+
makeBrokerDeps: (ctx: ExtensionContext) => EngineAdapterBroker | undefined;
|
|
41
|
+
userAgentDir: () => string;
|
|
42
|
+
childPiSettingsEnv: () => Record<string, string>;
|
|
43
|
+
runLimits: RunLimits;
|
|
44
|
+
bus: InProcessBus;
|
|
45
|
+
supervisorHandle: string;
|
|
46
|
+
/** Breadcrumb for a provider reroute (toast + log); absent ⇒ silent, as before. */
|
|
47
|
+
onFallback?: (info: { from: string; to: string; agent: string }) => void;
|
|
48
|
+
/** Injected git runner for the non-worktree files-changed report (tests only); defaults to
|
|
49
|
+
* `defaultGitExec`. */
|
|
50
|
+
gitExec?: GitExec;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type BuildEngine = (signal?: AbortSignal, onProgress?: (s: ProgressSnapshot) => void, engOpts?: { async?: boolean }) => StrategyEngine;
|
|
54
|
+
|
|
55
|
+
export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
|
|
56
|
+
return (signal, onProgress, engOpts) => {
|
|
57
|
+
const {
|
|
58
|
+
agents, contractDefs, controller, host, config, personaConfigs, lastCtx, workerSpineText,
|
|
59
|
+
engineFactories, makeBrokerDeps, userAgentDir, childPiSettingsEnv, runLimits: RUN_LIMITS,
|
|
60
|
+
bus, supervisorHandle: SUPERVISOR, onFallback,
|
|
61
|
+
} = d();
|
|
62
|
+
const resolveAgent = (n: string): AgentConfig | undefined => agents.find((a) => a.name === n);
|
|
63
|
+
// A named contract file (contracts/<name>.contract.json) wins; "default" is the built-in.
|
|
64
|
+
const contracts = (n: string): ContractDef | undefined => contractDefs[n] ?? (n === "default" ? DEFAULT_CONTRACT : undefined);
|
|
65
|
+
const contractNames = (): string[] => installedContractNames(contractDefs);
|
|
66
|
+
const modelFor = (agent: string): string | undefined => {
|
|
67
|
+
const persona = controller.activePersona?.name;
|
|
68
|
+
return persona ? personaModels(personaConfigs, persona)[agent] : undefined;
|
|
69
|
+
};
|
|
70
|
+
// The main model thinks adaptively (it picks effort by difficulty); a spawned child
|
|
71
|
+
// can't inherit "adaptive" if its model doesn't support it, so give children an
|
|
72
|
+
// explicit level — the supervisor's (if concrete) or a sane default, overridable.
|
|
73
|
+
const supLevel = host.getThinkingLevel();
|
|
74
|
+
const childThinking = config.childThinking ?? (isThinkingLevel(supLevel) ? supLevel : "high");
|
|
75
|
+
|
|
76
|
+
// A persona that opted out of the layer (`spine: false`) opts its legs out too — otherwise
|
|
77
|
+
// a short verdict persona (judge/verify/audit) would still pay for the baseline on every
|
|
78
|
+
// sub-agent it spawns. The per-AGENT opt-out applies on top, inside each engine.
|
|
79
|
+
const legSpine = controller.activePersona?.spine === false ? "" : workerSpineText;
|
|
80
|
+
|
|
81
|
+
// Cross-process broker (spec B1-B7): lazily built on the FIRST actual child-engine
|
|
82
|
+
// construction below (worktree leg, MCP leg, or `PI_PERSONA_ENGINE=child`) — NOT on every
|
|
83
|
+
// `buildEngine` call, most of which build the (default) in-process engine and never
|
|
84
|
+
// touch a child at all; starting a host for those would be neither lazy nor needed.
|
|
85
|
+
// Memoized so both call sites below share ONE broker object (and its `peerGroup`
|
|
86
|
+
// registration) per `buildEngine` invocation. `config.broker` off or no live
|
|
87
|
+
// `ctx` yet ⇒ stays undefined forever, so `deps.broker` is never set.
|
|
88
|
+
let brokerDepsMemo: EngineAdapterBroker | undefined;
|
|
89
|
+
let brokerDepsBuilt = false;
|
|
90
|
+
const getBrokerDeps = (): EngineAdapterBroker | undefined => {
|
|
91
|
+
if (!brokerDepsBuilt) {
|
|
92
|
+
brokerDepsBuilt = true;
|
|
93
|
+
const sessionId = (lastCtx as { sessionManager?: { getSessionId?: () => string } } | undefined)?.sessionManager?.getSessionId?.();
|
|
94
|
+
if (config.broker && lastCtx && sessionId) brokerDepsMemo = makeBrokerDeps(lastCtx);
|
|
95
|
+
}
|
|
96
|
+
return brokerDepsMemo;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// A child-process engine pinned to a specific cwd — the seam worktree isolation runs
|
|
100
|
+
// through (a worktree needs its own working dir, i.e. a separate process).
|
|
101
|
+
const childEngineAt = (cwd: string): StrategyEngine => {
|
|
102
|
+
const deps: EngineAdapterDeps = { resolveAgent, contracts, modelFor, childThinking, cwd };
|
|
103
|
+
deps.listAgents = () => agents.map((a) => a.name);
|
|
104
|
+
deps.listContracts = contractNames;
|
|
105
|
+
if (legSpine) deps.spine = legSpine; // legs get the worker variant (docs/SPINE.md)
|
|
106
|
+
if (signal) deps.signal = signal;
|
|
107
|
+
deps.childOptions = {
|
|
108
|
+
timeoutMs: RUN_LIMITS.timeoutMs,
|
|
109
|
+
hardTimeoutMs: config.agentHardTimeoutMs,
|
|
110
|
+
startupTimeoutMs: config.agentStartupTimeoutMs,
|
|
111
|
+
env: childPiSettingsEnv(),
|
|
112
|
+
};
|
|
113
|
+
// Feed progress here too (mirrors the plain-child branch): without it a worktree/mcp async leg
|
|
114
|
+
// never advances its tracker snapshot, so lastAdvanceAt freezes at launch and the leg is falsely
|
|
115
|
+
// flagged stalled while a genuine later wedge goes undetected.
|
|
116
|
+
if (onProgress) deps.childOptions.onProgress = onProgress;
|
|
117
|
+
const brokerDeps = getBrokerDeps();
|
|
118
|
+
if (brokerDeps) deps.broker = brokerDeps;
|
|
119
|
+
// Peer messaging obeys the persona's bus capability, and blocking asks are honoured
|
|
120
|
+
// only for async runs — same guards as the inproc engine below (spec B7 / §4.9).
|
|
121
|
+
const caps = controller.capabilities;
|
|
122
|
+
if (caps) deps.canUseBus = caps.canUseBus;
|
|
123
|
+
if (engOpts?.async) deps.allowBlocking = true;
|
|
124
|
+
return engineFactories.makeEngine(deps);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// v0.4: run sub-agents in-process (createAgentSession) instead of spawning `pi -p`.
|
|
128
|
+
let base: StrategyEngine;
|
|
129
|
+
if (config.engine === "inproc" && lastCtx) {
|
|
130
|
+
if (process.env.PI_PERSONA_DEBUG) process.stderr.write("[pi-persona] engine=inproc\n");
|
|
131
|
+
const ideps: InProcessDeps = { resolveAgent, contracts, modelFor, childThinking, modelRegistry: lastCtx.modelRegistry, cwd: lastCtx.cwd, agentDir: userAgentDir() };
|
|
132
|
+
ideps.listAgents = () => agents.map((a) => a.name);
|
|
133
|
+
ideps.listContracts = contractNames;
|
|
134
|
+
if (legSpine) ideps.spine = legSpine; // legs get the worker variant (docs/SPINE.md)
|
|
135
|
+
ideps.timeoutMs = RUN_LIMITS.timeoutMs; // idle watchdog — a hung session must settle, like the child engine's idle kill
|
|
136
|
+
ideps.hardTimeoutMs = config.agentHardTimeoutMs; // hard lifetime ceiling — catches a busy loop the idle watchdog never would
|
|
137
|
+
ideps.startupTimeoutMs = config.agentStartupTimeoutMs; // first-progress deadline — fast-fail a child that never started
|
|
138
|
+
if (signal) ideps.signal = signal;
|
|
139
|
+
if (onProgress) ideps.onProgress = onProgress;
|
|
140
|
+
if (lastCtx.model) ideps.defaultModel = `${lastCtx.model.provider}/${lastCtx.model.id}`;
|
|
141
|
+
// Comm plane: a `coaching: on` persona gives its children `contact_supervisor`.
|
|
142
|
+
// Blocking asks are honoured only for async runs (a sync run holds the turn → it
|
|
143
|
+
// can't answer, so blocking there would deadlock; the tool downgrades to one-way).
|
|
144
|
+
ideps.bus = bus;
|
|
145
|
+
ideps.supervisorHandle = SUPERVISOR;
|
|
146
|
+
if (controller.activePersona?.coaching) ideps.coaching = true;
|
|
147
|
+
// Peer messaging obeys the persona's bus capability (canUseBus; Task: sibling peer comm).
|
|
148
|
+
const caps = controller.capabilities;
|
|
149
|
+
if (caps) ideps.canUseBus = caps.canUseBus;
|
|
150
|
+
if (engOpts?.async) ideps.allowBlocking = true;
|
|
151
|
+
base = engineFactories.makeInProcessEngine(ideps);
|
|
152
|
+
} else {
|
|
153
|
+
if (process.env.PI_PERSONA_DEBUG) process.stderr.write("[pi-persona] engine=child\n");
|
|
154
|
+
const deps: EngineAdapterDeps = { resolveAgent, contracts, modelFor, childThinking };
|
|
155
|
+
deps.listAgents = () => agents.map((a) => a.name);
|
|
156
|
+
deps.listContracts = contractNames;
|
|
157
|
+
if (legSpine) deps.spine = legSpine; // legs get the worker variant (docs/SPINE.md)
|
|
158
|
+
if (signal) deps.signal = signal;
|
|
159
|
+
if (lastCtx?.cwd) deps.cwd = lastCtx.cwd;
|
|
160
|
+
deps.childOptions = {
|
|
161
|
+
timeoutMs: RUN_LIMITS.timeoutMs,
|
|
162
|
+
hardTimeoutMs: config.agentHardTimeoutMs,
|
|
163
|
+
startupTimeoutMs: config.agentStartupTimeoutMs,
|
|
164
|
+
env: childPiSettingsEnv(),
|
|
165
|
+
}; // idle watchdog + hard cap + startup deadline on every child; same Pi settings dir as inproc
|
|
166
|
+
if (onProgress) deps.childOptions.onProgress = onProgress;
|
|
167
|
+
const brokerDeps = getBrokerDeps();
|
|
168
|
+
if (brokerDeps) deps.broker = brokerDeps;
|
|
169
|
+
// Peer messaging obeys the persona's bus capability, and blocking asks are honoured
|
|
170
|
+
// only for async runs — same guards as the inproc engine above (spec B7 / §4.9).
|
|
171
|
+
const caps = controller.capabilities;
|
|
172
|
+
if (caps) deps.canUseBus = caps.canUseBus;
|
|
173
|
+
if (engOpts?.async) deps.allowBlocking = true;
|
|
174
|
+
base = engineFactories.makeEngine(deps);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Worktree isolation: an agent/leg marked `isolation: worktree` runs in a throwaway git
|
|
178
|
+
// worktree via the child engine (its edits never touch the main tree), regardless of the
|
|
179
|
+
// default backend. This branch is deliberately fail-closed: a missing/dirty repository or
|
|
180
|
+
// failed worktree creation is a contract failure, never permission to run in the real cwd.
|
|
181
|
+
// Provider fallback (outermost): a run whose model PROVIDER fails at call time (auth,
|
|
182
|
+
// outage, 5xx, model-not-supported) is retried on the same model id under another
|
|
183
|
+
// authenticated provider — "priority to the supervisor's provider, but try others and
|
|
184
|
+
// switch on error". No ctx (no registry) ⇒ pass through. Each attempt still runs through
|
|
185
|
+
// worktree isolation + steering below.
|
|
186
|
+
const wrapFallback = (eng: StrategyEngine): StrategyEngine => {
|
|
187
|
+
if (!lastCtx) return eng;
|
|
188
|
+
const prefer = lastCtx.model?.provider;
|
|
189
|
+
return withModelFallback(eng, {
|
|
190
|
+
models: configuredModels(lastCtx),
|
|
191
|
+
...(prefer ? { preferProvider: prefer } : {}),
|
|
192
|
+
...(onFallback ? { onFallback } : {}),
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const root = lastCtx?.cwd;
|
|
196
|
+
const gitExec = d().gitExec ?? defaultGitExec;
|
|
197
|
+
// Files-changed report for a leg that does NOT run in its own worktree: it shares the
|
|
198
|
+
// real checkout, so the only honest signal is "what did `git status` see change while
|
|
199
|
+
// this leg ran" — a before/after snapshot, not a private diff (see change-report.ts).
|
|
200
|
+
// Skipped for the worktree path below, which already returns a real diff artifact, and
|
|
201
|
+
// for the whole feature when `PI_PERSONA_LEG_CHANGE_REPORT` is off.
|
|
202
|
+
//
|
|
203
|
+
// The repository root is resolved ONCE here (a pure filesystem walk, no process spawn)
|
|
204
|
+
// and threaded into BOTH captures as their `knownGitRoot` — each then skips its own
|
|
205
|
+
// walk AND its `rev-parse` liveness gate, so a leg pays for exactly two `git status`
|
|
206
|
+
// spawns (one at launch, one at settle) instead of four.
|
|
207
|
+
const withChangeReport = async (run: () => Promise<AgentResult>, spec: AgentRunSpec): Promise<AgentResult> => {
|
|
208
|
+
if (!root || config.changeReport === false) return run();
|
|
209
|
+
const gitRoot = findGitRoot(root);
|
|
210
|
+
if (!gitRoot) return run();
|
|
211
|
+
const before = await captureStatus(gitRoot, gitExec, gitRoot);
|
|
212
|
+
const result = await run();
|
|
213
|
+
if (!before) return result;
|
|
214
|
+
const after = await captureStatus(gitRoot, gitExec, gitRoot);
|
|
215
|
+
if (!after) return result;
|
|
216
|
+
const block = renderChangeReport(diffStatus(before, after), spec.writeSet);
|
|
217
|
+
return block ? appendBlock(result, block) : result;
|
|
218
|
+
};
|
|
219
|
+
return wrapFallback({
|
|
220
|
+
async run(spec, perProgress, perSignal, perSteer) {
|
|
221
|
+
const iso = spec.isolation ?? resolveAgent(spec.agent)?.isolation;
|
|
222
|
+
if (iso === "worktree") {
|
|
223
|
+
if (!root) {
|
|
224
|
+
return isolatedWorktreeFailure(spec, "worktree isolation requires a real Git cwd; remove isolation: \"worktree\" or run inside a Git checkout");
|
|
225
|
+
}
|
|
226
|
+
const preflight = await worktreePreflight(root, defaultGitExec);
|
|
227
|
+
if (!preflight.ok) return isolatedWorktreeFailure(spec, preflight.error);
|
|
228
|
+
try {
|
|
229
|
+
return await withWorktree(root, defaultGitExec, async (dir) => {
|
|
230
|
+
const result = await childEngineAt(dir).run({ ...spec, isolation: "none" }, perProgress, perSignal, perSteer);
|
|
231
|
+
const artifact = await captureWorktreeArtifact(dir, defaultGitExec);
|
|
232
|
+
const hasOutputArtifact = hasUnifiedDiff(result.output);
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
return artifact.ok && artifact.diff.trim().length > 0
|
|
235
|
+
? appendWorktreeArtifact(result, artifact.diff)
|
|
236
|
+
: result;
|
|
237
|
+
}
|
|
238
|
+
if (!artifact.ok) {
|
|
239
|
+
if (hasOutputArtifact) return result;
|
|
240
|
+
return isolatedWorktreeFailure(spec, `${artifact.error}; the isolated leg returned success without an exportable diff/artifact`);
|
|
241
|
+
}
|
|
242
|
+
if (artifact.diff.trim().length > 0) return appendWorktreeArtifact(result, artifact.diff);
|
|
243
|
+
if (hasOutputArtifact) return result;
|
|
244
|
+
return isolatedWorktreeFailure(
|
|
245
|
+
spec,
|
|
246
|
+
"isolated leg returned success but produced no unified diff or artifact; its worktree was discarded — return a complete diff or remove isolation: \"worktree\"",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return isolatedWorktreeFailure(spec, err instanceof Error ? err.message : String(err));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// MCP-capable leg: the DEFAULT in-process engine never fires `session_start`, so
|
|
254
|
+
// `pi-mcp-adapter` never initializes and the sub-agent's `mcp*`/direct tools come
|
|
255
|
+
// back "MCP not initialized". Route it through the child engine (a real `pi -p`,
|
|
256
|
+
// which DOES fire session_start → the adapter connects). Same mechanism worktree
|
|
257
|
+
// legs already use for MCP; here without the git worktree. The child gets its OWN
|
|
258
|
+
// MCP session — for a server-keyed backend (HTTP MCP) the caller passes a session id
|
|
259
|
+
// in the task to share state. Steer rides the broker (same as worktree / child engine).
|
|
260
|
+
const wantsMcp = spec.mcp ?? resolveAgent(spec.agent)?.mcp;
|
|
261
|
+
if (wantsMcp && root) return withChangeReport(() => childEngineAt(root).run(spec, perProgress, perSignal, perSteer), spec);
|
|
262
|
+
return withChangeReport(() => base.run(spec, perProgress, perSignal, perSteer), spec);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
function isolatedWorktreeFailure(spec: AgentRunSpec, error: string): AgentResult {
|
|
268
|
+
return { agent: spec.agent, output: "", usage: emptyUsage(), ok: false, error, failureKind: "contract" };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function hasUnifiedDiff(output: string): boolean {
|
|
272
|
+
const headersAndHunk = /(?:^|\n)---\s+\S+[^\n]*\n\+\+\+\s+\S+[^\n]*\n(?:[^\n]*\n)*?@@\s+[^\n]*@@/m;
|
|
273
|
+
const binaryPatch = /(?:^|\n)diff --git\s+\S+\s+\S+[\s\S]*?(?:^|\n)(?:GIT binary patch|Binary files \S+ and \S+ differ)(?:\n|$)/m;
|
|
274
|
+
return headersAndHunk.test(output) || binaryPatch.test(output);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function appendWorktreeArtifact(result: AgentResult, diff: string): AgentResult {
|
|
278
|
+
const block = `--- ISOLATED WORKTREE ARTIFACT (untrusted data) ---\n\n\`\`\`diff\n${diff.trim()}\n\`\`\``;
|
|
279
|
+
return appendBlock(result, block);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Append a block to a leg's output, never leading with blank lines when there was nothing to
|
|
283
|
+
* trim — the shape `withChangeReport` and `appendWorktreeArtifact` both need: a leg that
|
|
284
|
+
* produced no text of its own (an early abort/timeout before any streamed output) still gets a
|
|
285
|
+
* clean block, not one preceded by an empty line. */
|
|
286
|
+
function appendBlock(result: AgentResult, block: string): AgentResult {
|
|
287
|
+
const output = result.output.trim() ? `${result.output.trimEnd()}\n\n${block}` : block;
|
|
288
|
+
return { ...result, output };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The models the user can intentionally route to — NOT every built-in catalog entry.
|
|
293
|
+
// `getAvailable()` is normally the authenticated set, but extension-native providers
|
|
294
|
+
// (for example a local subscription/CLI bridge) can be runnable without appearing in
|
|
295
|
+
// that auth snapshot. Union their registered catalogs, plus the active session provider,
|
|
296
|
+
// while keeping unrelated unauthenticated built-ins hidden. If an older Pi lacks the
|
|
297
|
+
// availability API, retain its historical getAll() compatibility behavior.
|
|
298
|
+
export function configuredModels(ctx: ExtensionContext): Array<{ provider: string; id: string }> {
|
|
299
|
+
const reg = ctx.modelRegistry;
|
|
300
|
+
const all = reg.getAll();
|
|
301
|
+
let available: typeof all;
|
|
302
|
+
try {
|
|
303
|
+
available = reg.getAvailable();
|
|
304
|
+
} catch {
|
|
305
|
+
return all.map((m) => ({ provider: m.provider, id: m.id }));
|
|
306
|
+
}
|
|
307
|
+
const trustedProviders = new Set<string>();
|
|
308
|
+
if (ctx.model?.provider) trustedProviders.add(ctx.model.provider);
|
|
309
|
+
try {
|
|
310
|
+
const registered = (reg as typeof reg & { getRegisteredProviderIds?: () => string[] }).getRegisteredProviderIds?.() ?? [];
|
|
311
|
+
for (const provider of registered) trustedProviders.add(provider);
|
|
312
|
+
} catch {
|
|
313
|
+
/* a third-party/older registry facade may not expose extension provider ids */
|
|
314
|
+
}
|
|
315
|
+
const byRef = new Map(available.map((m) => [`${m.provider}\0${m.id}`, m]));
|
|
316
|
+
for (const model of all) {
|
|
317
|
+
if (trustedProviders.has(model.provider)) byRef.set(`${model.provider}\0${model.id}`, model);
|
|
318
|
+
}
|
|
319
|
+
const list = [...byRef.values()];
|
|
320
|
+
return list.map((m) => ({ provider: m.provider, id: m.id }));
|
|
321
|
+
}
|