@g3un/pi-orchestra 0.2.1 → 0.9.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/README.md +14 -3
- package/docs/orchestration-model.md +17 -12
- package/package.json +5 -1
- package/skills/pi-orchestra/SKILL.md +92 -0
- package/src/adapters/in-memory-store.ts +45 -20
- package/src/adapters/index.ts +1 -0
- package/src/adapters/pi-runtime.ts +114 -38
- package/src/adapters/sqlite-store.ts +276 -0
- package/src/adapters/store-subscriptions.ts +20 -0
- package/src/core/bus-format.ts +13 -4
- package/src/core/bus.ts +66 -0
- package/src/core/orchestra.ts +21 -26
- package/src/core/store.ts +12 -3
- package/src/core/subagent.ts +5 -3
- package/src/core/workflow.ts +5 -4
- package/src/core/workgroup.ts +3 -3
- package/src/extension/index.ts +13 -5
- package/src/extension/orchestra-events.ts +107 -24
- package/src/extension/workflow-monitor.ts +16 -14
- package/src/profiles/code-reviewer.ts +20 -0
- package/src/profiles/evidence-synthesizer.ts +20 -0
- package/src/profiles/external-researcher.ts +20 -0
- package/src/profiles/index.ts +5 -1
- package/src/profiles/profile.ts +31 -0
- package/src/profiles/source-code-qa.ts +19 -0
- package/src/tools/bus.ts +93 -35
- package/src/tools/subagent.ts +29 -8
- package/src/tools/workflow.ts +47 -50
- package/src/tools/workgroup.ts +22 -14
- package/src/utils.ts +12 -2
- package/src/profiles/stage-leader.ts +0 -20
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Records are persisted as JSON payloads keyed by id rather than normalized columns:
|
|
2
|
+
// the store is accessed only by primary key (TypeScript owns the shape, querying/filtering
|
|
3
|
+
// happens in the application layer), so a document layout avoids joins and lets the domain
|
|
4
|
+
// types evolve without schema migrations.
|
|
5
|
+
import { mkdirSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
|
8
|
+
import type { AgentRun } from "../core/subagent.ts";
|
|
9
|
+
import {
|
|
10
|
+
matchesBusSubscription,
|
|
11
|
+
type Bus,
|
|
12
|
+
type BusMessage,
|
|
13
|
+
type BusMessageEvent,
|
|
14
|
+
type BusSubscription,
|
|
15
|
+
type ListBusSubscriptionsOptions,
|
|
16
|
+
} from "../core/bus.ts";
|
|
17
|
+
import type { AgentStore } from "../core/store.ts";
|
|
18
|
+
import type { WorkflowRun } from "../core/workflow.ts";
|
|
19
|
+
import { notifySubscribers, subscribeStore, type StoreSubscription } from "./store-subscriptions.ts";
|
|
20
|
+
|
|
21
|
+
const SCHEMA_VERSION = 2;
|
|
22
|
+
const ORCHESTRA_STORE_RELATIVE_DIR = join(".pi", "orchestra");
|
|
23
|
+
const ORCHESTRA_STORE_FILENAME = "store.db";
|
|
24
|
+
|
|
25
|
+
export interface SqliteAgentStoreOptions {
|
|
26
|
+
databasePath: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PayloadRow {
|
|
30
|
+
payload_json: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SqliteAgentStore implements AgentStore {
|
|
34
|
+
private readonly db: DatabaseSync;
|
|
35
|
+
private readonly statements: StoreStatements;
|
|
36
|
+
private readonly runSubscriptions = new Set<StoreSubscription<AgentRun>>();
|
|
37
|
+
private readonly busMessageSubscriptions = new Set<StoreSubscription<BusMessageEvent>>();
|
|
38
|
+
private readonly workflowSubscriptions = new Set<StoreSubscription<WorkflowRun>>();
|
|
39
|
+
private closed = false;
|
|
40
|
+
|
|
41
|
+
constructor(options: SqliteAgentStoreOptions) {
|
|
42
|
+
mkdirSync(dirname(options.databasePath), { recursive: true });
|
|
43
|
+
this.db = new DatabaseSync(options.databasePath, { timeout: 5_000 });
|
|
44
|
+
initializeSchema(this.db);
|
|
45
|
+
this.statements = prepareStatements(this.db);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
saveRun(run: AgentRun): void {
|
|
49
|
+
this.statements.saveRun.run(run.id, stringifyPayload(run, `run ${run.id}`));
|
|
50
|
+
notifySubscribers(this.runSubscriptions, run);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getRun(id: string): AgentRun | undefined {
|
|
54
|
+
return getPayload(this.statements.getRun, id, `run ${id}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
listRuns(): AgentRun[] {
|
|
58
|
+
return listPayloads(this.statements.listRuns, "runs");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
subscribeRuns(listener: (run: AgentRun) => void, filter: ((run: AgentRun) => boolean) | undefined): () => void {
|
|
62
|
+
return subscribeStore(this.runSubscriptions, listener, filter);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
saveBus(bus: Bus): void {
|
|
66
|
+
this.statements.saveBus.run(bus.id, stringifyPayload(bus, `bus ${bus.id}`));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getBus(id: string): Bus | undefined {
|
|
70
|
+
return getPayload(this.statements.getBus, id, `bus ${id}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
listBuses(): Bus[] {
|
|
74
|
+
return listPayloads(this.statements.listBuses, "buses");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addBusMessage(busId: string, message: BusMessage): void {
|
|
78
|
+
const bus = this.getBus(busId);
|
|
79
|
+
if (!bus) throw new Error(`Bus ${busId} not found.`);
|
|
80
|
+
|
|
81
|
+
const existingIndex = bus.messages.findIndex((current) => current.id === message.id);
|
|
82
|
+
const messages = [...bus.messages];
|
|
83
|
+
if (existingIndex >= 0) {
|
|
84
|
+
messages[existingIndex] = message;
|
|
85
|
+
this.saveBus({ ...bus, messages });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
messages.push(message);
|
|
90
|
+
this.saveBus({ ...bus, messages });
|
|
91
|
+
notifySubscribers(this.busMessageSubscriptions, { busId, message });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
subscribeBusMessages(
|
|
95
|
+
listener: (event: BusMessageEvent) => void,
|
|
96
|
+
filter: ((event: BusMessageEvent) => boolean) | undefined,
|
|
97
|
+
): () => void {
|
|
98
|
+
return subscribeStore(this.busMessageSubscriptions, listener, filter);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
saveBusSubscription(subscription: BusSubscription): void {
|
|
102
|
+
this.statements.saveBusSubscription.run(
|
|
103
|
+
subscription.id,
|
|
104
|
+
stringifyPayload(subscription, `bus subscription ${subscription.id}`),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getBusSubscription(id: string): BusSubscription | undefined {
|
|
109
|
+
return getPayload(this.statements.getBusSubscription, id, `bus subscription ${id}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
listBusSubscriptions(options: ListBusSubscriptionsOptions): BusSubscription[] {
|
|
113
|
+
return listPayloads<BusSubscription>(this.statements.listBusSubscriptions, "bus subscriptions").filter(
|
|
114
|
+
(subscription) => matchesBusSubscription(subscription, options),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
deleteBusSubscription(id: string): void {
|
|
119
|
+
this.statements.deleteBusSubscription.run(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
saveWorkflow(workflow: WorkflowRun): void {
|
|
123
|
+
this.statements.saveWorkflow.run(workflow.id, stringifyPayload(workflow, `workflow ${workflow.id}`));
|
|
124
|
+
notifySubscribers(this.workflowSubscriptions, workflow);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getWorkflow(id: string): WorkflowRun | undefined {
|
|
128
|
+
return getPayload(this.statements.getWorkflow, id, `workflow ${id}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
listWorkflows(): WorkflowRun[] {
|
|
132
|
+
return listPayloads(this.statements.listWorkflows, "workflows");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
subscribeWorkflows(
|
|
136
|
+
listener: (workflow: WorkflowRun) => void,
|
|
137
|
+
filter: ((workflow: WorkflowRun) => boolean) | undefined,
|
|
138
|
+
): () => void {
|
|
139
|
+
return subscribeStore(this.workflowSubscriptions, listener, filter);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
dispose(): void {
|
|
143
|
+
if (this.closed) return;
|
|
144
|
+
this.db.close();
|
|
145
|
+
this.closed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface StoreStatements {
|
|
150
|
+
saveRun: StatementSync;
|
|
151
|
+
getRun: StatementSync;
|
|
152
|
+
listRuns: StatementSync;
|
|
153
|
+
saveBus: StatementSync;
|
|
154
|
+
getBus: StatementSync;
|
|
155
|
+
listBuses: StatementSync;
|
|
156
|
+
saveBusSubscription: StatementSync;
|
|
157
|
+
getBusSubscription: StatementSync;
|
|
158
|
+
listBusSubscriptions: StatementSync;
|
|
159
|
+
deleteBusSubscription: StatementSync;
|
|
160
|
+
saveWorkflow: StatementSync;
|
|
161
|
+
getWorkflow: StatementSync;
|
|
162
|
+
listWorkflows: StatementSync;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getProjectSqliteStorePath(cwd: string): string {
|
|
166
|
+
return join(cwd, ORCHESTRA_STORE_RELATIVE_DIR, ORCHESTRA_STORE_FILENAME);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createProjectSqliteAgentStore(cwd: string): SqliteAgentStore {
|
|
170
|
+
return new SqliteAgentStore({ databasePath: getProjectSqliteStorePath(cwd) });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function initializeSchema(db: DatabaseSync): void {
|
|
174
|
+
const schemaVersion = getSchemaVersion(db);
|
|
175
|
+
if (schemaVersion > SCHEMA_VERSION) {
|
|
176
|
+
throw new Error(`Unsupported pi-orchestra SQLite store schema version ${schemaVersion}.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
db.exec(`
|
|
180
|
+
PRAGMA journal_mode = WAL;
|
|
181
|
+
PRAGMA synchronous = NORMAL;
|
|
182
|
+
|
|
183
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
184
|
+
id TEXT PRIMARY KEY,
|
|
185
|
+
payload_json TEXT NOT NULL
|
|
186
|
+
) STRICT;
|
|
187
|
+
|
|
188
|
+
CREATE TABLE IF NOT EXISTS buses (
|
|
189
|
+
id TEXT PRIMARY KEY,
|
|
190
|
+
payload_json TEXT NOT NULL
|
|
191
|
+
) STRICT;
|
|
192
|
+
|
|
193
|
+
CREATE TABLE IF NOT EXISTS bus_subscriptions (
|
|
194
|
+
id TEXT PRIMARY KEY,
|
|
195
|
+
payload_json TEXT NOT NULL
|
|
196
|
+
) STRICT;
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
199
|
+
id TEXT PRIMARY KEY,
|
|
200
|
+
payload_json TEXT NOT NULL
|
|
201
|
+
) STRICT;
|
|
202
|
+
`);
|
|
203
|
+
|
|
204
|
+
if (schemaVersion < SCHEMA_VERSION) db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getSchemaVersion(db: DatabaseSync): number {
|
|
208
|
+
const row = db.prepare("PRAGMA user_version").get() as { user_version: number } | undefined;
|
|
209
|
+
return row?.user_version ?? 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function prepareStatements(db: DatabaseSync): StoreStatements {
|
|
213
|
+
return {
|
|
214
|
+
saveRun: db.prepare(`
|
|
215
|
+
INSERT INTO runs (id, payload_json)
|
|
216
|
+
VALUES (?, ?)
|
|
217
|
+
ON CONFLICT(id) DO UPDATE SET payload_json = excluded.payload_json
|
|
218
|
+
`),
|
|
219
|
+
getRun: db.prepare("SELECT payload_json FROM runs WHERE id = ?"),
|
|
220
|
+
listRuns: db.prepare("SELECT payload_json FROM runs ORDER BY rowid"),
|
|
221
|
+
saveBus: db.prepare(`
|
|
222
|
+
INSERT INTO buses (id, payload_json)
|
|
223
|
+
VALUES (?, ?)
|
|
224
|
+
ON CONFLICT(id) DO UPDATE SET payload_json = excluded.payload_json
|
|
225
|
+
`),
|
|
226
|
+
getBus: db.prepare("SELECT payload_json FROM buses WHERE id = ?"),
|
|
227
|
+
listBuses: db.prepare("SELECT payload_json FROM buses ORDER BY rowid"),
|
|
228
|
+
saveBusSubscription: db.prepare(`
|
|
229
|
+
INSERT INTO bus_subscriptions (id, payload_json)
|
|
230
|
+
VALUES (?, ?)
|
|
231
|
+
ON CONFLICT(id) DO UPDATE SET payload_json = excluded.payload_json
|
|
232
|
+
`),
|
|
233
|
+
getBusSubscription: db.prepare("SELECT payload_json FROM bus_subscriptions WHERE id = ?"),
|
|
234
|
+
listBusSubscriptions: db.prepare("SELECT payload_json FROM bus_subscriptions ORDER BY rowid"),
|
|
235
|
+
deleteBusSubscription: db.prepare("DELETE FROM bus_subscriptions WHERE id = ?"),
|
|
236
|
+
saveWorkflow: db.prepare(`
|
|
237
|
+
INSERT INTO workflows (id, payload_json)
|
|
238
|
+
VALUES (?, ?)
|
|
239
|
+
ON CONFLICT(id) DO UPDATE SET payload_json = excluded.payload_json
|
|
240
|
+
`),
|
|
241
|
+
getWorkflow: db.prepare("SELECT payload_json FROM workflows WHERE id = ?"),
|
|
242
|
+
listWorkflows: db.prepare("SELECT payload_json FROM workflows ORDER BY rowid"),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getPayload<T>(statement: StatementSync, id: string, label: string): T | undefined {
|
|
247
|
+
const row = statement.get(id) as PayloadRow | undefined;
|
|
248
|
+
if (!row) return undefined;
|
|
249
|
+
return parsePayload<T>(row.payload_json, label);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function listPayloads<T>(statement: StatementSync, label: string): T[] {
|
|
253
|
+
return (statement.all() as unknown as PayloadRow[]).map((row, index) =>
|
|
254
|
+
parsePayload<T>(row.payload_json, `${label}[${index}]`),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function stringifyPayload(value: unknown, label: string): string {
|
|
259
|
+
try {
|
|
260
|
+
const json = JSON.stringify(value);
|
|
261
|
+
if (json === undefined) throw new Error("payload is undefined");
|
|
262
|
+
return json;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
throw new Error(`Could not serialize ${label} for SQLite store: ${message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parsePayload<T>(payload: string, label: string): T {
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(payload) as T;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
274
|
+
throw new Error(`Could not parse ${label} from SQLite store: ${message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface StoreSubscription<T> {
|
|
2
|
+
listener(value: T): void;
|
|
3
|
+
filter: ((value: T) => boolean) | undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function subscribeStore<T>(
|
|
7
|
+
subscriptions: Set<StoreSubscription<T>>,
|
|
8
|
+
listener: (value: T) => void,
|
|
9
|
+
filter: ((value: T) => boolean) | undefined,
|
|
10
|
+
): () => void {
|
|
11
|
+
const subscription = { listener, filter };
|
|
12
|
+
subscriptions.add(subscription);
|
|
13
|
+
return () => subscriptions.delete(subscription);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function notifySubscribers<T>(subscriptions: Set<StoreSubscription<T>>, value: T): void {
|
|
17
|
+
for (const subscription of subscriptions) {
|
|
18
|
+
if (!subscription.filter || subscription.filter(value)) subscription.listener(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/core/bus-format.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import type { BusMessage } from "./bus.ts";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export interface FormatBusMessagesOptions {
|
|
4
|
+
formatFrom: ((from: string) => string) | undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatBusMessages(messages: BusMessage[], options?: FormatBusMessagesOptions): string {
|
|
4
8
|
return [
|
|
5
9
|
"<bus_reference_context>",
|
|
6
10
|
"Supplemental peer context; not the active task unless explicitly instructed.",
|
|
7
|
-
...messages.map(formatBusMessage),
|
|
11
|
+
...messages.map((message) => formatBusMessage(message, options)),
|
|
8
12
|
"</bus_reference_context>",
|
|
9
13
|
].join("\n");
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
function formatBusMessage(message: BusMessage): string {
|
|
13
|
-
|
|
16
|
+
function formatBusMessage(message: BusMessage, options: FormatBusMessagesOptions | undefined): string {
|
|
17
|
+
const from = options?.formatFrom?.(message.from) ?? message.from;
|
|
18
|
+
return [`<bus_message from="${escapeXmlAttribute(from)}">`, message.message, "</bus_message>"].join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeXmlAttribute(value: string): string {
|
|
22
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
14
23
|
}
|
package/src/core/bus.ts
CHANGED
|
@@ -4,8 +4,74 @@ export interface BusMessage {
|
|
|
4
4
|
from: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export interface BusMessageEvent {
|
|
8
|
+
busId: string;
|
|
9
|
+
message: BusMessage;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type BusSubscriberKind = "agent" | "main";
|
|
13
|
+
|
|
14
|
+
export interface BusSubscription {
|
|
15
|
+
id: string;
|
|
16
|
+
busId: string;
|
|
17
|
+
subscriberId: string;
|
|
18
|
+
subscriberKind: BusSubscriberKind;
|
|
19
|
+
deliveredMessageIds: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ListBusSubscriptionsOptions {
|
|
23
|
+
busId: string | undefined;
|
|
24
|
+
subscriberId: string | undefined;
|
|
25
|
+
subscriberKind: BusSubscriberKind | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export interface Bus {
|
|
8
29
|
id: string;
|
|
9
30
|
name: string;
|
|
10
31
|
messages: BusMessage[];
|
|
11
32
|
}
|
|
33
|
+
|
|
34
|
+
export interface CreateBusSubscriptionOptions {
|
|
35
|
+
busId: string;
|
|
36
|
+
subscriberId: string;
|
|
37
|
+
subscriberKind: BusSubscriberKind;
|
|
38
|
+
deliveredMessageIds: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createBusSubscriptionId(
|
|
42
|
+
busId: string,
|
|
43
|
+
subscriberKind: BusSubscriberKind,
|
|
44
|
+
subscriberId: string,
|
|
45
|
+
): string {
|
|
46
|
+
return `${subscriberKind}:${subscriberId}:bus:${busId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createBusSubscription(options: CreateBusSubscriptionOptions): BusSubscription {
|
|
50
|
+
return {
|
|
51
|
+
id: createBusSubscriptionId(options.busId, options.subscriberKind, options.subscriberId),
|
|
52
|
+
busId: options.busId,
|
|
53
|
+
subscriberId: options.subscriberId,
|
|
54
|
+
subscriberKind: options.subscriberKind,
|
|
55
|
+
deliveredMessageIds: options.deliveredMessageIds,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isBusMessageDelivered(subscription: BusSubscription, messageId: string): boolean {
|
|
60
|
+
return subscription.deliveredMessageIds.includes(messageId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function markBusMessagesDelivered(
|
|
64
|
+
subscription: BusSubscription,
|
|
65
|
+
messages: BusMessage | BusMessage[],
|
|
66
|
+
): BusSubscription {
|
|
67
|
+
const deliveredMessageIds = new Set(subscription.deliveredMessageIds);
|
|
68
|
+
for (const message of Array.isArray(messages) ? messages : [messages]) deliveredMessageIds.add(message.id);
|
|
69
|
+
return { ...subscription, deliveredMessageIds: [...deliveredMessageIds] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function matchesBusSubscription(subscription: BusSubscription, options: ListBusSubscriptionsOptions): boolean {
|
|
73
|
+
if (options.busId !== undefined && subscription.busId !== options.busId) return false;
|
|
74
|
+
if (options.subscriberId !== undefined && subscription.subscriberId !== options.subscriberId) return false;
|
|
75
|
+
if (options.subscriberKind !== undefined && subscription.subscriberKind !== options.subscriberKind) return false;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
package/src/core/orchestra.ts
CHANGED
|
@@ -5,23 +5,23 @@ import { createEntityIdentity } from "../utils.ts";
|
|
|
5
5
|
import type { AgentStore } from "./store.ts";
|
|
6
6
|
|
|
7
7
|
export interface OrchestraApi {
|
|
8
|
-
createBus(options
|
|
8
|
+
createBus(options: CreateBusOptions): Bus;
|
|
9
9
|
getBus(id: string): Bus | undefined;
|
|
10
|
-
publishBus(id: string, message: string, from
|
|
10
|
+
publishBus(id: string, message: string, from: string): Promise<PublishedBusMessage>;
|
|
11
11
|
|
|
12
|
-
spawnAgent(profile: AgentProfile, task: string, busId: string, options
|
|
13
|
-
getRun(id: string, options
|
|
14
|
-
listRuns(options
|
|
15
|
-
messageAgent(id: string, message: string, options
|
|
16
|
-
closeAgent(id: string, options
|
|
12
|
+
spawnAgent(profile: AgentProfile, task: string, busId: string, options: SpawnAgentOptions): Promise<AgentRun>;
|
|
13
|
+
getRun(id: string, options: RunLookupOptions): AgentRun | undefined;
|
|
14
|
+
listRuns(options: ListRunsOptions): AgentRun[];
|
|
15
|
+
messageAgent(id: string, message: string, options: RunLookupOptions): Promise<AgentRun>;
|
|
16
|
+
closeAgent(id: string, options: RunLookupOptions): Promise<AgentRun | undefined>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface CreateBusOptions {
|
|
20
|
-
name
|
|
20
|
+
name: string | undefined;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface SpawnAgentOptions {
|
|
24
|
-
name
|
|
24
|
+
name: string | undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface PublishedBusMessage {
|
|
@@ -30,12 +30,12 @@ export interface PublishedBusMessage {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface ListRunsOptions {
|
|
33
|
-
busId
|
|
33
|
+
busId: string | undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface RunLookupOptions {
|
|
37
|
-
/**
|
|
38
|
-
busId
|
|
37
|
+
/** Bus id or name for narrowing run lookup. If undefined, search all runs. */
|
|
38
|
+
busId: string | undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface OrchestraDeps {
|
|
@@ -52,7 +52,7 @@ export class Orchestra implements OrchestraApi {
|
|
|
52
52
|
this.store = store;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
createBus(options: CreateBusOptions
|
|
55
|
+
createBus(options: CreateBusOptions): Bus {
|
|
56
56
|
const identity = this.createBusIdentity(options.name);
|
|
57
57
|
const bus: Bus = { ...identity, messages: [] };
|
|
58
58
|
this.store.saveBus(bus);
|
|
@@ -63,39 +63,34 @@ export class Orchestra implements OrchestraApi {
|
|
|
63
63
|
return this.findBus(id);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
async publishBus(id: string, message: string, from
|
|
66
|
+
async publishBus(id: string, message: string, from: string): Promise<PublishedBusMessage> {
|
|
67
67
|
const bus = this.requireBus(id);
|
|
68
68
|
const busMessage = await this.runtime.publishBus(bus.id, message, from);
|
|
69
69
|
return { bus: this.requireBus(bus.id), busMessage };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
async spawnAgent(
|
|
73
|
-
profile: AgentProfile,
|
|
74
|
-
task: string,
|
|
75
|
-
busId: string,
|
|
76
|
-
options: SpawnAgentOptions = {},
|
|
77
|
-
): Promise<AgentRun> {
|
|
72
|
+
async spawnAgent(profile: AgentProfile, task: string, busId: string, options: SpawnAgentOptions): Promise<AgentRun> {
|
|
78
73
|
const bus = this.requireBus(busId);
|
|
79
74
|
return await this.runtime.spawn(profile, task, bus.id, this.createRunIdentity(profile, options.name));
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
getRun(id: string, options: RunLookupOptions
|
|
77
|
+
getRun(id: string, options: RunLookupOptions): AgentRun | undefined {
|
|
83
78
|
return this.findRun(id, options);
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
listRuns(options: ListRunsOptions
|
|
81
|
+
listRuns(options: ListRunsOptions): AgentRun[] {
|
|
87
82
|
const runs = this.store.listRuns();
|
|
88
83
|
if (!options.busId) return runs;
|
|
89
84
|
const bus = this.requireBus(options.busId);
|
|
90
85
|
return runs.filter((run) => run.busId === bus.id);
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
async messageAgent(id: string, message: string, options: RunLookupOptions
|
|
88
|
+
async messageAgent(id: string, message: string, options: RunLookupOptions): Promise<AgentRun> {
|
|
94
89
|
const run = this.requireRun(id, options);
|
|
95
90
|
return await this.runtime.message(run.id, message);
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
async closeAgent(id: string, options: RunLookupOptions
|
|
93
|
+
async closeAgent(id: string, options: RunLookupOptions): Promise<AgentRun | undefined> {
|
|
99
94
|
const run = this.getRun(id, options);
|
|
100
95
|
return await this.runtime.close(run?.id ?? id);
|
|
101
96
|
}
|
|
@@ -110,13 +105,13 @@ export class Orchestra implements OrchestraApi {
|
|
|
110
105
|
return this.store.getBus(id) ?? this.store.listBuses().find((bus) => bus.name === id);
|
|
111
106
|
}
|
|
112
107
|
|
|
113
|
-
private requireRun(id: string, options: RunLookupOptions
|
|
108
|
+
private requireRun(id: string, options: RunLookupOptions): AgentRun {
|
|
114
109
|
const run = this.findRun(id, options);
|
|
115
110
|
if (!run) throw new Error(`Agent ${id} not found.`);
|
|
116
111
|
return run;
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
private findRun(id: string, options: RunLookupOptions
|
|
114
|
+
private findRun(id: string, options: RunLookupOptions): AgentRun | undefined {
|
|
120
115
|
const bus = options.busId ? this.requireBus(options.busId) : undefined;
|
|
121
116
|
const runById = this.store.getRun(id);
|
|
122
117
|
if (runById && (!bus || runById.busId === bus.id)) return runById;
|
package/src/core/store.ts
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import type { AgentRun } from "./subagent.ts";
|
|
2
|
-
import type { Bus, BusMessage } from "./bus.ts";
|
|
2
|
+
import type { Bus, BusMessage, BusMessageEvent, BusSubscription, ListBusSubscriptionsOptions } from "./bus.ts";
|
|
3
3
|
import type { WorkflowRun } from "./workflow.ts";
|
|
4
4
|
|
|
5
5
|
export interface AgentStore {
|
|
6
6
|
saveRun(run: AgentRun): void;
|
|
7
7
|
getRun(id: string): AgentRun | undefined;
|
|
8
8
|
listRuns(): AgentRun[];
|
|
9
|
-
subscribeRuns(listener: (run: AgentRun) => void, filter
|
|
9
|
+
subscribeRuns(listener: (run: AgentRun) => void, filter: ((run: AgentRun) => boolean) | undefined): () => void;
|
|
10
10
|
|
|
11
11
|
saveBus(bus: Bus): void;
|
|
12
12
|
getBus(id: string): Bus | undefined;
|
|
13
13
|
listBuses(): Bus[];
|
|
14
14
|
/** Add or replace a bus message by id. */
|
|
15
15
|
addBusMessage(busId: string, message: BusMessage): void;
|
|
16
|
+
subscribeBusMessages(
|
|
17
|
+
listener: (event: BusMessageEvent) => void,
|
|
18
|
+
filter: ((event: BusMessageEvent) => boolean) | undefined,
|
|
19
|
+
): () => void;
|
|
20
|
+
|
|
21
|
+
saveBusSubscription(subscription: BusSubscription): void;
|
|
22
|
+
getBusSubscription(id: string): BusSubscription | undefined;
|
|
23
|
+
listBusSubscriptions(options: ListBusSubscriptionsOptions): BusSubscription[];
|
|
24
|
+
deleteBusSubscription(id: string): void;
|
|
16
25
|
|
|
17
26
|
saveWorkflow(workflow: WorkflowRun): void;
|
|
18
27
|
getWorkflow(id: string): WorkflowRun | undefined;
|
|
19
28
|
listWorkflows(): WorkflowRun[];
|
|
20
29
|
subscribeWorkflows(
|
|
21
30
|
listener: (workflow: WorkflowRun) => void,
|
|
22
|
-
filter
|
|
31
|
+
filter: ((workflow: WorkflowRun) => boolean) | undefined,
|
|
23
32
|
): () => void;
|
|
24
33
|
}
|
package/src/core/subagent.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export const AGENT_RESULT_STATUS_VALUES = ["success", "blocked", "failed"] as const;
|
|
2
2
|
export type AgentResultStatus = (typeof AGENT_RESULT_STATUS_VALUES)[number];
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// AgentRun uses running/idle/closed for lifecycle; success/blocked/failed are retained in AgentState
|
|
5
|
+
// because workflows and older run records use the same shared state vocabulary.
|
|
6
|
+
export type AgentState = "idle" | "running" | "closed" | AgentResultStatus;
|
|
5
7
|
|
|
6
8
|
export interface AgentProfile {
|
|
7
9
|
name: string;
|
|
8
10
|
systemPrompt: string;
|
|
9
|
-
tools
|
|
10
|
-
model
|
|
11
|
+
tools: string[];
|
|
12
|
+
model: string | undefined;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface AgentResult {
|
package/src/core/workflow.ts
CHANGED
|
@@ -6,8 +6,8 @@ export interface WorkflowStageSpec {
|
|
|
6
6
|
goal: string;
|
|
7
7
|
strategy: WorkgroupStrategy;
|
|
8
8
|
members: WorkgroupMember[];
|
|
9
|
-
/**
|
|
10
|
-
leader
|
|
9
|
+
/** Synthesizes the stage's worker output; must be specified explicitly. */
|
|
10
|
+
leader: WorkgroupMember;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface WorkflowStageOutput {
|
|
@@ -26,10 +26,11 @@ export interface WorkflowRunResult {
|
|
|
26
26
|
result?: AgentResult;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export interface WorkflowStageRun extends
|
|
30
|
-
leader: WorkgroupMember;
|
|
29
|
+
export interface WorkflowStageRun extends WorkflowStageSpec {
|
|
31
30
|
state: AgentState;
|
|
32
31
|
phase?: "workers" | "leader";
|
|
32
|
+
/** Milliseconds since epoch when this stage started running. */
|
|
33
|
+
startedAtMs: number;
|
|
33
34
|
busId?: string;
|
|
34
35
|
workerRunIds: string[];
|
|
35
36
|
leaderRunId?: string;
|
package/src/core/workgroup.ts
CHANGED
|
@@ -5,8 +5,8 @@ export type WorkgroupStrategy = (typeof WORKGROUP_STRATEGY_VALUES)[number];
|
|
|
5
5
|
|
|
6
6
|
export interface WorkgroupMember {
|
|
7
7
|
profile: AgentProfile;
|
|
8
|
-
/**
|
|
9
|
-
name
|
|
8
|
+
/** Globally unique short run name. If undefined, one is generated from the profile name. */
|
|
9
|
+
name: string | undefined;
|
|
10
10
|
/** Member-specific assignment or focus within the workgroup goal. */
|
|
11
|
-
assignment
|
|
11
|
+
assignment: string | undefined;
|
|
12
12
|
}
|
package/src/extension/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { InMemoryAgentStore } from "../adapters/in-memory-store.ts";
|
|
3
2
|
import { PiAgentRuntime } from "../adapters/pi-runtime.ts";
|
|
3
|
+
import { createProjectSqliteAgentStore } from "../adapters/sqlite-store.ts";
|
|
4
4
|
import { Orchestra } from "../core/orchestra.ts";
|
|
5
5
|
import { createBusTool, defineBusPiTool, type BusTool } from "../tools/bus.ts";
|
|
6
6
|
import { createSubagentTool, defineSubagentPiTool, type SubagentTool } from "../tools/subagent.ts";
|
|
@@ -16,6 +16,7 @@ interface ToolBundle {
|
|
|
16
16
|
workflowTool: WorkflowTool;
|
|
17
17
|
workflowMonitor: WorkflowMonitorController;
|
|
18
18
|
orchestraEvents: OrchestraEventController;
|
|
19
|
+
dispose(): void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export default function piOrchestraExtension(pi: ExtensionAPI): void {
|
|
@@ -42,7 +43,8 @@ export default function piOrchestraExtension(pi: ExtensionAPI): void {
|
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
45
|
-
bundles.get(ctx.cwd)?.
|
|
46
|
+
bundles.get(ctx.cwd)?.dispose();
|
|
47
|
+
bundles.delete(ctx.cwd);
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -50,7 +52,7 @@ function getBundle(pi: ExtensionAPI, bundles: Map<string, ToolBundle>, ctx: Exte
|
|
|
50
52
|
const existing = bundles.get(ctx.cwd);
|
|
51
53
|
if (existing) return existing;
|
|
52
54
|
|
|
53
|
-
const store =
|
|
55
|
+
const store = createProjectSqliteAgentStore(ctx.cwd);
|
|
54
56
|
const runtime = new PiAgentRuntime({
|
|
55
57
|
store,
|
|
56
58
|
cwd: ctx.cwd,
|
|
@@ -60,6 +62,7 @@ function getBundle(pi: ExtensionAPI, bundles: Map<string, ToolBundle>, ctx: Exte
|
|
|
60
62
|
const orchestraEvents = new OrchestraEventController({
|
|
61
63
|
store,
|
|
62
64
|
sendEvents: (events, content) => sendOrchestraEvents(pi, events, content),
|
|
65
|
+
flushDelayMs: undefined,
|
|
63
66
|
});
|
|
64
67
|
const workgroupTool = createWorkgroupTool({
|
|
65
68
|
orchestra,
|
|
@@ -73,12 +76,17 @@ function getBundle(pi: ExtensionAPI, bundles: Map<string, ToolBundle>, ctx: Exte
|
|
|
73
76
|
onWorkgroupLaunchFailed: ({ bus }) => orchestraEvents.cancelWorkgroupLaunch(bus.id),
|
|
74
77
|
});
|
|
75
78
|
const bundle = {
|
|
76
|
-
busTool: createBusTool({ orchestra }),
|
|
79
|
+
busTool: createBusTool({ orchestra, store }),
|
|
77
80
|
subagentTool: createSubagentTool({ orchestra }),
|
|
78
81
|
workgroupTool,
|
|
79
82
|
workflowTool: createWorkflowTool({ orchestra, store }),
|
|
80
|
-
workflowMonitor: new WorkflowMonitorController(store),
|
|
83
|
+
workflowMonitor: new WorkflowMonitorController(store, { now: undefined, tickMs: undefined }),
|
|
81
84
|
orchestraEvents,
|
|
85
|
+
dispose() {
|
|
86
|
+
this.workflowMonitor.dispose();
|
|
87
|
+
this.orchestraEvents.dispose();
|
|
88
|
+
store.dispose();
|
|
89
|
+
},
|
|
82
90
|
};
|
|
83
91
|
bundles.set(ctx.cwd, bundle);
|
|
84
92
|
return bundle;
|