@aeondave/pi-persona 1.17.0 → 1.17.3
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 +79 -29
- package/CHANGELOG.md +264 -0
- package/README.md +27 -12
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +223 -57
- package/docs/EXPERIENCE.md +19 -9
- package/docs/MONITORS.md +62 -3
- package/docs/REFERENCE.md +189 -35
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +9 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +20 -0
- package/src/bridge.ts +3 -2
- package/src/core/brief.ts +429 -356
- package/src/core/config.ts +16 -11
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +251 -0
- package/src/core/timer.ts +34 -9
- package/src/engine/adapter.ts +12 -8
- package/src/engine/async.ts +195 -136
- package/src/engine/child.ts +1 -1
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +11 -6
- package/src/engine/stream.ts +3 -0
- package/src/exocom/install.ts +2 -1
- package/src/extension/companion-settings.ts +29 -0
- package/src/extension/engine.ts +43 -16
- package/src/extension/hooks.ts +106 -36
- package/src/extension/persona-model-settings.ts +336 -0
- package/src/extension/persona-settings.ts +459 -0
- package/src/extension/shared.ts +15 -7
- package/src/extension.ts +642 -267
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +26 -5
- package/src/orchestration/flow-run.ts +38 -13
- package/src/orchestration/flow.ts +9 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/sdk.ts +17 -3
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/persona/config-store.ts +6 -195
- package/src/persona/model-participants.ts +1 -1
- package/src/persona/persona.ts +7 -3
- package/src/persona/settings-store.ts +636 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +77 -52
- package/src/tools/delegate-tool.ts +315 -285
- package/src/tools/delegate.ts +22 -210
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +15 -19
- package/src/tools/flow.ts +85 -28
- package/src/tools/intercom-tool.ts +108 -118
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +19 -9
- package/src/tools/monitor.ts +39 -6
- package/src/tools/timer.ts +11 -13
- package/src/ui/agent-overlay.ts +55 -14
- package/src/ui/agent-tree.ts +84 -18
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/persona-settings.ts +328 -0
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +189 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
- package/src/persona/state.ts +0 -28
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `council` and `flow` as ONE tracked run (the same machinery `delegate` uses): a visible strategy or
|
|
3
|
+
* flow is launched into the {@link AsyncRunTracker}, rooted in the agent tree at `async:<runId>`, and
|
|
4
|
+
* reachable through the stop key `async:<runId>`: `intercom stop`, stop all, and `x` on the run's root row
|
|
5
|
+
* in the f9 overlay all end the whole run. Interactive sessions get a receipt at once and ONE completion
|
|
6
|
+
* follow-up (`renderCompletion`); headless sessions join the same run before the tool returns, so the
|
|
7
|
+
* single turn carries the result.
|
|
8
|
+
*
|
|
9
|
+
* Which semaphore gates what:
|
|
10
|
+
* - `slots` is the launch pool `delegate` already uses (`maxConcurrency`). A council or flow takes ONE
|
|
11
|
+
* slot for the whole run, exactly like one delegate leg, and shows `queued` until it has it.
|
|
12
|
+
* - The strategy SDK's own per-run semaphore (`orchestration/sdk.ts`) gates the legs INSIDE the run, so
|
|
13
|
+
* a council's members never compete with delegate legs for these slots and cannot deadlock on them.
|
|
14
|
+
* A flow parked at a human checkpoint keeps its slot for the wait: the operator owns that wait, and
|
|
15
|
+
* stopping the run releases it.
|
|
16
|
+
* - Token budgets, the child limit and the fork-bomb guard live in that SDK and the engine, untouched.
|
|
17
|
+
*/
|
|
18
|
+
import { END_TURN_NOTE } from "../core/brief.ts";
|
|
19
|
+
import type { ChildUsage } from "../engine/stream.ts";
|
|
20
|
+
import { emptyUsage } from "../engine/stream.ts";
|
|
21
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
22
|
+
import type { Semaphore } from "../orchestration/parallel.ts";
|
|
23
|
+
import { madeHeadway, type AsyncRun, type AsyncRunTracker, type IdleCoalescingNotifier, type ProgressReading } from "../engine/async.ts";
|
|
24
|
+
import type { AgentTree } from "../ui/agent-tree.ts";
|
|
25
|
+
import type { ChildUsageLedger } from "../ui/usage.ts";
|
|
26
|
+
|
|
27
|
+
/** What a visible runner is handed when it works for a tracked run (absent ⇒ it roots and stops itself). */
|
|
28
|
+
export interface TrackedRun {
|
|
29
|
+
/** The tree root of this run: `async:<runId>`. Its nodes hang under it. */
|
|
30
|
+
rootId: string;
|
|
31
|
+
/** Fires when the run is stopped — by key, by `intercom stop`, or by the headless tool call's abort. */
|
|
32
|
+
signal: AbortSignal;
|
|
33
|
+
/** Interactive: the launching turn has ended and the supervisor is idle, so a coached leg may block on an
|
|
34
|
+
* ask (the run's engines are built `async`). Headless: the call is parked in the turn and could not answer. */
|
|
35
|
+
background: boolean;
|
|
36
|
+
/** One leg progressed: feeds the run's cumulative tokens, and its stall clock when the leg made headway. */
|
|
37
|
+
onProgress(leg: string, progress: ProgressReading): void;
|
|
38
|
+
/** The run is blocked on the operator (a flow checkpoint) — or released again (`undefined`). */
|
|
39
|
+
onAwaiting(label: string | undefined): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BackgroundRunDeps {
|
|
43
|
+
tracker: AsyncRunTracker;
|
|
44
|
+
agentTree: AgentTree;
|
|
45
|
+
stopRegistry: Map<string, () => void>;
|
|
46
|
+
/** The launch pool shared with `delegate` (see the module comment). */
|
|
47
|
+
slots: Semaphore;
|
|
48
|
+
completionNotifier: Pick<IdleCoalescingNotifier<AsyncRun>, "discard">;
|
|
49
|
+
startPeek(): void;
|
|
50
|
+
childUsage: ChildUsageLedger;
|
|
51
|
+
publishPersonaCost(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `inline` (headless) skips the status monitor: the launching call joins the run and nobody reads a status card. */
|
|
55
|
+
export interface LaunchMeta { kind: "council" | "flow"; label: string; task: string; inline: boolean }
|
|
56
|
+
|
|
57
|
+
type RunWork = (run: TrackedRun) => Promise<AgentResult>;
|
|
58
|
+
|
|
59
|
+
/** The receipt an interactive `council`/`flow` launch returns. `wake` finishes "you will be woken ONCE ...". */
|
|
60
|
+
export function launchReceipt(kind: LaunchMeta["kind"], id: string, subject: string, wake: string): string {
|
|
61
|
+
return `Launched ${kind} ${id} (${subject}) in the background — you will be woken ONCE ${wake} (${END_TURN_NOTE}).`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BackgroundRuns {
|
|
65
|
+
/** Launch `work` as one tracked run and return its id. */
|
|
66
|
+
launch(meta: LaunchMeta, work: RunWork): string;
|
|
67
|
+
/** Join launched runs (headless): wait for all of them, stop them if `signal` aborts, drop the follow-ups
|
|
68
|
+
* this return replaces, and bill their usage once. `onSettled` sees each settle as (done, total). */
|
|
69
|
+
settle(ids: readonly string[], signal: AbortSignal | undefined, onSettled?: (done: number, total: number) => void): Promise<{ runs: AsyncRun[]; usage: ChildUsage }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createBackgroundRuns(d: BackgroundRunDeps): BackgroundRuns {
|
|
73
|
+
function launch(meta: LaunchMeta, work: RunWork): string {
|
|
74
|
+
// `mutates`: a strategy's legs and their tool grants are decided leg by leg inside the run, so the run
|
|
75
|
+
// is classed as a potential writer — the fail-closed answer for a persona's fresh-verification policy
|
|
76
|
+
// (whose refusal says "may mutate", which is all this classification knows).
|
|
77
|
+
const id = d.tracker.launch({ agent: meta.label, task: meta.task, label: meta.label, mutates: true, queued: true }, (onProgress, runId) => {
|
|
78
|
+
const rootId = `async:${runId}`;
|
|
79
|
+
const ac = new AbortController();
|
|
80
|
+
d.stopRegistry.set(rootId, () => ac.abort());
|
|
81
|
+
const legSeen = new Map<string, ProgressReading>();
|
|
82
|
+
const legTokens = new Map<string, number>();
|
|
83
|
+
const snapshot = (output = ""): { output: string; turns: number; tokens: number } => ({ output, turns: 0, tokens: [...legTokens.values()].reduce((a, b) => a + b, 0) });
|
|
84
|
+
const run: TrackedRun = {
|
|
85
|
+
rootId,
|
|
86
|
+
signal: ac.signal,
|
|
87
|
+
background: !meta.inline,
|
|
88
|
+
onProgress: (leg, progress) => {
|
|
89
|
+
// Headway is judged per leg - the run's own snapshot is only their token sum - by the same rule a
|
|
90
|
+
// delegate leg is held to, so one chatty tool in one leg cannot keep the whole run looking alive.
|
|
91
|
+
if (madeHeadway(legSeen.get(leg), progress)) d.tracker.advance(runId);
|
|
92
|
+
legSeen.set(leg, progress);
|
|
93
|
+
legTokens.set(leg, progress.tokens ?? legTokens.get(leg) ?? 0);
|
|
94
|
+
onProgress(snapshot());
|
|
95
|
+
},
|
|
96
|
+
onAwaiting: (label) => {
|
|
97
|
+
d.tracker.setAwaiting(runId, label);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return d.slots
|
|
101
|
+
.with(async () => {
|
|
102
|
+
d.tracker.markAdmitted(runId);
|
|
103
|
+
const result = await work(run);
|
|
104
|
+
// A ruling or plan that did not complete lives in `result.output`, but a failed or stopped run's
|
|
105
|
+
// report salvages the tracker's `progress.output` — hand it over so that work is not lost.
|
|
106
|
+
if (!result.ok && result.output) onProgress(snapshot(result.output));
|
|
107
|
+
return result;
|
|
108
|
+
}, ac.signal)
|
|
109
|
+
.catch((error: unknown): AgentResult => {
|
|
110
|
+
if (!ac.signal.aborted) throw error;
|
|
111
|
+
return { agent: meta.label, output: "", usage: emptyUsage(), ok: false, error: "run aborted", failureKind: "abort" };
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// Visible while queued, so `x` and the tree's stop work before the pool grants a slot; the visible runner
|
|
115
|
+
// upserts this same row when it starts.
|
|
116
|
+
d.agentTree.add({ id: `async:${id}`, label: meta.label, kind: meta.kind, status: "running", detail: "queued" });
|
|
117
|
+
if (!meta.inline) d.startPeek();
|
|
118
|
+
return id;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function settle(ids: readonly string[], signal: AbortSignal | undefined, onSettled?: (done: number, total: number) => void): Promise<{ runs: AsyncRun[]; usage: ChildUsage }> {
|
|
122
|
+
const runs = ids.flatMap((id) => d.tracker.peek(id) ?? []); // launch() registers each, and a running entry is never evicted
|
|
123
|
+
const stopAll = (): void => {
|
|
124
|
+
for (const id of ids) d.stopRegistry.get(`async:${id}`)?.();
|
|
125
|
+
};
|
|
126
|
+
if (signal?.aborted) stopAll();
|
|
127
|
+
else signal?.addEventListener("abort", stopAll, { once: true });
|
|
128
|
+
try {
|
|
129
|
+
if (runs.some((run) => !run.settled)) {
|
|
130
|
+
await new Promise<void>((resolve) => {
|
|
131
|
+
const off = d.tracker.onComplete(() => {
|
|
132
|
+
const done = runs.filter((run) => run.settled).length;
|
|
133
|
+
onSettled?.(done, runs.length);
|
|
134
|
+
if (done === runs.length) {
|
|
135
|
+
off();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
signal?.removeEventListener("abort", stopAll);
|
|
143
|
+
}
|
|
144
|
+
d.completionNotifier.discard((pending) => ids.includes(pending.id)); // this return IS the delivery
|
|
145
|
+
const usage = d.childUsage.accountMany(runs.map((run) => ({ key: run.id, usage: run.result?.usage ?? emptyUsage() })));
|
|
146
|
+
d.publishPersonaCost();
|
|
147
|
+
return { runs, usage };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { launch, settle };
|
|
151
|
+
}
|
package/src/tools/council.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
5
|
-
import { expandDetailHint, failureDetails, formatCouncilCallLabel } from "../extension/shared.ts";
|
|
6
|
-
import { knownParams,
|
|
5
|
+
import { expandDetailHint, failureDetails, formatCouncilCallLabel, isInline } from "../extension/shared.ts";
|
|
6
|
+
import { knownParams, missingRequiredParams, strategyCatalog } from "../orchestration/strategy.ts";
|
|
7
7
|
import { memberBaseLabel, type RosterMember } from "../orchestration/roster.ts";
|
|
8
8
|
import { formatCouncilResult, humanizeAggregateResult } from "../orchestration/render.ts";
|
|
9
9
|
import type { FailureKind } from "../orchestration/types.ts";
|
|
@@ -12,10 +12,11 @@ import { resolveCouncilInvocation } from "../persona/persona.ts";
|
|
|
12
12
|
import type { OrchestrationGrammar, Persona } from "../persona/persona.ts";
|
|
13
13
|
import type { PersonaController } from "../persona/controller.ts";
|
|
14
14
|
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
-
import {
|
|
16
|
-
import { compactVisibleText } from "../ui/presentation.ts";
|
|
17
|
-
import { toolUsageField
|
|
15
|
+
import { createReportBody } from "../ui/report-body.ts";
|
|
16
|
+
import { compactInlineText, compactVisibleText, sanitizeTerminalText } from "../ui/presentation.ts";
|
|
17
|
+
import { toolUsageField } from "../ui/usage.ts";
|
|
18
18
|
import { emptyUsage } from "../engine/stream.ts";
|
|
19
|
+
import { launchReceipt, type BackgroundRuns, type TrackedRun } from "./background-run.ts";
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
export interface CouncilToolDeps {
|
|
@@ -28,11 +29,11 @@ export interface CouncilToolDeps {
|
|
|
28
29
|
orch: OrchestrationGrammar,
|
|
29
30
|
task: string,
|
|
30
31
|
label: string,
|
|
31
|
-
|
|
32
|
+
tracked?: TrackedRun,
|
|
32
33
|
): Promise<AgentResult | undefined>;
|
|
34
|
+
/** Launch/join: a council is one tracked run, like a delegate leg (see background-run.ts). */
|
|
35
|
+
runs: BackgroundRuns;
|
|
33
36
|
drainBusBlock(): string;
|
|
34
|
-
childUsage: ChildUsageLedger;
|
|
35
|
-
publishPersonaCost(): void;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** What the card names as the panel: the team, or the ad-hoc members themselves — an
|
|
@@ -55,7 +56,7 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
55
56
|
strategy: Type.Optional(
|
|
56
57
|
Type.String({ description: 'Per-call strategy override (default: the selected or active council profile\'s strategy, or built-in "magi")' }),
|
|
57
58
|
),
|
|
58
|
-
roster: Type.Optional(Type.String({ description: 'Per-call roster override (default: the selected or active profile\'s roster, then its orchestration roster, or built-in "magi")' })),
|
|
59
|
+
roster: Type.Optional(Type.String({ description: 'Per-call roster override (default: the method\'s own team when you switch method; else the selected or active profile\'s roster, then its orchestration roster, or built-in "magi")' })),
|
|
59
60
|
members: Type.Optional(
|
|
60
61
|
Type.Array(Type.Unknown(), {
|
|
61
62
|
description:
|
|
@@ -72,30 +73,24 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
72
73
|
pi.registerTool({
|
|
73
74
|
name: "council",
|
|
74
75
|
label: "Council",
|
|
75
|
-
|
|
76
|
+
// The method catalog is generated from the strategy registry (`when`/`roster`/`required`), never hand-written.
|
|
77
|
+
description: `${[
|
|
76
78
|
"Convene a council of specialists with controlled, complementary biases to deliberate a",
|
|
77
|
-
"decision — returns the selected strategy's result,
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"deserve a whole panel. Then
|
|
81
|
-
"surfaces a NEW decision. Do NOT use it
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"(critic-loop); head-to-head argument (debate); many findings merged into ONE deliverable",
|
|
86
|
-
"(synthesize, map); rival implementations judged blind (compete); a single pair (pair).",
|
|
79
|
+
"decision or run a coordinated method over a task — returns the selected strategy's result,",
|
|
80
|
+
"preserving vote tally and dissent when produced.",
|
|
81
|
+
"Scale it to the task: convene when the method adds value — NOT before every decision, and a",
|
|
82
|
+
"routine task does not deserve a whole panel. Then act on the result yourself and re-convene only",
|
|
83
|
+
"when acting surfaces a NEW decision. Do NOT use it for plain independent parallel work: that is",
|
|
84
|
+
"`delegate`. Interactive sessions run a council in the background: you get a run id at once and ONE",
|
|
85
|
+
"follow-up carries the ruling, so end your turn and never poll it; headless sessions return the",
|
|
86
|
+
"ruling inline.",
|
|
87
87
|
'Use `persona: "magi"` to invoke an installed persona\'s declared council without switching away',
|
|
88
88
|
"from the active caller; its prompt, model, tools, and permissions are never inherited.",
|
|
89
|
-
`Strategies: ${strategyNames()
|
|
90
|
-
.map((n) => {
|
|
91
|
-
const p = knownParams(n);
|
|
92
|
-
const keys = p ? Object.keys(p) : [];
|
|
93
|
-
return keys.length > 0 ? `${n}(${keys.join(", ")})` : n;
|
|
94
|
-
})
|
|
95
|
-
.join(" · ")}.`,
|
|
96
89
|
"Pass `members` to seat an ad-hoc panel for one call, or `params` to vary the profile's default",
|
|
97
90
|
'council — e.g. { "reflect": false }.',
|
|
98
|
-
|
|
91
|
+
"Pick the strategy by problem class (`*` = required param):",
|
|
92
|
+
].join(" ")}
|
|
93
|
+
${strategyCatalog()}`,
|
|
99
94
|
parameters: CouncilParams,
|
|
100
95
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
101
96
|
d.lastCtx = ctx;
|
|
@@ -106,10 +101,13 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
106
101
|
members: params.members,
|
|
107
102
|
params: params.params as Record<string, unknown> | undefined,
|
|
108
103
|
});
|
|
109
|
-
|
|
104
|
+
// A call that cannot run (no such persona, a missing `required` param) is refused HERE, as an
|
|
105
|
+
// immediate tool error — before a run, a tree row or the model picker exists.
|
|
106
|
+
const error = resolved.ok ? missingRequiredParams(resolved.value.strategy, resolved.value.params) : resolved.error;
|
|
107
|
+
if (!resolved.ok || error) {
|
|
110
108
|
return {
|
|
111
|
-
content: [{ type: "text", text: `council failed: ${
|
|
112
|
-
details: failureDetails({ error:
|
|
109
|
+
content: [{ type: "text", text: `council failed: ${error}` }],
|
|
110
|
+
details: failureDetails({ error: error ?? "", persona: params.persona }),
|
|
113
111
|
isError: true,
|
|
114
112
|
};
|
|
115
113
|
}
|
|
@@ -137,8 +135,29 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
137
135
|
// Ephemeral members ride the SAME grammar path as a named team (see
|
|
138
136
|
// `resolveOrchestrationMembers`) — nothing about the profile is inherited.
|
|
139
137
|
if (members) orch.members = members;
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
// The same launch `delegate` uses: ONE tracked run. Interactive sessions get a receipt now and
|
|
139
|
+
// the ruling as ONE completion follow-up (the idle-gated wake, fenced and bounded); headless
|
|
140
|
+
// sessions join the run before returning, since nothing would deliver a follow-up there.
|
|
141
|
+
const inline = isInline(ctx);
|
|
142
|
+
const id = d.runs.launch({ kind: "council", label: `council ${strategy}`, task: params.question, inline }, async (run) =>
|
|
143
|
+
(await d.runStrategyVisible(ctx, orch, params.question, "council", run)) ??
|
|
144
|
+
{ agent: strategy, output: "(the council returned no ruling)", usage: emptyUsage(), ok: false, error: "no ruling" });
|
|
145
|
+
if (!inline) {
|
|
146
|
+
const names = compactInlineText(`${strategy} · ${panel}`, { maxChars: 120 });
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: `${launchReceipt("council", id, names, "with the ruling")}${paramNote}` }],
|
|
149
|
+
details: { runId: id, strategy, roster: panel, persona },
|
|
150
|
+
isError: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const { runs, usage } = await d.runs.settle([id], signal);
|
|
154
|
+
const run = runs[0];
|
|
155
|
+
const result = run?.result;
|
|
156
|
+
if (!result) {
|
|
157
|
+
const message = run?.error ?? run?.status ?? "the run record is gone";
|
|
158
|
+
return { content: [{ type: "text", text: `council failed: ${message}` }], details: failureDetails({ error: message, strategy, roster: panel }), isError: true };
|
|
159
|
+
}
|
|
160
|
+
const s = (result.structured ?? {}) as {
|
|
142
161
|
headline?: string;
|
|
143
162
|
status?: string;
|
|
144
163
|
tally?: Record<string, number>;
|
|
@@ -146,33 +165,28 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
146
165
|
count?: number;
|
|
147
166
|
items?: unknown;
|
|
148
167
|
};
|
|
149
|
-
const ruling = result?.output ?? "(the council returned no ruling)";
|
|
150
|
-
const uiBody = result ? (humanizeAggregateResult(result) ?? result.output) : "";
|
|
151
168
|
const headline = s.headline ?? (typeof s.count === "number" ? `${s.count} member results` : s.status ?? "");
|
|
152
|
-
const ok = result?.ok ?? false;
|
|
153
169
|
const details = {
|
|
154
|
-
ok,
|
|
170
|
+
ok: result.ok,
|
|
155
171
|
headline,
|
|
156
172
|
status: s.status,
|
|
157
173
|
tally: s.tally,
|
|
158
174
|
usedFallback: s.usedFallback,
|
|
159
175
|
items: s.items,
|
|
160
|
-
body:
|
|
176
|
+
body: humanizeAggregateResult(result) ?? result.output,
|
|
161
177
|
strategy,
|
|
162
178
|
roster: panel,
|
|
163
179
|
persona,
|
|
164
|
-
...(result
|
|
165
|
-
...(result
|
|
180
|
+
...(result.error ? { error: result.error } : {}),
|
|
181
|
+
...(result.failureKind ? { failureKind: result.failureKind } : {}),
|
|
166
182
|
};
|
|
167
|
-
const usage = result?.usage ?? emptyUsage();
|
|
168
|
-
d.childUsage.account(usage);
|
|
169
|
-
d.publishPersonaCost();
|
|
170
183
|
return {
|
|
171
184
|
// The ruling is sub-agent (council member) text — fence it like every other
|
|
172
185
|
// path that hands sub-agent output to the supervisor.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
// A run stopped before it convened has no ruling, only its cause: show that, not an empty fence.
|
|
187
|
+
content: [{ type: "text", text: `${fenceUntrusted(result.output || result.error || "")}${paramNote}${d.drainBusBlock()}` }],
|
|
188
|
+
details: result.ok ? details : failureDetails(details),
|
|
189
|
+
isError: !result.ok,
|
|
176
190
|
...toolUsageField(usage),
|
|
177
191
|
};
|
|
178
192
|
} catch (err) {
|
|
@@ -193,7 +207,7 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
193
207
|
const roster = resolved.ok ? panelLabel(resolved.value) : (args.roster ?? args.persona ?? "?");
|
|
194
208
|
return new Text(theme.fg("toolTitle", theme.bold(formatCouncilCallLabel(strategy, roster))), 0, 0);
|
|
195
209
|
},
|
|
196
|
-
renderResult(result, { expanded }, theme) {
|
|
210
|
+
renderResult(result, { expanded }, theme, context) {
|
|
197
211
|
const d = (result.details ?? {}) as {
|
|
198
212
|
ok?: boolean;
|
|
199
213
|
headline?: string;
|
|
@@ -205,24 +219,35 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
|
|
|
205
219
|
roster?: string;
|
|
206
220
|
error?: string;
|
|
207
221
|
failureKind?: FailureKind;
|
|
222
|
+
runId?: string;
|
|
208
223
|
};
|
|
209
224
|
const first = result.content[0];
|
|
225
|
+
const failed = (context?.isError ?? result.isError) || d.ok === false || !!d.error;
|
|
226
|
+
if (d.runId) {
|
|
227
|
+
// Interactive launch receipt: the ruling arrives later as the completion follow-up.
|
|
228
|
+
const launched = failed ? theme.fg("error", theme.bold("council failed"))
|
|
229
|
+
: `${theme.fg("toolTitle", theme.bold("council "))}${theme.fg("accent", compactInlineText(`launched ${d.runId} · ${d.strategy ?? "?"} · ${d.roster ?? "?"}`, { maxChars: 120 }))}`;
|
|
230
|
+
return new Text(expanded && first?.type === "text" ? `${launched}\n${theme.fg("toolOutput", sanitizeTerminalText(first.text))}` : launched, 0, 0);
|
|
231
|
+
}
|
|
210
232
|
const body = sanitizeTerminalText(d.body || (first && first.type === "text" ? first.text : ""));
|
|
211
|
-
const failed = d.ok === false || !!d.error;
|
|
212
233
|
if (failed) {
|
|
213
234
|
const cause = compactInlineText([d.failureKind, d.error].filter((part): part is string => !!part).join(" · "), { maxChars: 160 });
|
|
214
|
-
const title = theme.fg("error", theme.bold(`council failed${cause ? ` · ${cause}` : ""}`));
|
|
215
|
-
if (expanded)
|
|
235
|
+
const title = () => theme.fg("error", theme.bold(`council failed${cause ? ` · ${cause}` : ""}`));
|
|
236
|
+
if (expanded) {
|
|
237
|
+
// Only a completed council retains a ruling body; preflight/host errors are literal.
|
|
238
|
+
if (typeof d.body === "string") return createReportBody(body || "(no ruling)", theme, { header: title }, context?.lastComponent);
|
|
239
|
+
return new Text(`${title()}\n${theme.fg("toolOutput", body || "(no ruling)")}`, 0, 0);
|
|
240
|
+
}
|
|
216
241
|
const preview = compactVisibleText(body || "(no ruling)", { maxLines: 3, maxLineChars: 100 });
|
|
217
242
|
const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
|
|
218
|
-
return new Text(`${title}\n${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
|
|
243
|
+
return new Text(`${title()}\n${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
|
|
219
244
|
}
|
|
220
245
|
const text = formatCouncilResult(
|
|
221
246
|
{ headline: d.headline, status: d.status, tally: d.tally, usedFallback: d.usedFallback, body },
|
|
222
247
|
expanded,
|
|
223
248
|
expandDetailHint(),
|
|
224
249
|
);
|
|
225
|
-
return new Text(theme.fg(
|
|
250
|
+
return expanded ? createReportBody(text, theme, {}, context?.lastComponent) : new Text(theme.fg("accent", text), 0, 0);
|
|
226
251
|
},
|
|
227
252
|
});
|
|
228
253
|
}
|