@g3un/pi-orchestra 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/docs/orchestration-model.md +69 -0
- package/package.json +56 -0
- package/src/adapters/in-memory-store.ts +85 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/pi-runtime.ts +348 -0
- package/src/core/bus-format.ts +14 -0
- package/src/core/bus.ts +11 -0
- package/src/core/index.ts +8 -0
- package/src/core/orchestra.ts +322 -0
- package/src/core/runtime.ts +14 -0
- package/src/core/store.ts +21 -0
- package/src/core/subagent.ts +27 -0
- package/src/core/workflow.ts +49 -0
- package/src/core/workgroup.ts +12 -0
- package/src/extension/index.ts +58 -0
- package/src/index.ts +4 -0
- package/src/profiles/index.ts +1 -0
- package/src/profiles/stage-leader.ts +20 -0
- package/src/tools/bus.ts +275 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/subagent.ts +243 -0
- package/src/tools/workflow.ts +712 -0
- package/src/tools/workgroup.ts +422 -0
- package/src/utils.ts +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Changeun Park
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Pi-Orchestra
|
|
2
|
+
|
|
3
|
+
Subagent orchestration tools for Pi.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@g3un/pi-orchestra
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For a one-off run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi -e npm:@g3un/pi-orchestra
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Pi-Orchestra registers four tools: `bus`, `subagent`, `workgroup`, and `workflow`.
|
|
18
|
+
|
|
19
|
+
## Core concepts
|
|
20
|
+
|
|
21
|
+
### Subagent
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Use subagents when you want to delegate a focused task, such as review, research, implementation planning, or an alternative solution attempt.
|
|
26
|
+
|
|
27
|
+
### Workgroup
|
|
28
|
+
|
|
29
|
+
A workgroup starts multiple subagents on the same bus for one shared goal. Each member can have a different profile or assignment.
|
|
30
|
+
|
|
31
|
+
Workgroups support two strategies:
|
|
32
|
+
|
|
33
|
+
- `compete`: several agents attempt the same goal; one successful result can be enough.
|
|
34
|
+
- `synthesize`: agents work on complementary parts; their findings are collected and combined.
|
|
35
|
+
|
|
36
|
+
### Workflow
|
|
37
|
+
|
|
38
|
+
A workflow runs ordered workgroup stages. Each stage gets a fresh bus, starts its workers, collects results, and uses a stage leader to produce a canonical stage output.
|
|
39
|
+
|
|
40
|
+
Use workflows for multi-step plans where later stages should depend on the summarized output of earlier stages instead of raw worker transcripts.
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
- Create a `bus` before spawning related subagents or workgroups.
|
|
45
|
+
- Subagents report completion with `success`, `blocked`, or `failed`.
|
|
46
|
+
- Use `workflow` for linear staged work, not branching/DAG execution.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Orchestration Model
|
|
2
|
+
|
|
3
|
+
pi-orchestra builds delegation in four layers:
|
|
4
|
+
|
|
5
|
+
1. **Bus** — shared context channel.
|
|
6
|
+
2. **Subagent** — isolated child agent attached to a bus.
|
|
7
|
+
3. **Workgroup** — multiple subagents working on one bus.
|
|
8
|
+
4. **Workflow** — ordered workgroup stages with stage synthesis.
|
|
9
|
+
|
|
10
|
+
## Bus
|
|
11
|
+
|
|
12
|
+
A `Bus` is the coordination scope for related work. It has an `id`, `name`, and
|
|
13
|
+
ordered `messages`.
|
|
14
|
+
|
|
15
|
+
Bus messages are peer reference context only:
|
|
16
|
+
|
|
17
|
+
- `publish_bus` sends useful findings to sibling agents on the same bus.
|
|
18
|
+
- Bus context is injected as supplemental `<bus_reference_context>`.
|
|
19
|
+
- Decisions and escalation do not go through the bus; call the `finish` tool with
|
|
20
|
+
`status: "blocked"`.
|
|
21
|
+
|
|
22
|
+
## Subagent
|
|
23
|
+
|
|
24
|
+
A subagent is an `AgentRun`: a child agent with a profile, task, bus id, state,
|
|
25
|
+
and optional result.
|
|
26
|
+
|
|
27
|
+
Profiles define the child agent's `systemPrompt`, optional tool allowlist, and
|
|
28
|
+
optional model. The runtime attaches each subagent to exactly one bus.
|
|
29
|
+
|
|
30
|
+
Every subagent must call the `finish` tool with:
|
|
31
|
+
|
|
32
|
+
- `status`: `success`, `blocked`, or `failed`
|
|
33
|
+
- `summary`: concise handoff text
|
|
34
|
+
- `data`: optional structured output
|
|
35
|
+
|
|
36
|
+
State follows the result status. `closed` is separate and means the run has been
|
|
37
|
+
disposed.
|
|
38
|
+
|
|
39
|
+
## Workgroup
|
|
40
|
+
|
|
41
|
+
A workgroup is a set of subagents spawned on the same bus for one shared goal.
|
|
42
|
+
Each member has a profile and may add a member-specific assignment.
|
|
43
|
+
|
|
44
|
+
Strategies:
|
|
45
|
+
|
|
46
|
+
- `compete`: one successful member can be enough.
|
|
47
|
+
- `synthesize`: collect complementary findings and combine them.
|
|
48
|
+
|
|
49
|
+
Leaders collect results with bus wait actions:
|
|
50
|
+
|
|
51
|
+
- `wait_next`: handle terminal runs as they finish.
|
|
52
|
+
- `wait_settled`: wait until every attached run is terminal.
|
|
53
|
+
|
|
54
|
+
## Workflow
|
|
55
|
+
|
|
56
|
+
A workflow runs ordered stages. Each stage defines a goal, strategy, workers,
|
|
57
|
+
and optional leader.
|
|
58
|
+
|
|
59
|
+
For each stage:
|
|
60
|
+
|
|
61
|
+
1. Create a fresh bus.
|
|
62
|
+
2. Spawn the worker workgroup.
|
|
63
|
+
3. Collect worker results.
|
|
64
|
+
4. Spawn a restricted stage leader.
|
|
65
|
+
5. Store the leader's canonical output as the stage output.
|
|
66
|
+
|
|
67
|
+
The next stage receives the previous stage output, not raw worker transcripts.
|
|
68
|
+
If no leader is provided, `createStageLeaderProfile` supplies a restricted
|
|
69
|
+
leader with no tools.
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@g3un/pi-orchestra",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Subagent orchestration tools for Pi.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"orchestration",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"subagent"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Changeun Park <g3un@protonmail.ch>",
|
|
14
|
+
"files": [
|
|
15
|
+
"src/**/*.ts",
|
|
16
|
+
"!src/**/*.test.ts",
|
|
17
|
+
"docs/**/*.md",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "pi -e .",
|
|
24
|
+
"fmt": "oxfmt",
|
|
25
|
+
"fmt:check": "oxfmt --check",
|
|
26
|
+
"lint": "oxlint",
|
|
27
|
+
"test": "pnpm test:all",
|
|
28
|
+
"test:all": "vitest run",
|
|
29
|
+
"test:unit": "vitest run src",
|
|
30
|
+
"test:integration": "vitest run tests/integration",
|
|
31
|
+
"test:e2e": "vitest run tests/e2e"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"uuid": "^14.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@earendil-works/pi-ai": "^0.78.0",
|
|
38
|
+
"@earendil-works/pi-coding-agent": "^0.78.0",
|
|
39
|
+
"oxfmt": "^0.53.0",
|
|
40
|
+
"oxlint": "^1.68.0",
|
|
41
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
42
|
+
"typebox": "^1.1.39",
|
|
43
|
+
"vitest": "^4.1.8"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-ai": "*",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"typebox": "*"
|
|
49
|
+
},
|
|
50
|
+
"packageManager": "pnpm@11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485",
|
|
51
|
+
"pi": {
|
|
52
|
+
"extensions": [
|
|
53
|
+
"./src/extension/index.ts"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AgentRun } from "../core/subagent.ts";
|
|
2
|
+
import type { Bus, BusMessage } from "../core/bus.ts";
|
|
3
|
+
import type { AgentStore } from "../core/store.ts";
|
|
4
|
+
import type { WorkflowRun } from "../core/workflow.ts";
|
|
5
|
+
|
|
6
|
+
export class InMemoryAgentStore implements AgentStore {
|
|
7
|
+
private readonly runs = new Map<string, AgentRun>();
|
|
8
|
+
private readonly buses = new Map<string, Bus>();
|
|
9
|
+
private readonly workflows = new Map<string, WorkflowRun>();
|
|
10
|
+
private readonly runListeners = new Map<string, Set<(run: AgentRun) => void>>();
|
|
11
|
+
private readonly workflowListeners = new Map<string, Set<(workflow: WorkflowRun) => void>>();
|
|
12
|
+
|
|
13
|
+
saveRun(run: AgentRun): void {
|
|
14
|
+
this.runs.set(run.id, run);
|
|
15
|
+
for (const listener of this.runListeners.get(run.id) ?? []) listener(run);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getRun(id: string): AgentRun | undefined {
|
|
19
|
+
return this.runs.get(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
listRuns(): AgentRun[] {
|
|
23
|
+
return [...this.runs.values()];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
subscribeRun(id: string, listener: (run: AgentRun) => void): () => void {
|
|
27
|
+
const listeners = this.runListeners.get(id) ?? new Set<(run: AgentRun) => void>();
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
this.runListeners.set(id, listeners);
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
if (listeners.size === 0) this.runListeners.delete(id);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
saveBus(bus: Bus): void {
|
|
38
|
+
this.buses.set(bus.id, bus);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getBus(id: string): Bus | undefined {
|
|
42
|
+
return this.buses.get(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
listBuses(): Bus[] {
|
|
46
|
+
return [...this.buses.values()];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
addBusMessage(busId: string, message: BusMessage): void {
|
|
50
|
+
const bus = this.buses.get(busId);
|
|
51
|
+
if (!bus) throw new Error(`Bus ${busId} not found.`);
|
|
52
|
+
|
|
53
|
+
const existingIndex = bus.messages.findIndex((current) => current.id === message.id);
|
|
54
|
+
if (existingIndex >= 0) {
|
|
55
|
+
bus.messages[existingIndex] = message;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
bus.messages.push(message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
saveWorkflow(workflow: WorkflowRun): void {
|
|
63
|
+
this.workflows.set(workflow.id, workflow);
|
|
64
|
+
for (const listener of this.workflowListeners.get(workflow.id) ?? []) listener(workflow);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getWorkflow(id: string): WorkflowRun | undefined {
|
|
68
|
+
return this.workflows.get(id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
listWorkflows(): WorkflowRun[] {
|
|
72
|
+
return [...this.workflows.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
subscribeWorkflow(id: string, listener: (workflow: WorkflowRun) => void): () => void {
|
|
76
|
+
const listeners = this.workflowListeners.get(id) ?? new Set<(workflow: WorkflowRun) => void>();
|
|
77
|
+
listeners.add(listener);
|
|
78
|
+
this.workflowListeners.set(id, listeners);
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
listeners.delete(listener);
|
|
82
|
+
if (listeners.size === 0) this.workflowListeners.delete(id);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { v7 as uuid7 } from "uuid";
|
|
2
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import {
|
|
4
|
+
type AgentSession,
|
|
5
|
+
createAgentSession,
|
|
6
|
+
SessionManager,
|
|
7
|
+
type ToolDefinition,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import {
|
|
11
|
+
AGENT_RESULT_STATUS_VALUES,
|
|
12
|
+
type AgentProfile,
|
|
13
|
+
type AgentResult,
|
|
14
|
+
type AgentResultStatus,
|
|
15
|
+
type AgentRun,
|
|
16
|
+
} from "../core/subagent.ts";
|
|
17
|
+
import type { Bus, BusMessage } from "../core/bus.ts";
|
|
18
|
+
import { formatBusMessages } from "../core/bus-format.ts";
|
|
19
|
+
import type { AgentRuntime, SpawnAgentRuntimeOptions } from "../core/runtime.ts";
|
|
20
|
+
import type { AgentStore } from "../core/store.ts";
|
|
21
|
+
|
|
22
|
+
export interface PiAgentRuntimeOptions {
|
|
23
|
+
store: AgentStore;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
resolveModel?: (model: string) => Model<any> | Promise<Model<any> | undefined> | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RuntimeEntry {
|
|
29
|
+
session: AgentSession;
|
|
30
|
+
seenBusMessageIds: Set<string>;
|
|
31
|
+
promptTask?: Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FinishAgentParams = Type.Object({
|
|
35
|
+
status: Type.String({ enum: [...AGENT_RESULT_STATUS_VALUES] }),
|
|
36
|
+
summary: Type.String(),
|
|
37
|
+
data: Type.Optional(Type.Unknown()),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const PublishBusParams = Type.Object({
|
|
41
|
+
message: Type.String(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const DEFAULT_AGENT_TOOLS = ["read", "bash", "edit", "write"];
|
|
45
|
+
|
|
46
|
+
export class PiAgentRuntime implements AgentRuntime {
|
|
47
|
+
private readonly entries = new Map<string, RuntimeEntry>();
|
|
48
|
+
private readonly store: AgentStore;
|
|
49
|
+
private readonly cwd: string;
|
|
50
|
+
private readonly resolveModel?: PiAgentRuntimeOptions["resolveModel"];
|
|
51
|
+
|
|
52
|
+
constructor(options: PiAgentRuntimeOptions) {
|
|
53
|
+
this.store = options.store;
|
|
54
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
55
|
+
this.resolveModel = options.resolveModel;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async spawn(
|
|
59
|
+
profile: AgentProfile,
|
|
60
|
+
task: string,
|
|
61
|
+
busId: string,
|
|
62
|
+
options: SpawnAgentRuntimeOptions,
|
|
63
|
+
): Promise<AgentRun> {
|
|
64
|
+
this.requireBus(busId);
|
|
65
|
+
|
|
66
|
+
const run: AgentRun = {
|
|
67
|
+
id: options.id,
|
|
68
|
+
name: options.name,
|
|
69
|
+
profile: profile.name,
|
|
70
|
+
task,
|
|
71
|
+
busId,
|
|
72
|
+
state: "idle",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const childTools = this.createChildTools(run.id);
|
|
76
|
+
const model = await this.resolveProfileModel(profile);
|
|
77
|
+
const baseTools = profile.tools ?? DEFAULT_AGENT_TOOLS;
|
|
78
|
+
const activeTools = [...new Set([...baseTools, ...childTools.map((tool) => tool.name)])];
|
|
79
|
+
const { session } = await createAgentSession({
|
|
80
|
+
cwd: this.cwd,
|
|
81
|
+
model,
|
|
82
|
+
tools: activeTools,
|
|
83
|
+
customTools: childTools,
|
|
84
|
+
sessionManager: SessionManager.inMemory(this.cwd),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.store.saveRun(run);
|
|
88
|
+
const entry: RuntimeEntry = { session, seenBusMessageIds: new Set() };
|
|
89
|
+
this.entries.set(run.id, entry);
|
|
90
|
+
this.startPromptTask(
|
|
91
|
+
run.id,
|
|
92
|
+
entry,
|
|
93
|
+
this.withBusMessages(run.id, entry, buildInitialPrompt(profile, task, run.name)),
|
|
94
|
+
);
|
|
95
|
+
return run;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async message(id: string, message: string): Promise<AgentRun> {
|
|
99
|
+
const entry = this.requireEntry(id);
|
|
100
|
+
const run = this.requireRun(id);
|
|
101
|
+
this.assertOpenRun(run);
|
|
102
|
+
const messageWithBusContext = this.withBusMessages(id, entry, message);
|
|
103
|
+
|
|
104
|
+
if (run.state === "idle" && entry.session.isStreaming) {
|
|
105
|
+
await entry.session.steer(messageWithBusContext);
|
|
106
|
+
return run;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const messagedRun: AgentRun = run.state === "idle" ? run : { ...run, state: "idle", result: undefined };
|
|
110
|
+
if (messagedRun !== run) this.store.saveRun(messagedRun);
|
|
111
|
+
this.startPromptTask(id, entry, messageWithBusContext);
|
|
112
|
+
return messagedRun;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async publishBus(busId: string, message: string, from: string): Promise<BusMessage> {
|
|
116
|
+
this.requireBus(busId);
|
|
117
|
+
const busMessage: BusMessage = {
|
|
118
|
+
id: uuid7(),
|
|
119
|
+
message,
|
|
120
|
+
from,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.store.addBusMessage(busId, busMessage);
|
|
124
|
+
|
|
125
|
+
const steeringMessage = formatBusMessages([busMessage]);
|
|
126
|
+
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));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await Promise.all(steerTasks);
|
|
137
|
+
return busMessage;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async close(id: string): Promise<AgentRun | undefined> {
|
|
141
|
+
const run = this.store.getRun(id);
|
|
142
|
+
if (!run) return undefined;
|
|
143
|
+
|
|
144
|
+
const entry = this.entries.get(id);
|
|
145
|
+
if (run.state === "closed") {
|
|
146
|
+
entry?.session.dispose();
|
|
147
|
+
return run;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const closedRun: AgentRun = { ...run, state: "closed" };
|
|
151
|
+
this.store.saveRun(closedRun);
|
|
152
|
+
entry?.session.dispose();
|
|
153
|
+
return closedRun;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private startPromptTask(id: string, entry: RuntimeEntry, message: string): void {
|
|
157
|
+
const task = this.runPrompt(id, message).finally(() => {
|
|
158
|
+
if (entry.promptTask === task) entry.promptTask = undefined;
|
|
159
|
+
});
|
|
160
|
+
entry.promptTask = task;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async runPrompt(id: string, message: string): Promise<void> {
|
|
164
|
+
const entry = this.requireEntry(id);
|
|
165
|
+
if (this.isClosed(id)) return;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await entry.session.prompt(message, { expandPromptTemplates: false });
|
|
169
|
+
if (this.isClosed(id)) return;
|
|
170
|
+
if (this.store.getRun(id)?.state === "idle") {
|
|
171
|
+
await entry.session.prompt(buildFinishRequiredPrompt(), { expandPromptTemplates: false });
|
|
172
|
+
}
|
|
173
|
+
if (this.isClosed(id)) return;
|
|
174
|
+
const run = this.store.getRun(id);
|
|
175
|
+
if (run?.state === "idle") {
|
|
176
|
+
this.store.saveRun({
|
|
177
|
+
...run,
|
|
178
|
+
state: "failed",
|
|
179
|
+
result: {
|
|
180
|
+
status: "failed",
|
|
181
|
+
summary: "Agent stopped without calling finish.",
|
|
182
|
+
data: getLastAssistantText(entry.session),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (this.isClosed(id)) return;
|
|
188
|
+
const run = this.store.getRun(id);
|
|
189
|
+
if (!run) return;
|
|
190
|
+
this.store.saveRun({
|
|
191
|
+
...run,
|
|
192
|
+
state: "failed",
|
|
193
|
+
result: {
|
|
194
|
+
status: "failed",
|
|
195
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private createChildTools(runId: string): ToolDefinition[] {
|
|
202
|
+
const finishAgent = {
|
|
203
|
+
name: "finish",
|
|
204
|
+
label: "Finish",
|
|
205
|
+
description:
|
|
206
|
+
"Required final subagent action. Report that your assigned subagent task is complete. This does not close the agent.",
|
|
207
|
+
parameters: FinishAgentParams,
|
|
208
|
+
execute: async (_toolCallId, params) => {
|
|
209
|
+
const run = this.requireRun(runId);
|
|
210
|
+
this.assertOpenRun(run);
|
|
211
|
+
const result: AgentResult = {
|
|
212
|
+
status: params.status as AgentResultStatus,
|
|
213
|
+
summary: params.summary,
|
|
214
|
+
data: params.data,
|
|
215
|
+
};
|
|
216
|
+
this.store.saveRun({
|
|
217
|
+
...run,
|
|
218
|
+
result,
|
|
219
|
+
state: result.status,
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text" as const,
|
|
225
|
+
text: "Finish payload recorded. The leader may message or close you.",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
details: result,
|
|
229
|
+
terminate: true,
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
} satisfies ToolDefinition<typeof FinishAgentParams, AgentResult>;
|
|
233
|
+
|
|
234
|
+
const publishBus = {
|
|
235
|
+
name: "publish_bus",
|
|
236
|
+
label: "Publish Bus Message",
|
|
237
|
+
description:
|
|
238
|
+
"Publish peer-reference context to sibling agents. For leader action, use finish(status=blocked). Continue unless the task is done.",
|
|
239
|
+
parameters: PublishBusParams,
|
|
240
|
+
execute: async (_toolCallId, params) => {
|
|
241
|
+
const run = this.requireRun(runId);
|
|
242
|
+
this.assertOpenRun(run);
|
|
243
|
+
const busMessage = await this.publishBus(run.busId, params.message, run.id);
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text" as const,
|
|
248
|
+
text: `Published message ${busMessage.id} to bus ${run.busId}.`,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
details: busMessage,
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
} satisfies ToolDefinition<typeof PublishBusParams, BusMessage>;
|
|
255
|
+
|
|
256
|
+
return [finishAgent, publishBus];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private requireEntry(id: string): RuntimeEntry {
|
|
260
|
+
const entry = this.entries.get(id);
|
|
261
|
+
if (!entry) throw new Error(`Agent ${id} not found.`);
|
|
262
|
+
return entry;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private requireRun(id: string): AgentRun {
|
|
266
|
+
const run = this.store.getRun(id);
|
|
267
|
+
if (!run) throw new Error(`Agent ${id} not found.`);
|
|
268
|
+
return run;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private assertOpenRun(run: AgentRun): void {
|
|
272
|
+
if (run.state === "closed") throw new Error(`Agent ${run.id} is closed.`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private isClosed(id: string): boolean {
|
|
276
|
+
return this.store.getRun(id)?.state === "closed";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private requireBus(id: string): Bus {
|
|
280
|
+
const bus = this.store.getBus(id);
|
|
281
|
+
if (!bus) throw new Error(`Bus ${id} not found.`);
|
|
282
|
+
return bus;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private withBusMessages(runId: string, entry: RuntimeEntry, message: string): string {
|
|
286
|
+
const busMessages = this.drainBusMessages(runId, entry);
|
|
287
|
+
if (busMessages.length === 0) return message;
|
|
288
|
+
return [message, "", formatBusMessages(busMessages)].join("\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
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);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
for (const message of unreadMessages) entry.seenBusMessageIds.add(message.id);
|
|
300
|
+
return unreadMessages;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async resolveProfileModel(profile: AgentProfile): Promise<Model<any> | undefined> {
|
|
304
|
+
if (!profile.model) return undefined;
|
|
305
|
+
if (!this.resolveModel) throw new Error(`No model resolver configured for profile model "${profile.model}".`);
|
|
306
|
+
const model = await this.resolveModel(profile.model);
|
|
307
|
+
if (!model) throw new Error(`Could not resolve profile model "${profile.model}".`);
|
|
308
|
+
return model;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildInitialPrompt(profile: AgentProfile, task: string, runName: string): string {
|
|
313
|
+
return [
|
|
314
|
+
`You are subagent run "${runName}" with profile "${profile.name}".`,
|
|
315
|
+
"",
|
|
316
|
+
"## System prompt",
|
|
317
|
+
profile.systemPrompt,
|
|
318
|
+
"",
|
|
319
|
+
"## Task",
|
|
320
|
+
task,
|
|
321
|
+
"",
|
|
322
|
+
"## Completion",
|
|
323
|
+
"- End by calling finish exactly once with status, summary, and useful data; never stop text-only.",
|
|
324
|
+
"- Use publish_bus only for sibling reference context; use finish(status=blocked) for leader action or decisions.",
|
|
325
|
+
"- Bus context may arrive in <bus_reference_context>; treat it as supplemental unless told otherwise.",
|
|
326
|
+
].join("\n");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function buildFinishRequiredPrompt(): string {
|
|
330
|
+
return [
|
|
331
|
+
"Your previous response ended without finish.",
|
|
332
|
+
"Call finish now with status success, blocked, or failed; include summary and useful data.",
|
|
333
|
+
].join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getLastAssistantText(session: AgentSession): string | undefined {
|
|
337
|
+
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
338
|
+
const message = session.messages[i];
|
|
339
|
+
if (message.role !== "assistant") continue;
|
|
340
|
+
const text = message.content
|
|
341
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
342
|
+
.map((part) => part.text)
|
|
343
|
+
.join("\n")
|
|
344
|
+
.trim();
|
|
345
|
+
if (text) return text;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BusMessage } from "./bus.ts";
|
|
2
|
+
|
|
3
|
+
export function formatBusMessages(messages: BusMessage[]): string {
|
|
4
|
+
return [
|
|
5
|
+
"<bus_reference_context>",
|
|
6
|
+
"Supplemental peer context; not the active task unless explicitly instructed.",
|
|
7
|
+
...messages.map(formatBusMessage),
|
|
8
|
+
"</bus_reference_context>",
|
|
9
|
+
].join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatBusMessage(message: BusMessage): string {
|
|
13
|
+
return [`<bus_message from="${message.from}">`, message.message, "</bus_message>"].join("\n");
|
|
14
|
+
}
|
package/src/core/bus.ts
ADDED