@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 CHANGED
@@ -25,7 +25,7 @@ widget with the current stage and agent completion counts. Use
25
25
 
26
26
  ### Subagent
27
27
 
28
- A subagent is an isolated child agent with its own role, task, optional tool allowlist, and optional model. Subagents attach to a bus so they can receive shared reference context while working independently.
28
+ A subagent is an isolated child agent with its own role, task, explicit tool allowlist, and optional model. Subagents attach to a bus so they can receive shared reference context while working independently.
29
29
 
30
30
  Use subagents when you want to delegate a focused task, such as review, research, implementation planning, or an alternative solution attempt.
31
31
 
@@ -40,12 +40,23 @@ Workgroups support two strategies:
40
40
 
41
41
  ### Workflow
42
42
 
43
- A workflow runs ordered workgroup stages. Each stage gets a fresh bus, starts its workers, collects results through internal finish-event subscriptions, and uses a stage leader to produce a canonical stage output. The main agent receives a single `workflow.finished` event for the whole workflow.
43
+ A workflow runs ordered workgroup stages. Each stage gets a fresh bus, starts its workers, collects results through internal finish-event subscriptions, and uses an evidence synthesizer to produce a canonical stage output. The main agent receives a single `workflow.finished` event for the whole workflow.
44
44
 
45
45
  Use workflows for multi-step plans where later stages should depend on the summarized output of earlier stages instead of raw worker transcripts.
46
46
 
47
+ ## Reusable profiles
48
+
49
+ `src/profiles/` exports reusable `AgentProfile` factories:
50
+
51
+ - `createSourceCodeQaProfile`: answer repository questions from local code, tests, and docs.
52
+ - `createExternalResearcherProfile`: gather and synthesize external source material with citations and uncertainty handling.
53
+ - `createCodeReviewerProfile`: review local code or changes with findings-first output.
54
+ - `createEvidenceSynthesizerProfile`: synthesize supplied evidence and context, using tools only for targeted verification or gap-filling.
55
+
56
+ Profile factories require an options object with an explicit `tools` allowlist. The main agent should inject the installed/active tool names each child actually needs. Pass `undefined` for `name` or `model` to use the factory default.
57
+
47
58
  ## Notes
48
59
 
49
60
  - Create a `bus` before spawning related subagents or workgroups.
50
- - Subagents report completion with `success`, `blocked`, or `failed`; completions are surfaced as pi-orchestra events.
61
+ - Subagents report completion results with `success`, `blocked`, or `failed`; after `finish`, reusable runs return to `idle` until messaged or closed.
51
62
  - Use `workflow` for linear staged work, not branching/DAG execution.
@@ -3,7 +3,7 @@
3
3
  pi-orchestra builds delegation in four layers:
4
4
 
5
5
  1. **Bus** — shared context channel.
6
- 2. **Subagent** — isolated child agent attached to a bus.
6
+ 2. **Subagent** — isolated child agent subscribed to a bus.
7
7
  3. **Workgroup** — multiple subagents working on one bus.
8
8
  4. **Workflow** — ordered workgroup stages with stage synthesis.
9
9
 
@@ -14,8 +14,9 @@ ordered `messages`.
14
14
 
15
15
  Bus messages are peer reference context only:
16
16
 
17
- - `publish_bus` sends useful findings to sibling agents on the same bus.
18
- - Bus context is injected as supplemental `<bus_reference_context>`.
17
+ - `publish_bus` sends useful findings to subscribers of the same bus.
18
+ - Subagents subscribe to their assigned bus when spawned.
19
+ - Subscribed bus context is delivered as supplemental `<bus_reference_context>`.
19
20
  - Decisions and escalation do not go through the bus; call the `finish` tool with
20
21
  `status: "blocked"`.
21
22
 
@@ -24,8 +25,10 @@ Bus messages are peer reference context only:
24
25
  A subagent is an `AgentRun`: a child agent with a profile, task, bus id, state,
