@g3un/pi-orchestra 0.1.0 → 0.2.0
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/README.md +8 -3
- package/docs/orchestration-model.md +8 -4
- package/package.json +12 -1
- package/src/adapters/in-memory-store.ts +25 -21
- package/src/core/orchestra.ts +1 -189
- package/src/core/store.ts +5 -2
- package/src/core/subagent.ts +8 -0
- package/src/extension/index.ts +55 -4
- package/src/extension/orchestra-events.ts +231 -0
- package/src/extension/workflow-monitor.ts +143 -0
- package/src/tools/bus.ts +9 -133
- package/src/tools/workflow.ts +53 -109
- package/src/tools/workgroup.ts +186 -71
- package/src/utils.ts +4 -14
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { AgentRun, AgentRunResult, AgentState } from "../core/subagent.ts";
|
|
2
|
+
import type { AgentStore } from "../core/store.ts";
|
|
3
|
+
import type { WorkflowRun } from "../core/workflow.ts";
|
|
4
|
+
import type { WorkgroupStrategy } from "../core/workgroup.ts";
|
|
5
|
+
import { formatNamedEntityLabel, isTerminalAgentState, toAgentRunResult } from "../utils.ts";
|
|
6
|
+
|
|
7
|
+
export const ORCHESTRA_EVENT_CUSTOM_TYPE = "pi-orchestra.event";
|
|
8
|
+
|
|
9
|
+
export type OrchestraMainEvent =
|
|
10
|
+
| {
|
|
11
|
+
type: "subagent.finished";
|
|
12
|
+
busId: string;
|
|
13
|
+
run: AgentRunResult;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
type: "workgroup.member_finished";
|
|
17
|
+
busId: string;
|
|
18
|
+
strategy: WorkgroupStrategy;
|
|
19
|
+
run: AgentRunResult;
|
|
20
|
+
pendingRunIds: string[];
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
type: "workflow.finished";
|
|
24
|
+
workflow: WorkflowRun;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface WorkgroupRegistration {
|
|
28
|
+
busId: string;
|
|
29
|
+
strategy: WorkgroupStrategy;
|
|
30
|
+
runIds: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OrchestraEventControllerOptions {
|
|
34
|
+
store: AgentStore;
|
|
35
|
+
sendEvents: (events: OrchestraMainEvent[], content: string) => void;
|
|
36
|
+
/** Defaults to 50 ms. Use 0 in tests for immediate delivery. */
|
|
37
|
+
flushDelayMs?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface RegisteredWorkgroup {
|
|
41
|
+
strategy: WorkgroupStrategy;
|
|
42
|
+
runIds: Set<string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface LaunchingWorkgroup {
|
|
46
|
+
strategy: WorkgroupStrategy;
|
|
47
|
+
finishedRunIds: Set<string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class OrchestraEventController {
|
|
51
|
+
private readonly store: AgentStore;
|
|
52
|
+
private readonly sendEvents: OrchestraEventControllerOptions["sendEvents"];
|
|
53
|
+
private readonly flushDelayMs: number;
|
|
54
|
+
private readonly runStates = new Map<string, AgentState>();
|
|
55
|
+
private readonly workflowStates = new Map<string, AgentState>();
|
|
56
|
+
private readonly mainWorkgroupsByBusId = new Map<string, RegisteredWorkgroup>();
|
|
57
|
+
private readonly launchingWorkgroupsByBusId = new Map<string, LaunchingWorkgroup>();
|
|
58
|
+
private readonly queuedEvents: OrchestraMainEvent[] = [];
|
|
59
|
+
private readonly unsubscribeRuns: () => void;
|
|
60
|
+
private readonly unsubscribeWorkflows: () => void;
|
|
61
|
+
private flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
|
|
63
|
+
constructor(options: OrchestraEventControllerOptions) {
|
|
64
|
+
this.store = options.store;
|
|
65
|
+
this.sendEvents = options.sendEvents;
|
|
66
|
+
this.flushDelayMs = options.flushDelayMs ?? 50;
|
|
67
|
+
|
|
68
|
+
for (const run of this.store.listRuns()) this.runStates.set(run.id, run.state);
|
|
69
|
+
for (const workflow of this.store.listWorkflows()) this.workflowStates.set(workflow.id, workflow.state);
|
|
70
|
+
|
|
71
|
+
this.unsubscribeRuns = this.store.subscribeRuns((run) => this.handleRunSaved(run));
|
|
72
|
+
this.unsubscribeWorkflows = this.store.subscribeWorkflows((workflow) => this.handleWorkflowSaved(workflow));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
beginWorkgroup(busId: string, strategy: WorkgroupStrategy): void {
|
|
76
|
+
this.launchingWorkgroupsByBusId.set(busId, { strategy, finishedRunIds: new Set() });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
registerWorkgroup(registration: WorkgroupRegistration): void {
|
|
80
|
+
const launching = this.launchingWorkgroupsByBusId.get(registration.busId);
|
|
81
|
+
const existing = this.mainWorkgroupsByBusId.get(registration.busId);
|
|
82
|
+
const runIds = new Set<string>(existing?.runIds);
|
|
83
|
+
for (const runId of launching?.finishedRunIds ?? []) runIds.add(runId);
|
|
84
|
+
for (const runId of registration.runIds) runIds.add(runId);
|
|
85
|
+
|
|
86
|
+
this.mainWorkgroupsByBusId.set(registration.busId, {
|
|
87
|
+
strategy: registration.strategy,
|
|
88
|
+
runIds,
|
|
89
|
+
});
|
|
90
|
+
this.launchingWorkgroupsByBusId.delete(registration.busId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cancelWorkgroupLaunch(busId: string): void {
|
|
94
|
+
this.launchingWorkgroupsByBusId.delete(busId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
dispose(): void {
|
|
98
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
99
|
+
this.flushTimer = undefined;
|
|
100
|
+
this.queuedEvents.length = 0;
|
|
101
|
+
this.unsubscribeRuns();
|
|
102
|
+
this.unsubscribeWorkflows();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
flush(): void {
|
|
106
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
107
|
+
this.flushTimer = undefined;
|
|
108
|
+
if (this.queuedEvents.length === 0) return;
|
|
109
|
+
|
|
110
|
+
const events = this.queuedEvents.splice(0);
|
|
111
|
+
this.sendEvents(events, formatOrchestraEvents(events));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private handleRunSaved(run: AgentRun): void {
|
|
115
|
+
const previousState = this.runStates.get(run.id);
|
|
116
|
+
this.runStates.set(run.id, run.state);
|
|
117
|
+
|
|
118
|
+
if (!isAgentFinishState(run.state) || isAgentFinishState(previousState)) return;
|
|
119
|
+
if (this.isWorkflowBus(run.busId)) return;
|
|
120
|
+
|
|
121
|
+
const registeredWorkgroup = this.mainWorkgroupsByBusId.get(run.busId);
|
|
122
|
+
if (registeredWorkgroup?.runIds.has(run.id)) {
|
|
123
|
+
this.queueEvent({
|
|
124
|
+
type: "workgroup.member_finished",
|
|
125
|
+
busId: run.busId,
|
|
126
|
+
strategy: registeredWorkgroup.strategy,
|
|
127
|
+
run: toAgentRunResult(run),
|
|
128
|
+
pendingRunIds: this.getPendingWorkgroupRunIds(run.busId, registeredWorkgroup.runIds),
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const launchingWorkgroup = this.launchingWorkgroupsByBusId.get(run.busId);
|
|
134
|
+
if (launchingWorkgroup) {
|
|
135
|
+
launchingWorkgroup.finishedRunIds.add(run.id);
|
|
136
|
+
this.queueEvent({
|
|
137
|
+
type: "workgroup.member_finished",
|
|
138
|
+
busId: run.busId,
|
|
139
|
+
strategy: launchingWorkgroup.strategy,
|
|
140
|
+
run: toAgentRunResult(run),
|
|
141
|
+
pendingRunIds: this.getPendingBusRunIds(run.busId),
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.queueEvent({ type: "subagent.finished", busId: run.busId, run: toAgentRunResult(run) });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleWorkflowSaved(workflow: WorkflowRun): void {
|
|
150
|
+
const previousState = this.workflowStates.get(workflow.id);
|
|
151
|
+
this.workflowStates.set(workflow.id, workflow.state);
|
|
152
|
+
|
|
153
|
+
if (!isTerminalAgentState(workflow.state) || isTerminalWorkflowState(previousState)) return;
|
|
154
|
+
this.queueEvent({ type: "workflow.finished", workflow });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private isWorkflowBus(busId: string): boolean {
|
|
158
|
+
return this.store.listWorkflows().some((workflow) => workflow.stages.some((stage) => stage.busId === busId));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private getPendingWorkgroupRunIds(busId: string, runIds: Set<string>): string[] {
|
|
162
|
+
return this.store
|
|
163
|
+
.listRuns()
|
|
164
|
+
.filter((run) => run.busId === busId && runIds.has(run.id) && run.state === "idle")
|
|
165
|
+
.map((run) => run.id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private getPendingBusRunIds(busId: string): string[] {
|
|
169
|
+
return this.store
|
|
170
|
+
.listRuns()
|
|
171
|
+
.filter((run) => run.busId === busId && run.state === "idle")
|
|
172
|
+
.map((run) => run.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private queueEvent(event: OrchestraMainEvent): void {
|
|
176
|
+
this.queuedEvents.push(event);
|
|
177
|
+
if (this.flushDelayMs === 0) {
|
|
178
|
+
this.flush();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.flushTimer ??= setTimeout(() => this.flush(), this.flushDelayMs);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatOrchestraEvents(events: OrchestraMainEvent[]): string {
|
|
187
|
+
const headline = events.length === 1 ? "Pi-orchestra event:" : `Pi-orchestra events (${events.length}):`;
|
|
188
|
+
return [headline, ...events.flatMap((event) => ["", formatOrchestraEvent(event)])].join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatOrchestraEvent(event: OrchestraMainEvent): string {
|
|
192
|
+
if (event.type === "workflow.finished") return formatWorkflowFinishedEvent(event.workflow);
|
|
193
|
+
|
|
194
|
+
const runLabel = event.run.name === event.run.runId ? event.run.runId : `${event.run.name} (${event.run.runId})`;
|
|
195
|
+
const lines =
|
|
196
|
+
event.type === "workgroup.member_finished"
|
|
197
|
+
? [
|
|
198
|
+
`- Workgroup member finished on bus ${event.busId}: ${runLabel} is ${event.run.state}.`,
|
|
199
|
+
` Strategy: ${event.strategy}`,
|
|
200
|
+
` Pending workgroup run ids: ${event.pendingRunIds.length > 0 ? event.pendingRunIds.join(", ") : "none"}`,
|
|
201
|
+
]
|
|
202
|
+
: [`- Subagent finished on bus ${event.busId}: ${runLabel} is ${event.run.state}.`];
|
|
203
|
+
|
|
204
|
+
lines.push(...formatRunResultLines(event.run));
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function formatWorkflowFinishedEvent(workflow: WorkflowRun): string {
|
|
209
|
+
const lines = [`- Workflow finished: ${formatNamedEntityLabel(workflow)} is ${workflow.state}.`];
|
|
210
|
+
if (workflow.result) {
|
|
211
|
+
lines.push(` Result: ${workflow.result.status}`, ` Summary: ${workflow.result.summary}`);
|
|
212
|
+
}
|
|
213
|
+
if (workflow.error) lines.push(` Error: ${workflow.error}`);
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatRunResultLines(run: AgentRunResult): string[] {
|
|
218
|
+
if (!run.result) return [" No result payload recorded."];
|
|
219
|
+
|
|
220
|
+
const lines = [` Result: ${run.result.status}`, ` Summary: ${run.result.summary}`];
|
|
221
|
+
if (run.result.data !== undefined) lines.push(` Data: ${JSON.stringify(run.result.data)}`);
|
|
222
|
+
return lines;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isAgentFinishState(state: AgentState | undefined): boolean {
|
|
226
|
+
return state === "success" || state === "blocked" || state === "failed";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isTerminalWorkflowState(state: AgentState | undefined): boolean {
|
|
230
|
+
return state !== undefined && isTerminalAgentState(state);
|
|
231
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentRun } from "../core/subagent.ts";
|
|
3
|
+
import type { AgentStore } from "../core/store.ts";
|
|
4
|
+
import type { WorkflowRun, WorkflowStageRun } from "../core/workflow.ts";
|
|
5
|
+
import { formatNamedEntityLabel, isTerminalAgentState } from "../utils.ts";
|
|
6
|
+
|
|
7
|
+
const WIDGET_KEY = "pi-orchestra.workflow-monitor";
|
|
8
|
+
const MAX_MONITORED_WORKFLOWS = 2;
|
|
9
|
+
const MAX_WIDGET_LINES = 10;
|
|
10
|
+
|
|
11
|
+
export class WorkflowMonitorController {
|
|
12
|
+
private unsubscribe?: () => void;
|
|
13
|
+
private ctx?: ExtensionContext;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly store: AgentStore) {}
|
|
16
|
+
|
|
17
|
+
hasActiveWorkflows(): boolean {
|
|
18
|
+
return listActiveWorkflows(this.store).length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
show(ctx: ExtensionContext): boolean {
|
|
22
|
+
if (!ctx.hasUI) return false;
|
|
23
|
+
|
|
24
|
+
this.ctx = ctx;
|
|
25
|
+
if (!this.unsubscribe) {
|
|
26
|
+
const unsubscribeRuns = this.store.subscribeRuns(() => this.render());
|
|
27
|
+
const unsubscribeWorkflows = this.store.subscribeWorkflows(() => this.render());
|
|
28
|
+
this.unsubscribe = () => {
|
|
29
|
+
unsubscribeRuns();
|
|
30
|
+
unsubscribeWorkflows();
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.render();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dispose(): void {
|
|
38
|
+
this.unsubscribe?.();
|
|
39
|
+
this.unsubscribe = undefined;
|
|
40
|
+
|
|
41
|
+
if (this.ctx?.hasUI) {
|
|
42
|
+
this.ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
43
|
+
}
|
|
44
|
+
this.ctx = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private render(): boolean {
|
|
48
|
+
const ctx = this.ctx;
|
|
49
|
+
if (!ctx?.hasUI) return false;
|
|
50
|
+
|
|
51
|
+
const lines = buildWorkflowMonitorLines(this.store);
|
|
52
|
+
if (lines.length === 0) {
|
|
53
|
+
this.dispose();
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "belowEditor" });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildWorkflowMonitorLines(store: AgentStore): string[] {
|
|
63
|
+
const workflows = listActiveWorkflows(store);
|
|
64
|
+
if (workflows.length === 0) return [];
|
|
65
|
+
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
for (const workflow of workflows.slice(0, MAX_MONITORED_WORKFLOWS)) {
|
|
68
|
+
appendWorkflowLines(lines, store, workflow);
|
|
69
|
+
if (lines.length >= MAX_WIDGET_LINES) break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hiddenWorkflowCount = workflows.length - MAX_MONITORED_WORKFLOWS;
|
|
73
|
+
if (hiddenWorkflowCount > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
74
|
+
lines.push(`... +${hiddenWorkflowCount} more active ${pluralize("workflow", hiddenWorkflowCount)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.slice(0, MAX_WIDGET_LINES);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function appendWorkflowLines(lines: string[], store: AgentStore, workflow: WorkflowRun): void {
|
|
81
|
+
if (lines.length >= MAX_WIDGET_LINES) return;
|
|
82
|
+
|
|
83
|
+
const stage = getCurrentStage(workflow);
|
|
84
|
+
const stageLabel = stage ? formatStageLabel(store, workflow, stage) : "none · agents 0/0";
|
|
85
|
+
lines.push(`${formatNamedEntityLabel(workflow)} | ${stageLabel}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function listActiveWorkflows(store: AgentStore): WorkflowRun[] {
|
|
89
|
+
return store.listWorkflows().filter((workflow) => !isTerminalAgentState(workflow.state));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getCurrentStage(workflow: WorkflowRun): WorkflowStageRun | undefined {
|
|
93
|
+
return (
|
|
94
|
+
workflow.stages[workflow.currentStageIndex] ?? workflow.stages.find((stage) => !isTerminalAgentState(stage.state))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatStageLabel(store: AgentStore, workflow: WorkflowRun, stage: WorkflowStageRun): string {
|
|
99
|
+
const stageIndex = workflow.stages.indexOf(stage);
|
|
100
|
+
const stagePosition = stageIndex >= 0 ? `${stageIndex + 1}/${workflow.stages.length}` : `?/${workflow.stages.length}`;
|
|
101
|
+
return `${stage.name} · step ${stagePosition} · agents ${formatStageProgress(store, stage)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string {
|
|
105
|
+
const progress = calculateStageProgress(store, stage);
|
|
106
|
+
return `${progress.completed}/${progress.total}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function calculateStageProgress(store: AgentStore, stage: WorkflowStageRun): { completed: number; total: number } {
|
|
110
|
+
const runs = collectStageRuns(store, stage);
|
|
111
|
+
const completed = runs.filter((run) => isTerminalAgentState(run.state)).length;
|
|
112
|
+
const workerRunCount = runs.filter((run) => run.id !== stage.leaderRunId).length;
|
|
113
|
+
const workerTotal = Math.max(stage.members.length, stage.workerRunIds.length, workerRunCount);
|
|
114
|
+
const leaderTotal = stage.phase === "leader" || stage.leaderRunId !== undefined ? 1 : 0;
|
|
115
|
+
const total = Math.max(workerTotal + leaderTotal, runs.length);
|
|
116
|
+
return { completed: Math.min(completed, total), total };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectStageRuns(store: AgentStore, stage: WorkflowStageRun): AgentRun[] {
|
|
120
|
+
const runsById = new Map<string, AgentRun>();
|
|
121
|
+
|
|
122
|
+
for (const runId of stage.workerRunIds) {
|
|
123
|
+
const run = store.getRun(runId);
|
|
124
|
+
if (run) runsById.set(run.id, run);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (stage.leaderRunId) {
|
|
128
|
+
const leaderRun = store.getRun(stage.leaderRunId);
|
|
129
|
+
if (leaderRun) runsById.set(leaderRun.id, leaderRun);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (stage.busId) {
|
|
133
|
+
for (const run of store.listRuns().filter((current) => current.busId === stage.busId)) {
|
|
134
|
+
runsById.set(run.id, run);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [...runsById.values()];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pluralize(noun: string, count: number): string {
|
|
142
|
+
return count === 1 ? noun : `${noun}s`;
|
|
143
|
+
}
|
package/src/tools/bus.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import type { AgentRun } from "../core/subagent.ts";
|
|
4
3
|
import type { Bus, BusMessage } from "../core/bus.ts";
|
|
5
|
-
import type { OrchestraApi
|
|
4
|
+
import type { OrchestraApi } from "../core/orchestra.ts";
|
|
6
5
|
import { formatNamedEntityLabel } from "../utils.ts";
|
|
7
6
|
|
|
8
7
|
export type BusInput =
|
|
@@ -19,31 +18,11 @@ export type BusInput =
|
|
|
19
18
|
id: string;
|
|
20
19
|
message: string;
|
|
21
20
|
from?: string;
|
|
22
|
-
}
|
|
23
|
-
| {
|
|
24
|
-
action: "wait_settled";
|
|
25
|
-
id: string;
|
|
26
|
-
/** Defaults to 10 minutes. Use null to wait indefinitely. */
|
|
27
|
-
timeoutMs?: number | null;
|
|
28
|
-
}
|
|
29
|
-
| {
|
|
30
|
-
action: "wait_next";
|
|
31
|
-
id: string;
|
|
32
|
-
/** Run ids or names to ignore. */
|
|
33
|
-
excludeRunIds?: string[];
|
|
34
|
-
/** Defaults to 10 minutes. Use null to wait indefinitely. */
|
|
35
|
-
timeoutMs?: number | null;
|
|
36
21
|
};
|
|
37
22
|
|
|
38
23
|
export interface BusOutput {
|
|
39
24
|
bus?: Bus;
|
|
40
25
|
busMessage?: BusMessage;
|
|
41
|
-
run?: AgentRun;
|
|
42
|
-
runResult?: WaitRunResult;
|
|
43
|
-
runs?: AgentRun[];
|
|
44
|
-
runResults?: WaitRunResult[];
|
|
45
|
-
timedOut?: boolean;
|
|
46
|
-
pendingRunIds?: string[];
|
|
47
26
|
message: string;
|
|
48
27
|
}
|
|
49
28
|
|
|
@@ -57,8 +36,8 @@ export interface BusToolDeps {
|
|
|
57
36
|
}
|
|
58
37
|
|
|
59
38
|
const BusActionParams = Type.String({
|
|
60
|
-
enum: ["create", "status", "publish"
|
|
61
|
-
description: "create/status/publish
|
|
39
|
+
enum: ["create", "status", "publish"],
|
|
40
|
+
description: "create/status/publish shared context buses; completion is delivered through pi-orchestra events.",
|
|
62
41
|
});
|
|
63
42
|
|
|
64
43
|
const BusToolParams = Type.Object(
|
|
@@ -79,24 +58,6 @@ const BusToolParams = Type.Object(
|
|
|
79
58
|
description: "Required for action=publish. Shared context for attached agents.",
|
|
80
59
|
}),
|
|
81
60
|
),
|
|
82
|
-
excludeRunIds: Type.Optional(
|
|
83
|
-
Type.Array(Type.String(), {
|
|
84
|
-
description: "Optional for action=wait_next. Already handled run ids/names.",
|
|
85
|
-
}),
|
|
86
|
-
),
|
|
87
|
-
timeoutMs: Type.Optional(
|
|
88
|
-
Type.Union(
|
|
89
|
-
[
|
|
90
|
-
Type.Number({
|
|
91
|
-
exclusiveMinimum: 0,
|
|
92
|
-
}),
|
|
93
|
-
Type.Null(),
|
|
94
|
-
],
|
|
95
|
-
{
|
|
96
|
-
description: "Optional for wait actions. Positive ms; default 10 min; null waits indefinitely.",
|
|
97
|
-
},
|
|
98
|
-
),
|
|
99
|
-
),
|
|
100
61
|
},
|
|
101
62
|
{ additionalProperties: false },
|
|
102
63
|
);
|
|
@@ -118,35 +79,6 @@ export function createBusTool({ orchestra }: BusToolDeps): BusTool {
|
|
|
118
79
|
return { bus, message: formatBusStatus(bus) };
|
|
119
80
|
}
|
|
120
81
|
|
|
121
|
-
if (input.action === "wait_settled") {
|
|
122
|
-
const output = await orchestra.waitBusSettled(bus.id, { timeoutMs: input.timeoutMs });
|
|
123
|
-
return {
|
|
124
|
-
bus: output.bus,
|
|
125
|
-
runs: output.runs,
|
|
126
|
-
runResults: output.runResults,
|
|
127
|
-
timedOut: output.timedOut,
|
|
128
|
-
pendingRunIds: output.pendingRunIds,
|
|
129
|
-
message: formatWaitBusSettledMessage(output),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (input.action === "wait_next") {
|
|
134
|
-
const output = await orchestra.waitNextRun(bus.id, {
|
|
135
|
-
excludeRunIds: input.excludeRunIds,
|
|
136
|
-
timeoutMs: input.timeoutMs,
|
|
137
|
-
});
|
|
138
|
-
return {
|
|
139
|
-
bus: output.bus,
|
|
140
|
-
run: output.run,
|
|
141
|
-
runResult: output.runResult,
|
|
142
|
-
runs: output.runs,
|
|
143
|
-
runResults: output.runResults,
|
|
144
|
-
timedOut: output.timedOut,
|
|
145
|
-
pendingRunIds: output.pendingRunIds,
|
|
146
|
-
message: formatWaitNextRunMessage(output),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
82
|
const published = await orchestra.publishBus(input.id, input.message, input.from ?? "main");
|
|
151
83
|
return {
|
|
152
84
|
bus: published.bus,
|
|
@@ -161,13 +93,12 @@ export function defineBusPiTool(resolveTool: (ctx: ExtensionContext) => BusTool)
|
|
|
161
93
|
return defineTool({
|
|
162
94
|
name: "bus",
|
|
163
95
|
label: "Bus",
|
|
164
|
-
description: "Create, inspect, publish to
|
|
165
|
-
promptSnippet:
|
|
166
|
-
"Use one bus per delegated work item; spawn related subagents on it and collect results with wait actions.",
|
|
96
|
+
description: "Create, inspect, and publish to work buses.",
|
|
97
|
+
promptSnippet: "Use one bus per delegated work item; spawn related subagents or workgroups on it.",
|
|
167
98
|
promptGuidelines: [
|
|
168
|
-
"
|
|
169
|
-
"publish
|
|
170
|
-
"
|
|
99
|
+
"Use bus create before spawning related subagents or workgroups; reuse it for that work item.",
|
|
100
|
+
"Use bus publish to send shared context to attached agents; bus status shows published messages.",
|
|
101
|
+
"Do not wait on buses; pi-orchestra delivers subagent and workgroup finish events automatically.",
|
|
171
102
|
],
|
|
172
103
|
parameters: BusToolParams,
|
|
173
104
|
executionMode: "sequential",
|
|
@@ -190,21 +121,6 @@ function toBusInput(params: RawBusParams): BusInput {
|
|
|
190
121
|
return { action: "status", id: params.id };
|
|
191
122
|
}
|
|
192
123
|
|
|
193
|
-
if (params.action === "wait_settled") {
|
|
194
|
-
if (!params.id) throw new Error("bus action=wait_settled requires id.");
|
|
195
|
-
return { action: "wait_settled", id: params.id, timeoutMs: params.timeoutMs };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (params.action === "wait_next") {
|
|
199
|
-
if (!params.id) throw new Error("bus action=wait_next requires id.");
|
|
200
|
-
return {
|
|
201
|
-
action: "wait_next",
|
|
202
|
-
id: params.id,
|
|
203
|
-
excludeRunIds: params.excludeRunIds,
|
|
204
|
-
timeoutMs: params.timeoutMs,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
124
|
if (!params.id) throw new Error("bus action=publish requires id.");
|
|
209
125
|
if (!params.message) throw new Error("bus action=publish requires message.");
|
|
210
126
|
return { action: "publish", id: params.id, message: params.message };
|
|
@@ -227,49 +143,9 @@ function formatBusMessage(message: BusMessage): string {
|
|
|
227
143
|
return [`- ${message.id} from ${message.from}:`, message.message].join("\n");
|
|
228
144
|
}
|
|
229
145
|
|
|
230
|
-
function formatWaitBusSettledMessage(result: WaitBusSettledResult): string {
|
|
231
|
-
const busLabel = formatNamedEntityLabel(result.bus);
|
|
232
|
-
const headline = result.timedOut
|
|
233
|
-
? `Timed out waiting for bus ${busLabel} to settle; ${result.pendingRunIds.length} run(s) still pending.`
|
|
234
|
-
: `All ${result.runs.length} run(s) attached to bus ${busLabel} reached terminal state.`;
|
|
235
|
-
if (result.runs.length === 0) return headline;
|
|
236
|
-
|
|
237
|
-
return [headline, "", "Runs:", ...result.runs.map(formatRunSummary)].join("\n");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function formatRunSummary(run: AgentRun): string {
|
|
241
|
-
const runLabel = formatNamedEntityLabel(run);
|
|
242
|
-
if (!run.result) return `- ${runLabel}: ${run.state}`;
|
|
243
|
-
return `- ${runLabel}: ${run.state} result=${run.result.status} summary=${run.result.summary}`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function formatWaitNextRunMessage(result: WaitNextRunResult): string {
|
|
247
|
-
const busLabel = formatNamedEntityLabel(result.bus);
|
|
248
|
-
if (result.run) {
|
|
249
|
-
return [
|
|
250
|
-
`Next terminal run on bus ${busLabel}: ${formatNamedEntityLabel(result.run)} is ${result.run.state}.`,
|
|
251
|
-
"",
|
|
252
|
-
formatRunResult(result.run),
|
|
253
|
-
].join("\n");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (result.timedOut) {
|
|
257
|
-
return `Timed out waiting for the next run on bus ${busLabel}; ${result.pendingRunIds.length} run(s) still pending.`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return `No unhandled current runs remain on bus ${busLabel}.`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function formatRunResult(run: AgentRun): string {
|
|
264
|
-
if (!run.result) return "No result payload recorded.";
|
|
265
|
-
return [`Result: ${run.result.status}`, run.result.summary].join("\n");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
146
|
type RawBusParams = {
|
|
269
|
-
action: "create" | "status" | "publish"
|
|
147
|
+
action: "create" | "status" | "publish";
|
|
270
148
|
name?: string;
|
|
271
149
|
id?: string;
|
|
272
150
|
message?: string;
|
|
273
|
-
excludeRunIds?: string[];
|
|
274
|
-
timeoutMs?: number | null;
|
|
275
151
|
};
|