@g3un/pi-orchestra 0.1.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.
@@ -0,0 +1,275 @@
1
+ import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { AgentRun } from "../core/subagent.ts";
4
+ import type { Bus, BusMessage } from "../core/bus.ts";
5
+ import type { OrchestraApi, WaitBusSettledResult, WaitNextRunResult, WaitRunResult } from "../core/orchestra.ts";
6
+ import { formatNamedEntityLabel } from "../utils.ts";
7
+
8
+ export type BusInput =
9
+ | {
10
+ action: "create";
11
+ name?: string;
12
+ }
13
+ | {
14
+ action: "status";
15
+ id: string;
16
+ }
17
+ | {
18
+ action: "publish";
19
+ id: string;
20
+ message: string;
21
+ 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
+ };
37
+
38
+ export interface BusOutput {
39
+ bus?: Bus;
40
+ busMessage?: BusMessage;
41
+ run?: AgentRun;
42
+ runResult?: WaitRunResult;
43
+ runs?: AgentRun[];
44
+ runResults?: WaitRunResult[];
45
+ timedOut?: boolean;
46
+ pendingRunIds?: string[];
47
+ message: string;
48
+ }
49
+
50
+ export interface BusTool {
51
+ name: "bus";
52
+ execute(input: BusInput): Promise<BusOutput>;
53
+ }
54
+
55
+ export interface BusToolDeps {
56
+ orchestra: OrchestraApi;
57
+ }
58
+
59
+ const BusActionParams = Type.String({
60
+ enum: ["create", "status", "publish", "wait_settled", "wait_next"],
61
+ description: "create/status/publish; wait_settled waits all attached runs; wait_next waits the next terminal run.",
62
+ });
63
+
64
+ const BusToolParams = Type.Object(
65
+ {
66
+ action: BusActionParams,
67
+ name: Type.Optional(
68
+ Type.String({
69
+ description: "Optional short bus name for action=create.",
70
+ }),
71
+ ),
72
+ id: Type.Optional(
73
+ Type.String({
74
+ description: "Required except create. Bus id/name returned by action=create.",
75
+ }),
76
+ ),
77
+ message: Type.Optional(
78
+ Type.String({
79
+ description: "Required for action=publish. Shared context for attached agents.",
80
+ }),
81
+ ),
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
+ },
101
+ { additionalProperties: false },
102
+ );
103
+
104
+ export function createBusTool({ orchestra }: BusToolDeps): BusTool {
105
+ return {
106
+ name: "bus",
107
+
108
+ async execute(input) {
109
+ if (input.action === "create") {
110
+ const bus = orchestra.createBus({ name: input.name });
111
+ return { bus, message: formatBusStatus(bus, `Created bus ${formatNamedEntityLabel(bus)}.`) };
112
+ }
113
+
114
+ const bus = orchestra.getBus(input.id);
115
+ if (!bus) return { message: formatBusNotFound(input.id) };
116
+
117
+ if (input.action === "status") {
118
+ return { bus, message: formatBusStatus(bus) };
119
+ }
120
+
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
+ const published = await orchestra.publishBus(input.id, input.message, input.from ?? "main");
151
+ return {
152
+ bus: published.bus,
153
+ busMessage: published.busMessage,
154
+ message: formatBusStatus(published.bus, `Published message to bus ${formatNamedEntityLabel(published.bus)}.`),
155
+ };
156
+ },
157
+ };
158
+ }
159
+
160
+ export function defineBusPiTool(resolveTool: (ctx: ExtensionContext) => BusTool) {
161
+ return defineTool({
162
+ name: "bus",
163
+ label: "Bus",
164
+ description: "Create, inspect, publish to, and wait on work buses.",
165
+ promptSnippet:
166
+ "Use one bus per delegated work item; spawn related subagents on it and collect results with wait actions.",
167
+ promptGuidelines: [
168
+ "Create a bus before spawning related subagents; reuse it for that work item.",
169
+ "publish sends shared context to attached agents; status shows published messages.",
170
+ "wait_next handles results as they arrive; wait_settled waits for full fan-in.",
171
+ ],
172
+ parameters: BusToolParams,
173
+ executionMode: "sequential",
174
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
175
+ const output = await resolveTool(ctx).execute(toBusInput(params as RawBusParams));
176
+
177
+ return {
178
+ content: [{ type: "text", text: output.message }],
179
+ details: output,
180
+ };
181
+ },
182
+ });
183
+ }
184
+
185
+ function toBusInput(params: RawBusParams): BusInput {
186
+ if (params.action === "create") return { action: "create", name: params.name };
187
+
188
+ if (params.action === "status") {
189
+ if (!params.id) throw new Error("bus action=status requires id.");
190
+ return { action: "status", id: params.id };
191
+ }
192
+
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
+ if (!params.id) throw new Error("bus action=publish requires id.");
209
+ if (!params.message) throw new Error("bus action=publish requires message.");
210
+ return { action: "publish", id: params.id, message: params.message };
211
+ }
212
+
213
+ function formatBusNotFound(id: string): string {
214
+ return `Bus ${id} not found.`;
215
+ }
216
+
217
+ function formatBusStatus(
218
+ bus: Bus,
219
+ headline = `Bus ${formatNamedEntityLabel(bus)} has ${bus.messages.length} message(s).`,
220
+ ): string {
221
+ if (bus.messages.length === 0) return headline;
222
+
223
+ return [headline, "", "Messages:", ...bus.messages.map(formatBusMessage)].join("\n");
224
+ }
225
+
226
+ function formatBusMessage(message: BusMessage): string {
227
+ return [`- ${message.id} from ${message.from}:`, message.message].join("\n");
228
+ }
229
+
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
+ type RawBusParams = {
269
+ action: "create" | "status" | "publish" | "wait_settled" | "wait_next";
270
+ name?: string;
271
+ id?: string;
272
+ message?: string;
273
+ excludeRunIds?: string[];
274
+ timeoutMs?: number | null;
275
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./bus.ts";
2
+ export * from "./subagent.ts";
3
+ export * from "./workflow.ts";
4
+ export * from "./workgroup.ts";
@@ -0,0 +1,243 @@
1
+ import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { AgentProfile, AgentRun } from "../core/subagent.ts";
4
+ import type { OrchestraApi } from "../core/orchestra.ts";
5
+ import { formatNamedEntityLabel } from "../utils.ts";
6
+
7
+ export interface SubagentSpawnInput {
8
+ action: "spawn";
9
+ profile: AgentProfile;
10
+ task: string;
11
+ busId: string;
12
+ name?: string;
13
+ }
14
+
15
+ export type SubagentInput =
16
+ | SubagentSpawnInput
17
+ | {
18
+ action: "status";
19
+ id: string;
20
+ busId?: string;
21
+ }
22
+ | {
23
+ action: "message";
24
+ id: string;
25
+ message: string;
26
+ busId?: string;
27
+ }
28
+ | {
29
+ action: "close";
30
+ id: string;
31
+ busId?: string;
32
+ };
33
+
34
+ export interface SubagentOutput {
35
+ run?: AgentRun;
36
+ message: string;
37
+ }
38
+
39
+ export interface SubagentTool {
40
+ name: "subagent";
41
+ execute(input: SubagentInput): Promise<SubagentOutput>;
42
+ }
43
+
44
+ export interface SubagentToolDeps {
45
+ orchestra: OrchestraApi;
46
+ }
47
+
48
+ export const SubagentRunNameParam = Type.String({
49
+ description: "Optional globally unique short run name.",
50
+ });
51
+
52
+ export const AgentProfileParams = Type.Object(
53
+ {
54
+ name: Type.String({ description: "Short role/name for the subagent." }),
55
+ systemPrompt: Type.String({ description: "System prompt for the subagent." }),
56
+ tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist for the subagent." })),
57
+ model: Type.Optional(
58
+ Type.String({
59
+ description: "Optional provider/model id.",
60
+ }),
61
+ ),
62
+ },
63
+ { description: "Required for action=spawn. Defines the subagent role." },
64
+ );
65
+
66
+ const SubagentActionParams = Type.String({
67
+ enum: ["spawn", "status", "message", "close"],
68
+ description: "spawn creates; status inspects; message steers/restarts; close disposes by id/name.",
69
+ });
70
+
71
+ const SubagentToolParams = Type.Object(
72
+ {
73
+ action: SubagentActionParams,
74
+ profile: Type.Optional(AgentProfileParams),
75
+ task: Type.Optional(
76
+ Type.String({
77
+ description: "Required for action=spawn. Task to delegate to the new subagent.",
78
+ }),
79
+ ),
80
+ busId: Type.Optional(
81
+ Type.String({
82
+ description: "Required for action=spawn. Existing bus id/name.",
83
+ }),
84
+ ),
85
+ name: Type.Optional(SubagentRunNameParam),
86
+ id: Type.Optional(
87
+ Type.String({
88
+ description: "Required for status/message/close. Subagent run id/name.",
89
+ }),
90
+ ),
91
+ message: Type.Optional(
92
+ Type.String({
93
+ description: "Required for action=message. Instruction to send to the subagent.",
94
+ }),
95
+ ),
96
+ },
97
+ { additionalProperties: false },
98
+ );
99
+
100
+ export async function spawnSubagent(orchestra: OrchestraApi, input: SubagentSpawnInput): Promise<AgentRun> {
101
+ return await orchestra.spawnAgent(input.profile, input.task, input.busId, { name: input.name });
102
+ }
103
+
104
+ export function createSubagentTool({ orchestra }: SubagentToolDeps): SubagentTool {
105
+ return {
106
+ name: "subagent",
107
+
108
+ async execute(input) {
109
+ if (input.action === "spawn") {
110
+ const run = await spawnSubagent(orchestra, input);
111
+ return { run, message: formatRunMessage(run) };
112
+ }
113
+
114
+ if (input.action === "status") {
115
+ const run = orchestra.getRun(input.id, { busId: input.busId });
116
+ if (!run) return { message: formatMissingSubagentMessage(input.id) };
117
+ return { run, message: formatRunMessage(run) };
118
+ }
119
+
120
+ if (input.action === "message") {
121
+ const run = await orchestra.messageAgent(input.id, input.message, { busId: input.busId });
122
+ return {
123
+ run,
124
+ message: formatRunMessage(run, `Messaged subagent ${formatNamedEntityLabel(run)}; it is ${run.state}.`),
125
+ };
126
+ }
127
+
128
+ const run = await orchestra.closeAgent(input.id, { busId: input.busId });
129
+ return {
130
+ run,
131
+ message: run
132
+ ? formatRunMessage(run, `Closed subagent ${formatNamedEntityLabel(run)}.`)
133
+ : formatClosedMissingSubagentMessage(input.id),
134
+ };
135
+ },
136
+ };
137
+ }
138
+
139
+ export function defineSubagentPiTool(resolveTool: (ctx: ExtensionContext) => SubagentTool) {
140
+ return defineTool({
141
+ name: "subagent",
142
+ label: "Subagent",
143
+ description: "Create and manage isolated subagents.",
144
+ promptSnippet: "Spawn a subagent on an existing bus, then status/message/close it later.",
145
+ promptGuidelines: [
146
+ "Create a bus first; spawn attaches the subagent via busId.",
147
+ "Attach cooperating subagents to the same bus.",
148
+ "Use returned run id/name for status, message, or close.",
149
+ ],
150
+ parameters: SubagentToolParams,
151
+ executionMode: "parallel",
152
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
153
+ const input = withDefaultModel(toSubagentInput(params as RawSubagentParams), ctx);
154
+ const output = await resolveTool(ctx).execute(input);
155
+
156
+ return {
157
+ content: [{ type: "text", text: output.message }],
158
+ details: output,
159
+ };
160
+ },
161
+ });
162
+ }
163
+
164
+ function toSubagentInput(params: RawSubagentParams): SubagentInput {
165
+ if (params.action === "spawn") {
166
+ if (!params.profile) throw new Error("subagent action=spawn requires profile.");
167
+ if (!params.task) throw new Error("subagent action=spawn requires task.");
168
+ if (!params.busId) throw new Error("subagent action=spawn requires busId.");
169
+ return { action: "spawn", profile: params.profile, task: params.task, busId: params.busId, name: params.name };
170
+ }
171
+
172
+ if (params.action === "status") {
173
+ if (!params.id) throw new Error("subagent action=status requires id.");
174
+ return { action: "status", id: params.id, busId: params.busId };
175
+ }
176
+
177
+ if (params.action === "message") {
178
+ if (!params.id) throw new Error("subagent action=message requires id.");
179
+ if (!params.message) throw new Error("subagent action=message requires message.");
180
+ return { action: "message", id: params.id, message: params.message, busId: params.busId };
181
+ }
182
+
183
+ if (!params.id) throw new Error("subagent action=close requires id.");
184
+ return { action: "close", id: params.id, busId: params.busId };
185
+ }
186
+
187
+ function withDefaultModel(input: SubagentInput, ctx: ExtensionContext): SubagentInput {
188
+ if (input.action !== "spawn" || input.profile.model || !ctx.model) return input;
189
+ return {
190
+ ...input,
191
+ profile: withDefaultProfileModel(input.profile, ctx),
192
+ };
193
+ }
194
+
195
+ export function withDefaultProfileModel(profile: AgentProfile, ctx: ExtensionContext): AgentProfile {
196
+ if (profile.model || !ctx.model) return profile;
197
+ return {
198
+ ...profile,
199
+ model: formatModelId(ctx.model),
200
+ };
201
+ }
202
+
203
+ function formatMissingSubagentMessage(id: string): string {
204
+ return `Subagent ${id} not found.`;
205
+ }
206
+
207
+ function formatClosedMissingSubagentMessage(id: string): string {
208
+ return `Closed subagent ${id}.`;
209
+ }
210
+
211
+ function formatRunMessage(
212
+ run: AgentRun,
213
+ headline = `Subagent ${formatNamedEntityLabel(run)} is ${run.state}.`,
214
+ ): string {
215
+ if (!run.result) return headline;
216
+
217
+ const parts = [headline, "", `Result: ${run.result.status}`, run.result.summary];
218
+ if (run.result.data !== undefined) {
219
+ parts.push("", "Data:", formatResultData(run.result.data));
220
+ }
221
+ return parts.join("\n");
222
+ }
223
+
224
+ function formatResultData(data: unknown): string {
225
+ if (typeof data === "string") return data;
226
+ return JSON.stringify(data, null, 2) ?? String(data);
227
+ }
228
+
229
+ function formatModelId(model: AgentProfileModel): string {
230
+ return `${model.provider}/${model.id}`;
231
+ }
232
+
233
+ type RawSubagentParams = {
234
+ action: "spawn" | "status" | "message" | "close";
235
+ profile?: AgentProfile;
236
+ task?: string;
237
+ busId?: string;
238
+ name?: string;
239
+ id?: string;
240
+ message?: string;
241
+ };
242
+
243
+ type AgentProfileModel = NonNullable<ExtensionContext["model"]>;