25
26
  and optional result.
26
27
 
27
- Profiles define the child agent's `systemPrompt`, optional tool allowlist, and
28
- optional model. The runtime attaches each subagent to exactly one bus.
28
+ Profiles define the child agent's `systemPrompt`, explicit tool allowlist, and
29
+ optional model. The runtime creates a bus subscription for each spawned subagent;
30
+ `AgentRun.busId` remains the lifecycle/query scope, not the context delivery
31
+ path.
29
32
 
30
33
  Every subagent must call the `finish` tool with:
31
34
 
@@ -33,8 +36,10 @@ Every subagent must call the `finish` tool with:
33
36
  - `summary`: concise handoff text
34
37
  - `data`: optional structured output
35
38
 
36
- State follows the result status. `closed` is separate and means the run has been
37
- disposed.
39
+ While a subagent is working, its state is `running`. Calling `finish` records the
40
+ result status and returns the reusable run to `idle`, so the leader can message it
41
+ again without recreating the session. `closed` is separate and means the run has
42
+ been disposed.
38
43
 
39
44
  ## Workgroup
40
45
 
@@ -56,18 +61,18 @@ Main receives finish events instead of blocking on completion calls:
56
61
  ## Workflow
57
62
 
58
63
  A workflow runs ordered stages. Each stage defines a goal, strategy, workers,
59
- and optional leader.
64
+ and a leader.
60
65
 
61
66
  For each stage:
62
67
 
63
68
  1. Create a fresh bus.
64
- 2. Spawn the worker workgroup.
69
+ 2. Spawn the worker workgroup; workers subscribe to the stage bus.
65
70
  3. Collect worker results through store finish-event subscriptions in the background.
66
- 4. Spawn a restricted stage leader.
71
+ 4. Spawn the stage leader to synthesize the worker results.
67
72
  5. Store the leader's canonical output as the stage output.
68
73
 
69
74
  Workflow-internal worker and leader completions are consumed by the workflow runner. Main receives a single `workflow.finished` event when the whole workflow reaches `success`, `blocked`, `failed`, or `closed`.
70
75
 
71
76
  The next stage receives the previous stage output, not raw worker transcripts.
72
- If no leader is provided, `createStageLeaderProfile` supplies a restricted
73
- leader with no tools.
77
+ Each stage specifies its leader explicitly the leader synthesizes the workers'
78
+ results into that canonical stage output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@g3un/pi-orchestra",
3
- "version": "0.2.1",
3
+ "version": "0.9.2",
4
4
  "description": "Subagent orchestration tools for Pi.",
5
5
  "keywords": [
6
6
  "orchestration",
@@ -22,6 +22,7 @@
22
22
  "src/**/*.ts",
23
23
  "!src/**/*.test.ts",
24
24
  "docs/**/*.md",
25
+ "skills/**/*",
25
26
  "README.md",
26
27
  "LICENSE"
27
28
  ],
@@ -61,6 +62,9 @@
61
62
  "pi": {
62
63
  "extensions": [
63
64
  "./src/extension/index.ts"
65
+ ],
66
+ "skills": [
67
+ "./skills/pi-orchestra"
64
68
  ]
65
69
  }
