@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
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,
|
|
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
|
|
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`;
|
|
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
|
|
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
|
|
18
|
-
-
|
|
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`,
|
|
28
|
-
optional model. The runtime
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
32
|
-
|
|
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
|
|
110
|
+
filter: ((workflow: WorkflowRun) => boolean) | undefined,
|
|
78
111
|
): () => void {
|
|
79
|
-
|
|
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
|
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -14,20 +14,26 @@ import {
|
|
|
14
14
|
type AgentResultStatus,
|
|
15
15
|
type AgentRun,
|
|
16
16
|
} from "../core/subagent.ts";
|
|
17
|
-
import
|
|
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
|
|
25
|
-
resolveModel
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
107
|
+
const messageWithBusContext = this.withSubscribedBusMessages(id, message);
|
|
103
108
|
|
|
104
|
-
if (run.state === "
|
|
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 =
|
|
110
|
-
|
|
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 =
|
|
130
|
+
const steeringMessage = this.formatBusMessagesForPrompt([busMessage]);
|
|
126
131
|
const steerTasks: Array<Promise<void>> = [];
|
|
127
|
-
for (const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
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
|
|
190
|
+
if (run && isRunningWithoutResult(run)) {
|
|
176
191
|
this.store.saveRun({
|
|
177
192
|
...run,
|
|
178
|
-
state: "
|
|
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: "
|
|
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:
|
|
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
|
|
286
|
-
const busMessages = this.
|
|
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, "",
|
|
308
|
+
return [message, "", this.formatBusMessagesForPrompt(busMessages)].join("\n");
|
|
289
309
|
}
|
|
290
310
|
|
|
291
|
-
private
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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];
|