66
70
  }
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: pi-orchestra
3
+ description: "Use when delegating work with Pi-Orchestra tools: bus, subagent, workgroup, or workflow. Helps choose the right orchestration primitive, create buses, brief child agents, use compete vs synthesize strategies, react to completion events, and avoid polling or over-delegation."
4
+ ---
5
+
6
+ # Pi-Orchestra
7
+
8
+ Use Pi-Orchestra to parallelize or structure work without losing the main thread. Do not delegate trivial tasks that you can finish faster yourself.
9
+
10
+ ## Tool choice
11
+
12
+ - Use `subagent` for one focused, isolated task such as review, research, planning, or an independent implementation attempt.
13
+ - Use `workgroup` when multiple agents should start together on the same goal.
14
+ - `compete`: agents try alternative solutions; one successful result may be enough.
15
+ - `synthesize`: agents cover complementary angles; collect and combine all useful findings.
16
+ - Use `workflow` for ordered, linear stages where later stages should consume canonical outputs from earlier stages.
17
+ - Use `bus` as the shared context scope for related subagents/workgroups. Buses are reference context, not a blocking queue or decision channel.
18
+
19
+ ## Default workflow
20
+
21
+ 1. Decide the smallest useful delegation unit and expected final output.
22
+ 2. Create one named bus per delegated work item before `subagent` or `workgroup`.
23
+ 3. Put stable shared context in the initial task/goal. Use `bus action=publish` only for new facts, constraints, artifacts, blockers, or course corrections useful to attached agents.
24
+ 4. Give every child agent a specific profile, assignment, success criteria, handoff shape, and explicit tool allowlist.
25
+ 5. Continue main-thread work while waiting. Pi-Orchestra completion events arrive automatically.
26
+ 6. On completion:
27
+ - For standalone subagents, consume the `subagent.finished` summary/data.
28
+ - For `workgroup compete`, use the first clearly successful result when sufficient; close pending losers if extra work is wasteful.
29
+ - For `workgroup synthesize`, combine complementary results and note gaps, conflicts, and confidence.
30
+ - For `workflow`, wait for `workflow.finished` or use `workflow status` only when you need progress.
31
+
32
+ ## Briefing child agents
33
+
34
+ Prefer concise, outcome-oriented tasks:
35
+
36
+ ```text
37
+ Role: <specialist role>
38
+ Objective: <specific result needed>
39
+ Context: <files, commands, constraints, prior findings>
40
+ Do: <steps or focus areas>
41
+ Do not: <boundaries, destructive actions, scope exclusions>
42
+ Finish with: status success/blocked/failed, a concise summary, evidence, risks, and structured data if useful.
43
+ ```
44
+
45
+ Profile defaults:
46
+
47
+ - `name`: short role name, e.g. `reviewer`, `planner`, `doc-researcher`.
48
+ - `systemPrompt`: one paragraph describing expertise, constraints, and output discipline.
49
+ - `tools`: always inject an explicit allowlist from the tools available to the main agent. Include only tools the child needs, including installed extension tool names for research/browser work. Use `[]` for supplied-context-only roles.
50
+ - `model`: omit unless the task needs a specific provider/model.
51
+
52
+ ## Patterns
53
+
54
+ ### Single specialist
55
+
56
+ 1. `bus create` with a short name.
57
+ 2. `subagent spawn` with the bus id/name, specialist profile, and focused task.
58
+ 3. Incorporate the finish event. Use `subagent message` only for meaningful new guidance; avoid micromanagement.
59
+
60
+ ### Alternative solution race
61
+
62
+ 1. `bus create`.
63
+ 2. `workgroup` with `strategy: "compete"` and 2-4 members with distinct approaches.
64
+ 3. When a strong success arrives, close remaining active runs unless their outputs are still valuable.
65
+
66
+ ### Multi-angle review
67
+
68
+ 1. `bus create`.
69
+ 2. `workgroup` with `strategy: "synthesize"` and members for complementary lenses, e.g. correctness, tests, UX/docs, risk/security.
70
+ 3. Synthesize results yourself; call out disagreements and unresolved blockers.
71
+
72
+ ### Linear pipeline
73
+
74
+ Use `workflow action=start` when there are explicit stages such as discover → design → implement → review. Keep stages linear; do not model branching/DAG work as a workflow.
75
+
76
+ Every stage requires an explicit `leader` that condenses its workers' output into the canonical stage result fed to the next stage. Prefer the `evidence-synthesizer` profile for this role unless a stage needs a specialized synthesizer; give the leader an explicit tool allowlist (often the union of its workers' tools) so it can verify evidence or fill concrete gaps without broadening scope.
77
+
78
+ ## Gotchas
79
+
80
+ - Always create a bus before spawning related agents.
81
+ - Reuse the same bus only for agents working on the same delegated work item.
82
+ - Do not wait on or poll buses; use `bus status` only to inspect shared messages.
83
+ - Do not rely on bus messages for leader-only decisions or urgent escalation; child agents should finish with `blocked` for that.
84
+ - Keep child context bounded. Publish summaries and artifact paths, not long transcripts.
85
+ - Prefer fewer, better-briefed agents over many vague agents.
86
+
87
+ ## Final response checklist
88
+
89
+ - State which orchestration primitive was used and why, if relevant.
90
+ - Include the winning/synthesized answer, not raw child transcripts.
91
+ - Mention important blockers, risks, and follow-up actions.
92
+ - Close or cancel unnecessary active runs/workflows when the task is done.
@@ -1,18 +1,27 @@
1
1
  import type { AgentRun } from "../core/subagent.ts";
2
- import type { Bus, BusMessage } from "../core/bus.ts";
2
+ import {
3
+ matchesBusSubscription,
4
+ type Bus,
5
+ type BusMessage,
6
+ type BusMessageEvent,
7
+ type BusSubscription,
8
+ type ListBusSubscriptionsOptions,
9
+ } from "../core/bus.ts";
3
10
  import type { AgentStore } from "../core/store.ts";
4
11
  import type { WorkflowRun } from "../core/workflow.ts";
12
+ import { notifySubscribers, subscribeStore, type StoreSubscription } from "./store-subscriptions.ts";
5
13
 
6
- interface StoreSubscription<T> {
7
- listener(value: T): void;
8
- filter?: (value: T) => boolean;
9
- }
10
-
14
+ /**
15
+ * In-memory {@link AgentStore} used as a lightweight fixture in tests.
16
+ * Production code persists state through {@link SqliteAgentStore} instead.
17
+ */
11
18
  export class InMemoryAgentStore implements AgentStore {
12
19
  private readonly runs = new Map<string, AgentRun>();
13
20
  private readonly buses = new Map<string, Bus>();
21
+ private readonly busSubscriptionsById = new Map<string, BusSubscription>();
14
22
  private readonly workflows = new Map<string, WorkflowRun>();
15
23
  private readonly runSubscriptions = new Set<StoreSubscription<AgentRun>>();
24
+ private readonly busMessageSubscriptions = new Set<StoreSubscription<BusMessageEvent>>();
16
25
  private readonly workflowSubscriptions = new Set<StoreSubscription<WorkflowRun>>();
17
26
 
18
27
  saveRun(run: AgentRun): void {
@@ -28,10 +37,8 @@ export class InMemoryAgentStore implements AgentStore {
28
37
  return [...this.runs.values()];
29
38
  }
30
39
 
31
- subscribeRuns(listener: (run: AgentRun) => void, filter?: (run: AgentRun) => boolean): () => void {
32
- const subscription = { listener, filter };
33
- this.runSubscriptions.add(subscription);
34
- return () => this.runSubscriptions.delete(subscription);
40
+ subscribeRuns(listener: (run: AgentRun) => void, filter: ((run: AgentRun) => boolean) | undefined): () => void {
41
+ return subscribeStore(this.runSubscriptions, listener, filter);
35
42
  }
36
43
 
37
44
  saveBus(bus: Bus): void {
@@ -57,6 +64,32 @@ export class InMemoryAgentStore implements AgentStore {
57
64
  }
58
65
 
59
66
  bus.messages.push(message);
67
+ notifySubscribers(this.busMessageSubscriptions, { busId, message });
68
+ }
69
+
70
+ subscribeBusMessages(
71
+ listener: (event: BusMessageEvent) => void,
72
+ filter: ((event: BusMessageEvent) => boolean) | undefined,
73
+ ): () => void {
74
+ return subscribeStore(this.busMessageSubscriptions, listener, filter);
75
+ }
76
+
77
+ saveBusSubscription(subscription: BusSubscription): void {
78
+ this.busSubscriptionsById.set(subscription.id, subscription);
79
+ }
80
+
81
+ getBusSubscription(id: string): BusSubscription | undefined {
82
+ return this.busSubscriptionsById.get(id);
83
+ }
84
+
85
+ listBusSubscriptions(options: ListBusSubscriptionsOptions): BusSubscription[] {
86
+ return [...this.busSubscriptionsById.values()].filter((subscription) =>
87
+ matchesBusSubscription(subscription, options),
88
+ );
89
+ }
90
+
91
+ deleteBusSubscription(id: string): void {
92
+ this.busSubscriptionsById.delete(id);
60
93
  }
61
94
 
62
95
  saveWorkflow(workflow: WorkflowRun): void {
@@ -74,16 +107,8 @@ export class InMemoryAgentStore implements AgentStore {
74
107
 
75
108
  subscribeWorkflows(
76
109
  listener: (workflow: WorkflowRun) => void,
77
- filter?: (workflow: WorkflowRun) => boolean,
110
+ filter: ((workflow: WorkflowRun) => boolean) | undefined,
78
111
  ): () => void {
79
- const subscription = { listener, filter };
80
- this.workflowSubscriptions.add(subscription);
81
- return () => this.workflowSubscriptions.delete(subscription);
82
- }
83
- }
84
-
85
- function notifySubscribers<T>(subscriptions: Set<StoreSubscription<T>>, value: T): void {
86
- for (const subscription of subscriptions) {
87
- if (!subscription.filter || subscription.filter(value)) subscription.listener(value);
112
+ return subscribeStore(this.workflowSubscriptions, listener, filter);
88
113
  }
89
114
  }
@@ -1,2 +1,3 @@
1
1
  export * from "./in-memory-store.ts";
2
2
  export * from "./pi-runtime.ts";
3
+ export * from "./sqlite-store.ts";
@@ -14,20 +14,26 @@ import {
14
14
  type AgentResultStatus,
15
15
  type AgentRun,
16
16
  } from "../core/subagent.ts";
17
- import type { Bus, BusMessage } from "../core/bus.ts";
17
+ import {
18
+ createBusSubscription,
19
+ isBusMessageDelivered,
20
+ markBusMessagesDelivered,
21
+ type Bus,
22
+ type BusMessage,
23
+ type BusSubscription,
24
+ } from "../core/bus.ts";
18
25
  import { formatBusMessages } from "../core/bus-format.ts";
19
26
  import type { AgentRuntime, SpawnAgentRuntimeOptions } from "../core/runtime.ts";
20
27
  import type { AgentStore } from "../core/store.ts";
21
28
 
22
29
  export interface PiAgentRuntimeOptions {
23
30
  store: AgentStore;
24
- cwd?: string;
25
- resolveModel?: (model: string) => Model<any> | Promise<Model<any> | undefined> | undefined;
31
+ cwd: string | undefined;
32
+ resolveModel: ((model: string) => Model<any> | Promise<Model<any> | undefined> | undefined) | undefined;
26
33
  }
27
34
 
28
35
  interface RuntimeEntry {
29
36
  session: AgentSession;
30
- seenBusMessageIds: Set<string>;
31
37
  promptTask?: Promise<void>;
32
38
  }
33
39
 
@@ -41,13 +47,11 @@ const PublishBusParams = Type.Object({
41
47
  message: Type.String(),
42
48
  });
43
49
 
44
- const DEFAULT_AGENT_TOOLS = ["read", "bash", "edit", "write"];
45
-
46
50
  export class PiAgentRuntime implements AgentRuntime {
47
51
  private readonly entries = new Map<string, RuntimeEntry>();
48
52
  private readonly store: AgentStore;
49
53
  private readonly cwd: string;
50
- private readonly resolveModel?: PiAgentRuntimeOptions["resolveModel"];
54
+ private readonly resolveModel: PiAgentRuntimeOptions["resolveModel"];
51
55
 
52
56
  constructor(options: PiAgentRuntimeOptions) {
53
57
  this.store = options.store;
@@ -69,12 +73,12 @@ export class PiAgentRuntime implements AgentRuntime {
69
73
  profile: profile.name,
70
74
  task,
71
75
  busId,
72
- state: "idle",
76
+ state: "running",
73
77
  };
74
78
 
75
79
  const childTools = this.createChildTools(run.id);
76
80
  const model = await this.resolveProfileModel(profile);
77
- const baseTools = profile.tools ?? DEFAULT_AGENT_TOOLS;
81
+ const baseTools = requireProfileTools(profile);
78
82
  const activeTools = [...new Set([...baseTools, ...childTools.map((tool) => tool.name)])];
79
83
  const { session } = await createAgentSession({
80
84
  cwd: this.cwd,
@@ -85,12 +89,13 @@ export class PiAgentRuntime implements AgentRuntime {
85
89
  });
86
90
 
87
91
  this.store.saveRun(run);
88
- const entry: RuntimeEntry = { session, seenBusMessageIds: new Set() };
92
+ this.store.saveBusSubscription(createAgentBusSubscription(run.id, busId));
93
+ const entry: RuntimeEntry = { session };
89
94
  this.entries.set(run.id, entry);
90
95
  this.startPromptTask(
91
96
  run.id,
92
97
  entry,
93
- this.withBusMessages(run.id, entry, buildInitialPrompt(profile, task, run.name)),
98
+ this.withSubscribedBusMessages(run.id, buildInitialPrompt(profile, task, run.name)),
94
99
  );
95
100
  return run;
96
101
  }
@@ -99,15 +104,15 @@ export class PiAgentRuntime implements AgentRuntime {
99
104
  const entry = this.requireEntry(id);
100
105
  const run = this.requireRun(id);
101
106
  this.assertOpenRun(run);
102
- const messageWithBusContext = this.withBusMessages(id, entry, message);
107
+ const messageWithBusContext = this.withSubscribedBusMessages(id, message);
103
108
 
104
- if (run.state === "idle" && entry.session.isStreaming) {
109
+ if (run.state === "running" && entry.session.isStreaming) {
105
110
  await entry.session.steer(messageWithBusContext);
106
111
  return run;
107
112
  }
108
113
 
109
- const messagedRun: AgentRun = run.state === "idle" ? run : { ...run, state: "idle", result: undefined };
110
- if (messagedRun !== run) this.store.saveRun(messagedRun);
114
+ const messagedRun: AgentRun = { ...run, state: "running", result: undefined };
115
+ this.store.saveRun(messagedRun);
111
116
  this.startPromptTask(id, entry, messageWithBusContext);
112
117
  return messagedRun;
113
118
  }
@@ -122,15 +127,24 @@ export class PiAgentRuntime implements AgentRuntime {
122
127
 
123
128
  this.store.addBusMessage(busId, busMessage);
124
129
 
125
- const steeringMessage = formatBusMessages([busMessage]);
130
+ const steeringMessage = this.formatBusMessagesForPrompt([busMessage]);
126
131
  const steerTasks: Array<Promise<void>> = [];
127
- for (const [runId, entry] of this.entries) {
128
- const run = this.store.getRun(runId);
129
- if (!run || run.busId !== busId) continue;
130
- if (run.id === from || run.state === "closed" || !entry.session.isStreaming) continue;
131
-
132
- entry.seenBusMessageIds.add(busMessage.id);
133
- steerTasks.push(entry.session.steer(steeringMessage));
132
+ for (const subscription of this.store.listBusSubscriptions({
133
+ busId,
134
+ subscriberId: undefined,
135
+ subscriberKind: "agent",
136
+ })) {
137
+ if (subscription.subscriberId === from || isBusMessageDelivered(subscription, busMessage.id)) continue;
138
+
139
+ const run = this.store.getRun(subscription.subscriberId);
140
+ const entry = this.entries.get(subscription.subscriberId);
141
+ if (!run || !entry || run.state !== "running" || !entry.session.isStreaming) continue;
142
+
143
+ steerTasks.push(
144
+ entry.session
145
+ .steer(steeringMessage)
146
+ .then(() => this.markSubscriptionMessagesDelivered(subscription, busMessage)),
147
+ );
134
148
  }
135
149
 
136
150
  await Promise.all(steerTasks);
@@ -149,6 +163,7 @@ export class PiAgentRuntime implements AgentRuntime {
149
163
 
150
164
  const closedRun: AgentRun = { ...run, state: "closed" };
151
165
  this.store.saveRun(closedRun);
166
+ this.deleteAgentBusSubscriptions(id);
152
167
  entry?.session.dispose();
153
168
  return closedRun;
154
169
  }
@@ -167,15 +182,15 @@ export class PiAgentRuntime implements AgentRuntime {
167
182
  try {
168
183
  await entry.session.prompt(message, { expandPromptTemplates: false });
169
184
  if (this.isClosed(id)) return;
170
- if (this.store.getRun(id)?.state === "idle") {
185
+ if (this.isRunningWithoutResult(id)) {
171
186
  await entry.session.prompt(buildFinishRequiredPrompt(), { expandPromptTemplates: false });
172
187
  }
173
188
  if (this.isClosed(id)) return;
174
189
  const run = this.store.getRun(id);
175
- if (run?.state === "idle") {
190
+ if (run && isRunningWithoutResult(run)) {
176
191
  this.store.saveRun({
177
192
  ...run,
178
- state: "failed",
193
+ state: "idle",
179
194
  result: {
180
195
  status: "failed",
181
196
  summary: "Agent stopped without calling finish.",
@@ -189,7 +204,7 @@ export class PiAgentRuntime implements AgentRuntime {
189
204
  if (!run) return;
190
205
  this.store.saveRun({
191
206
  ...run,
192
- state: "failed",
207
+ state: "idle",
193
208
  result: {
194
209
  status: "failed",
195
210
  summary: error instanceof Error ? error.message : String(error),
@@ -216,7 +231,7 @@ export class PiAgentRuntime implements AgentRuntime {
216
231
  this.store.saveRun({
217
232
  ...run,
218
233
  result,
219
- state: result.status,
234
+ state: "idle",
220
235
  });
221
236
  return {
222
237
  content: [
@@ -276,30 +291,73 @@ export class PiAgentRuntime implements AgentRuntime {
276
291
  return this.store.getRun(id)?.state === "closed";
277
292
  }
278
293
 
294
+ private isRunningWithoutResult(id: string): boolean {
295
+ const run = this.store.getRun(id);
296
+ return run !== undefined && isRunningWithoutResult(run);
297
+ }
298
+
279
299
  private requireBus(id: string): Bus {
280
300
  const bus = this.store.getBus(id);
281
301
  if (!bus) throw new Error(`Bus ${id} not found.`);
282
302
  return bus;
283
303
  }
284
304
 
285
- private withBusMessages(runId: string, entry: RuntimeEntry, message: string): string {
286
- const busMessages = this.drainBusMessages(runId, entry);
305
+ private withSubscribedBusMessages(runId: string, message: string): string {
306
+ const busMessages = this.drainSubscribedBusMessages(runId);
287
307
  if (busMessages.length === 0) return message;
288
- return [message, "", formatBusMessages(busMessages)].join("\n");
308
+ return [message, "", this.formatBusMessagesForPrompt(busMessages)].join("\n");
289
309
  }
290
310
 
291
- private drainBusMessages(runId: string, entry: RuntimeEntry): BusMessage[] {
292
- const run = this.requireRun(runId);
293
- const bus = this.requireBus(run.busId);
294
- const unreadMessages = bus.messages.filter((message) => {
295
- if (message.from === run.id) return false;
296
- return !entry.seenBusMessageIds.has(message.id);
311
+ private drainSubscribedBusMessages(runId: string): BusMessage[] {
312
+ const subscriptions = this.store.listBusSubscriptions({
313
+ busId: undefined,
314
+ subscriberId: runId,
315
+ subscriberKind: "agent",
297
316
  });
317
+ const unreadMessages: BusMessage[] = [];
318
+ for (const subscription of subscriptions) {
319
+ const bus = this.store.getBus(subscription.busId);
320
+ if (!bus) {
321
+ this.store.deleteBusSubscription(subscription.id);
322
+ continue;
323
+ }
324
+
325
+ const subscriptionUnreadMessages = bus.messages.filter((message) => {
326
+ if (message.from === runId) return false;
327
+ return !isBusMessageDelivered(subscription, message.id);
328
+ });
329
+ if (subscriptionUnreadMessages.length === 0) continue;
298
330
 
299
- for (const message of unreadMessages) entry.seenBusMessageIds.add(message.id);
331
+ unreadMessages.push(...subscriptionUnreadMessages);
332
+ this.markSubscriptionMessagesDelivered(subscription, subscriptionUnreadMessages);
333
+ }
300
334
  return unreadMessages;
301
335
  }
302
336
 
337
+ private markSubscriptionMessagesDelivered(subscription: BusSubscription, messages: BusMessage | BusMessage[]): void {
338
+ const latestSubscription = this.store.getBusSubscription(subscription.id) ?? subscription;
339
+ this.store.saveBusSubscription(markBusMessagesDelivered(latestSubscription, messages));
340
+ }
341
+
342
+ private formatBusMessagesForPrompt(messages: BusMessage[]): string {
343
+ return formatBusMessages(messages, { formatFrom: (from) => this.formatBusMessageFrom(from) });
344
+ }
345
+
346
+ private formatBusMessageFrom(from: string): string {
347
+ if (from === "main") return from;
348
+ return this.store.getRun(from)?.name ?? from;
349
+ }
350
+
351
+ private deleteAgentBusSubscriptions(runId: string): void {
352
+ for (const subscription of this.store.listBusSubscriptions({
353
+ busId: undefined,
354
+ subscriberId: runId,
355
+ subscriberKind: "agent",
356
+ })) {
357
+ this.store.deleteBusSubscription(subscription.id);
358
+ }
359
+ }
360
+
303
361
  private async resolveProfileModel(profile: AgentProfile): Promise<Model<any> | undefined> {
304
362
  if (!profile.model) return undefined;
305
363
  if (!this.resolveModel) throw new Error(`No model resolver configured for profile model "${profile.model}".`);
@@ -326,6 +384,20 @@ function buildInitialPrompt(profile: AgentProfile, task: string, runName: string
326
384
  ].join("\n");
327
385
  }
328
386
 
387
+ function requireProfileTools(profile: AgentProfile): string[] {
388
+ if (!Array.isArray(profile.tools)) throw new Error(`Profile "${profile.name}" must specify tools.`);
389
+ return profile.tools;
390
+ }
391
+
392
+ function createAgentBusSubscription(runId: string, busId: string): BusSubscription {
393
+ return createBusSubscription({
394
+ busId,
395
+ subscriberId: runId,
396
+ subscriberKind: "agent",
397
+ deliveredMessageIds: [],
398
+ });
399
+ }
400
+
329
401
  function buildFinishRequiredPrompt(): string {
330
402
  return [
331
403
  "Your previous response ended without finish.",
@@ -333,6 +405,10 @@ function buildFinishRequiredPrompt(): string {
333
405
  ].join("\n");
334
406
  }
335
407
 
408
+ function isRunningWithoutResult(run: AgentRun): boolean {
409
+ return run.state === "running" && run.result === undefined;
410
+ }
411
+
336
412
  function getLastAssistantText(session: AgentSession): string | undefined {
337
413
  for (let i = session.messages.length - 1; i >= 0; i--) {
338
414
  const message = session.messages[i];