@akira-tl/forgerelay 0.6.2 → 0.7.1
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/CHANGELOG.md +26 -0
- package/capabilities/subagents/GUIDE.md +100 -40
- package/dist/activity/lifecycle.js +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capability-registry.js +34 -0
- package/dist/cli.js +80 -165
- package/dist/db/migrations.js +14 -0
- package/dist/db/schema.js +6 -0
- package/dist/server.js +29 -32
- package/dist/{local-agent-targets.js → subagents/cli-target.js} +6 -6
- package/dist/{local-agent-profiles.js → subagents/profiles.js} +13 -5
- package/dist/subagents/providers/adapters/acp.js +148 -0
- package/dist/subagents/providers/adapters/claude.js +75 -0
- package/dist/{local-agent-runtime.js → subagents/providers/adapters/codex.js} +10 -4
- package/dist/subagents/providers/adapters/opencode.js +137 -0
- package/dist/subagents/providers/adapters/pi.js +232 -0
- package/dist/{local-agent-availability.js → subagents/providers/availability.js} +24 -10
- package/dist/subagents/providers/continuation.js +11 -0
- package/dist/subagents/providers/contract.js +1 -0
- package/dist/subagents/providers/registry.js +26 -0
- package/dist/subagents/providers/shared.js +40 -0
- package/dist/subagents/sessions/capability.js +214 -0
- package/dist/subagents/sessions/delivery-mailbox.js +115 -0
- package/dist/subagents/sessions/execution.js +171 -0
- package/dist/subagents/sessions/manager.js +107 -0
- package/dist/subagents/sessions/mcp/audit.js +85 -0
- package/dist/subagents/sessions/mcp/runtime.js +19 -0
- package/dist/{local-agent-store.js → subagents/sessions/store.js} +75 -39
- package/dist/workspaces.js +3 -3
- package/docs/chatgpt-coding-workflow.md +2 -9
- package/docs/roadmap.md +42 -7
- package/package.json +2 -2
- package/scripts/release/release-gate.test.mjs +2 -2
- package/dist/local-agent-adapters.js +0 -653
- /package/dist/{local-agent-path.js → subagents/providers/path.js} +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { CapabilityError, } from "../../capability-registry.js";
|
|
2
|
+
import { isSubagentProvider } from "../profiles.js";
|
|
3
|
+
import { subagentProviderContinuationSupported } from "../providers/continuation.js";
|
|
4
|
+
import { SubagentDeliveryMailbox } from "./delivery-mailbox.js";
|
|
5
|
+
import { executeSubagentRun, } from "./execution.js";
|
|
6
|
+
import { SubagentSessionError, SubagentSessionManager, } from "./manager.js";
|
|
7
|
+
export class SubagentSessionCapability {
|
|
8
|
+
config;
|
|
9
|
+
activityLifecycle;
|
|
10
|
+
mailbox;
|
|
11
|
+
providerRunner;
|
|
12
|
+
constructor(config, activityLifecycle, options = {}) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.activityLifecycle = activityLifecycle;
|
|
15
|
+
this.mailbox = new SubagentDeliveryMailbox(config.stateDir);
|
|
16
|
+
this.providerRunner = options.providerRunner ?? defaultProviderRunner;
|
|
17
|
+
}
|
|
18
|
+
async run(input, context, options) {
|
|
19
|
+
const manager = new SubagentSessionManager(this.config, {
|
|
20
|
+
launch: (request) => this.launch(request),
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
switch (input.operation) {
|
|
24
|
+
case "start": {
|
|
25
|
+
const started = await manager.start({
|
|
26
|
+
workspaceId: context.workspaceId,
|
|
27
|
+
workspaceRoot: context.workspaceRoot,
|
|
28
|
+
target: input.target,
|
|
29
|
+
prompt: input.prompt,
|
|
30
|
+
model: input.model,
|
|
31
|
+
thinking: input.thinking,
|
|
32
|
+
activityId: options.activityId,
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
value: {
|
|
36
|
+
operation: "start",
|
|
37
|
+
session: publicSession(started.session),
|
|
38
|
+
run: publicRun(started.run),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
case "resume": {
|
|
43
|
+
const resumed = manager.resume({
|
|
44
|
+
sessionId: input.sessionId,
|
|
45
|
+
prompt: input.prompt,
|
|
46
|
+
activityId: options.activityId,
|
|
47
|
+
}, { workspaceId: context.workspaceId });
|
|
48
|
+
return {
|
|
49
|
+
value: {
|
|
50
|
+
operation: "resume",
|
|
51
|
+
session: publicSession(resumed.session),
|
|
52
|
+
run: publicRun(resumed.run),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
case "status": {
|
|
57
|
+
const session = manager.get(input.sessionId, { workspaceId: context.workspaceId });
|
|
58
|
+
if (!session) {
|
|
59
|
+
throw new SubagentSessionError("subagent.session_not_found", `Unknown Subagent Session in this Workspace: ${input.sessionId}`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
value: {
|
|
63
|
+
operation: "status",
|
|
64
|
+
session: publicSession(session),
|
|
65
|
+
...(session.activeRun ? { activeRun: publicRun(session.activeRun) } : {}),
|
|
66
|
+
...(session.latestRun ? { latestRun: publicRun(session.latestRun) } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case "list":
|
|
71
|
+
return {
|
|
72
|
+
value: {
|
|
73
|
+
operation: "list",
|
|
74
|
+
sessions: manager.list({ workspaceId: context.workspaceId }).map(publicSessionSummary),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof SubagentSessionError) {
|
|
81
|
+
throw new CapabilityError(error.code, error.message);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
manager.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
decorateResult(workspaceId, result) {
|
|
90
|
+
if (typeof result !== "object" || result === null)
|
|
91
|
+
return result;
|
|
92
|
+
const content = result.content;
|
|
93
|
+
if (!Array.isArray(content))
|
|
94
|
+
return result;
|
|
95
|
+
const excludeRunId = currentRunId(result);
|
|
96
|
+
const deliveries = this.mailbox.claimWorkspace(workspaceId, excludeRunId);
|
|
97
|
+
if (deliveries.length === 0)
|
|
98
|
+
return result;
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
content: [
|
|
102
|
+
...content,
|
|
103
|
+
...deliveries.map((delivery) => ({ type: "text", text: deliveryText(delivery) })),
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
launch(request) {
|
|
108
|
+
void executeSubagentRun(this.config, request, this.providerRunner)
|
|
109
|
+
.then((completion) => this.recordCompletion(completion))
|
|
110
|
+
.catch(() => {
|
|
111
|
+
// The worker writes durable Session/mailbox state for provider failures.
|
|
112
|
+
// An unexpected orchestration failure must not become an unhandled rejection.
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
recordCompletion(completion) {
|
|
116
|
+
if (!completion.activityId)
|
|
117
|
+
return;
|
|
118
|
+
this.activityLifecycle.recordLinked({
|
|
119
|
+
sourceActivityId: completion.activityId,
|
|
120
|
+
tool: "subagent_result",
|
|
121
|
+
request: {
|
|
122
|
+
sessionId: completion.sessionId,
|
|
123
|
+
runId: completion.runId,
|
|
124
|
+
},
|
|
125
|
+
result: {
|
|
126
|
+
sessionId: completion.sessionId,
|
|
127
|
+
runId: completion.runId,
|
|
128
|
+
provider: completion.provider,
|
|
129
|
+
status: completion.outcome,
|
|
130
|
+
},
|
|
131
|
+
outcome: completion.outcome === "failed"
|
|
132
|
+
? { type: "failed", error: "Subagent Run failed." }
|
|
133
|
+
: { type: "succeeded" },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function publicSession(session) {
|
|
138
|
+
const continuationSupported = sessionContinuationSupported(session);
|
|
139
|
+
return {
|
|
140
|
+
id: session.id,
|
|
141
|
+
status: session.status,
|
|
142
|
+
profileName: session.profileName,
|
|
143
|
+
provider: session.provider,
|
|
144
|
+
continuationSupported,
|
|
145
|
+
resumable: continuationSupported && session.status === "idle" && Boolean(session.providerSessionId),
|
|
146
|
+
...(session.model ? { model: session.model } : {}),
|
|
147
|
+
...(session.thinking ? { thinking: session.thinking } : {}),
|
|
148
|
+
...(session.activeRun ? { activeRun: publicRun(session.activeRun) } : {}),
|
|
149
|
+
...(session.latestRun ? { latestRun: publicRun(session.latestRun) } : {}),
|
|
150
|
+
createdAt: session.createdAt,
|
|
151
|
+
updatedAt: session.updatedAt,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function publicSessionSummary(session) {
|
|
155
|
+
const continuationSupported = sessionContinuationSupported(session);
|
|
156
|
+
return {
|
|
157
|
+
id: session.id,
|
|
158
|
+
status: session.status,
|
|
159
|
+
profileName: session.profileName,
|
|
160
|
+
provider: session.provider,
|
|
161
|
+
continuationSupported,
|
|
162
|
+
resumable: continuationSupported && session.status === "idle" && Boolean(session.providerSessionId),
|
|
163
|
+
...(session.model ? { model: session.model } : {}),
|
|
164
|
+
...(session.thinking ? { thinking: session.thinking } : {}),
|
|
165
|
+
...(session.activeRun ? { activeRunId: session.activeRun.id } : {}),
|
|
166
|
+
...(session.latestRun ? {
|
|
167
|
+
latestRun: {
|
|
168
|
+
id: session.latestRun.id,
|
|
169
|
+
status: session.latestRun.status,
|
|
170
|
+
},
|
|
171
|
+
} : {}),
|
|
172
|
+
updatedAt: session.updatedAt,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function sessionContinuationSupported(session) {
|
|
176
|
+
return isSubagentProvider(session.provider)
|
|
177
|
+
? subagentProviderContinuationSupported(session.provider)
|
|
178
|
+
: false;
|
|
179
|
+
}
|
|
180
|
+
function publicRun(run) {
|
|
181
|
+
return {
|
|
182
|
+
id: run.id,
|
|
183
|
+
status: run.status,
|
|
184
|
+
...(run.startedAt ? { startedAt: run.startedAt } : {}),
|
|
185
|
+
...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function currentRunId(result) {
|
|
189
|
+
if (typeof result !== "object" || result === null)
|
|
190
|
+
return undefined;
|
|
191
|
+
const structured = result.structuredContent;
|
|
192
|
+
if (typeof structured !== "object" || structured === null)
|
|
193
|
+
return undefined;
|
|
194
|
+
const capabilityResult = structured.result;
|
|
195
|
+
if (typeof capabilityResult !== "object" || capabilityResult === null)
|
|
196
|
+
return undefined;
|
|
197
|
+
const run = capabilityResult.run;
|
|
198
|
+
if (typeof run !== "object" || run === null)
|
|
199
|
+
return undefined;
|
|
200
|
+
const id = run.id;
|
|
201
|
+
return typeof id === "string" ? id : undefined;
|
|
202
|
+
}
|
|
203
|
+
function deliveryText(delivery) {
|
|
204
|
+
const header = `Subagent ${delivery.sessionId} Run ${delivery.runId} ${delivery.outcome}.`;
|
|
205
|
+
const body = delivery.outcome === "succeeded"
|
|
206
|
+
? delivery.finalResponse
|
|
207
|
+
: delivery.error;
|
|
208
|
+
const suffix = delivery.truncated ? "\n[Subagent result truncated for delivery.]" : "";
|
|
209
|
+
return body ? `${header}\n${body}${suffix}` : `${header}${suffix}`;
|
|
210
|
+
}
|
|
211
|
+
async function defaultProviderRunner(provider, input) {
|
|
212
|
+
const { runSubagentProvider } = await import("../providers/registry.js");
|
|
213
|
+
return runSubagentProvider(provider, input);
|
|
214
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const MAX_DELIVERY_TEXT_BYTES = 64 * 1024;
|
|
4
|
+
const MAX_DELIVERIES_PER_RESPONSE = 4;
|
|
5
|
+
export class SubagentDeliveryMailbox {
|
|
6
|
+
directory;
|
|
7
|
+
constructor(stateDir) {
|
|
8
|
+
this.directory = join(stateDir, "subagent-delivery");
|
|
9
|
+
}
|
|
10
|
+
write(input) {
|
|
11
|
+
const response = boundText(input.finalResponse);
|
|
12
|
+
const error = boundText(input.error);
|
|
13
|
+
const delivery = {
|
|
14
|
+
sessionId: input.sessionId,
|
|
15
|
+
runId: input.runId,
|
|
16
|
+
workspaceId: input.workspaceId,
|
|
17
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
18
|
+
provider: input.provider,
|
|
19
|
+
outcome: input.outcome,
|
|
20
|
+
...(response.text !== undefined ? { finalResponse: response.text } : {}),
|
|
21
|
+
...(error.text !== undefined ? { error: error.text } : {}),
|
|
22
|
+
truncated: response.truncated || error.truncated,
|
|
23
|
+
createdAt: new Date().toISOString(),
|
|
24
|
+
};
|
|
25
|
+
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
26
|
+
const path = this.pathFor(delivery.sessionId);
|
|
27
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
28
|
+
writeFileSync(temporary, `${JSON.stringify(delivery)}\n`, { mode: 0o600 });
|
|
29
|
+
rmSync(path, { force: true });
|
|
30
|
+
renameSync(temporary, path);
|
|
31
|
+
return delivery;
|
|
32
|
+
}
|
|
33
|
+
claimWorkspace(workspaceId, excludeRunId) {
|
|
34
|
+
return this.claim((delivery) => delivery.workspaceId === workspaceId && delivery.runId !== excludeRunId);
|
|
35
|
+
}
|
|
36
|
+
claimSession(workspaceId, sessionId) {
|
|
37
|
+
return this.claim((delivery) => delivery.workspaceId === workspaceId && delivery.sessionId === sessionId);
|
|
38
|
+
}
|
|
39
|
+
hasSession(sessionId) {
|
|
40
|
+
return this.files().includes(`${sessionId}.json`);
|
|
41
|
+
}
|
|
42
|
+
claim(predicate) {
|
|
43
|
+
const deliveries = [];
|
|
44
|
+
for (const file of this.files()) {
|
|
45
|
+
if (deliveries.length >= MAX_DELIVERIES_PER_RESPONSE)
|
|
46
|
+
break;
|
|
47
|
+
const path = join(this.directory, file);
|
|
48
|
+
const delivery = readDelivery(path);
|
|
49
|
+
if (!delivery || !predicate(delivery))
|
|
50
|
+
continue;
|
|
51
|
+
rmSync(path, { force: true });
|
|
52
|
+
deliveries.push(delivery);
|
|
53
|
+
}
|
|
54
|
+
return deliveries.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
55
|
+
}
|
|
56
|
+
files() {
|
|
57
|
+
try {
|
|
58
|
+
return readdirSync(this.directory)
|
|
59
|
+
.filter((file) => /^agt_[a-z0-9]+\.json$/i.test(file))
|
|
60
|
+
.sort();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code === "ENOENT")
|
|
64
|
+
return [];
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
pathFor(sessionId) {
|
|
69
|
+
if (!/^agt_[a-z0-9]+$/i.test(sessionId))
|
|
70
|
+
throw new Error(`Invalid Subagent Session id: ${sessionId}`);
|
|
71
|
+
return join(this.directory, `${sessionId}.json`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readDelivery(path) {
|
|
75
|
+
try {
|
|
76
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
+
if (typeof value.sessionId !== "string" ||
|
|
78
|
+
typeof value.runId !== "string" ||
|
|
79
|
+
typeof value.workspaceId !== "string" ||
|
|
80
|
+
typeof value.provider !== "string" ||
|
|
81
|
+
!isOutcome(value.outcome) ||
|
|
82
|
+
typeof value.createdAt !== "string") {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
sessionId: value.sessionId,
|
|
87
|
+
runId: value.runId,
|
|
88
|
+
workspaceId: value.workspaceId,
|
|
89
|
+
...(typeof value.activityId === "string" ? { activityId: value.activityId } : {}),
|
|
90
|
+
provider: value.provider,
|
|
91
|
+
outcome: value.outcome,
|
|
92
|
+
...(typeof value.finalResponse === "string" ? { finalResponse: value.finalResponse } : {}),
|
|
93
|
+
...(typeof value.error === "string" ? { error: value.error } : {}),
|
|
94
|
+
truncated: value.truncated === true,
|
|
95
|
+
createdAt: value.createdAt,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function boundText(value) {
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return { truncated: false };
|
|
105
|
+
const bytes = Buffer.from(value, "utf8");
|
|
106
|
+
if (bytes.length <= MAX_DELIVERY_TEXT_BYTES)
|
|
107
|
+
return { text: value, truncated: false };
|
|
108
|
+
return {
|
|
109
|
+
text: bytes.subarray(0, MAX_DELIVERY_TEXT_BYTES).toString("utf8"),
|
|
110
|
+
truncated: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function isOutcome(value) {
|
|
114
|
+
return value === "succeeded" || value === "failed" || value === "cancelled" || value === "interrupted";
|
|
115
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { HookRunner } from "../../hooks.js";
|
|
2
|
+
import { isSubagentProvider, loadSubagentProfiles, } from "../profiles.js";
|
|
3
|
+
import { runSubagentProvider } from "../providers/registry.js";
|
|
4
|
+
import { SubagentDeliveryMailbox } from "./delivery-mailbox.js";
|
|
5
|
+
import { createSubagentSessionStore, } from "./store.js";
|
|
6
|
+
export async function executeSubagentRun(config, input, providerRunner = runSubagentProvider) {
|
|
7
|
+
const store = createSubagentSessionStore(config);
|
|
8
|
+
const mailbox = new SubagentDeliveryMailbox(config.stateDir);
|
|
9
|
+
try {
|
|
10
|
+
const record = store.get(input.sessionId);
|
|
11
|
+
if (!record)
|
|
12
|
+
throw new Error(`Unknown subagent id: ${input.sessionId}`);
|
|
13
|
+
if (record.activeRun?.id !== input.runId) {
|
|
14
|
+
throw new Error(`Subagent Run ${input.runId} is not active for Session ${record.id}.`);
|
|
15
|
+
}
|
|
16
|
+
const hooks = new HookRunner(config.hooks, config.logging);
|
|
17
|
+
const hookInvocation = {
|
|
18
|
+
workspaceId: record.workspaceId,
|
|
19
|
+
workspaceRoot: record.workspaceRoot,
|
|
20
|
+
payload: {
|
|
21
|
+
agentId: record.id,
|
|
22
|
+
sessionId: record.id,
|
|
23
|
+
runId: input.runId,
|
|
24
|
+
profile: record.profileName,
|
|
25
|
+
provider: record.provider,
|
|
26
|
+
model: record.model,
|
|
27
|
+
thinking: record.thinking,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
await hooks.run("SubagentStart", hookInvocation);
|
|
31
|
+
try {
|
|
32
|
+
const result = await runSessionProvider(config, record, input.prompt, providerRunner);
|
|
33
|
+
await hooks.run("SubagentStop", {
|
|
34
|
+
...hookInvocation,
|
|
35
|
+
payload: {
|
|
36
|
+
...hookInvocation.payload,
|
|
37
|
+
status: "succeeded",
|
|
38
|
+
providerSessionId: result.providerSessionId,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const finishedAt = new Date().toISOString();
|
|
42
|
+
store.update(record.id, {
|
|
43
|
+
providerSessionId: result.providerSessionId ?? undefined,
|
|
44
|
+
status: "idle",
|
|
45
|
+
activeRun: undefined,
|
|
46
|
+
latestRun: {
|
|
47
|
+
id: input.runId,
|
|
48
|
+
status: "succeeded",
|
|
49
|
+
finishedAt,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (record.workspaceId) {
|
|
53
|
+
mailbox.write({
|
|
54
|
+
sessionId: record.id,
|
|
55
|
+
runId: input.runId,
|
|
56
|
+
workspaceId: record.workspaceId,
|
|
57
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
58
|
+
provider: record.provider,
|
|
59
|
+
outcome: "succeeded",
|
|
60
|
+
finalResponse: result.finalResponse,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return completion(record, input, "succeeded");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
await hooks.run("SubagentStop", {
|
|
68
|
+
...hookInvocation,
|
|
69
|
+
payload: {
|
|
70
|
+
...hookInvocation.payload,
|
|
71
|
+
status: "failed",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const finishedAt = new Date().toISOString();
|
|
75
|
+
store.update(record.id, {
|
|
76
|
+
status: "idle",
|
|
77
|
+
activeRun: undefined,
|
|
78
|
+
latestRun: {
|
|
79
|
+
id: input.runId,
|
|
80
|
+
status: "failed",
|
|
81
|
+
finishedAt,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (record.workspaceId) {
|
|
85
|
+
mailbox.write({
|
|
86
|
+
sessionId: record.id,
|
|
87
|
+
runId: input.runId,
|
|
88
|
+
workspaceId: record.workspaceId,
|
|
89
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
90
|
+
provider: record.provider,
|
|
91
|
+
outcome: "failed",
|
|
92
|
+
error: message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return completion(record, input, "failed", message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
store.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function executeSubagentSession(config, sessionId, prompt) {
|
|
103
|
+
const store = createSubagentSessionStore(config);
|
|
104
|
+
try {
|
|
105
|
+
const record = store.get(sessionId);
|
|
106
|
+
if (!record)
|
|
107
|
+
throw new Error(`Unknown subagent id: ${sessionId}`);
|
|
108
|
+
if (!record.activeRun)
|
|
109
|
+
throw new Error(`Subagent Session ${sessionId} has no active Run.`);
|
|
110
|
+
return executeSubagentRun(config, {
|
|
111
|
+
sessionId,
|
|
112
|
+
runId: record.activeRun.id,
|
|
113
|
+
...(record.activeRun.activityId ? { activityId: record.activeRun.activityId } : {}),
|
|
114
|
+
prompt,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
store.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function runSessionProvider(config, session, prompt, providerRunner) {
|
|
122
|
+
if (!isSubagentProvider(session.provider)) {
|
|
123
|
+
throw new Error(`Unknown subagent provider for Session ${session.id}: ${session.provider}`);
|
|
124
|
+
}
|
|
125
|
+
if (session.providerSessionId) {
|
|
126
|
+
return providerRunner(session.provider, {
|
|
127
|
+
prompt,
|
|
128
|
+
workspace: session.workspaceRoot,
|
|
129
|
+
providerSessionId: session.providerSessionId,
|
|
130
|
+
writeMode: "allowed",
|
|
131
|
+
model: session.model,
|
|
132
|
+
thinking: session.thinking,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (session.profileName === session.provider) {
|
|
136
|
+
return providerRunner(session.provider, {
|
|
137
|
+
prompt,
|
|
138
|
+
workspace: session.workspaceRoot,
|
|
139
|
+
writeMode: "allowed",
|
|
140
|
+
model: session.model,
|
|
141
|
+
thinking: session.thinking,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const profiles = await loadSubagentProfiles(config, session.workspaceRoot);
|
|
145
|
+
const profile = profiles.find((candidate) => candidate.name === session.profileName);
|
|
146
|
+
if (!profile)
|
|
147
|
+
throw new Error(`Subagent profile not found: ${session.profileName}`);
|
|
148
|
+
return runSubagentProfile(profile, session, prompt, providerRunner);
|
|
149
|
+
}
|
|
150
|
+
async function runSubagentProfile(profile, session, prompt, providerRunner) {
|
|
151
|
+
const body = profile.body.trim();
|
|
152
|
+
const firstPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
|
|
153
|
+
return providerRunner(session.provider, {
|
|
154
|
+
prompt: firstPrompt,
|
|
155
|
+
workspace: session.workspaceRoot,
|
|
156
|
+
writeMode: "allowed",
|
|
157
|
+
model: session.model,
|
|
158
|
+
thinking: session.thinking,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function completion(session, input, outcome, error) {
|
|
162
|
+
return {
|
|
163
|
+
sessionId: session.id,
|
|
164
|
+
runId: input.runId,
|
|
165
|
+
workspaceId: session.workspaceId,
|
|
166
|
+
activityId: input.activityId,
|
|
167
|
+
provider: session.provider,
|
|
168
|
+
outcome,
|
|
169
|
+
...(error ? { error } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { formatAvailableSubagentTargets, resolveSubagentTarget, } from "../cli-target.js";
|
|
3
|
+
import { isSubagentProvider, loadSubagentProfiles, } from "../profiles.js";
|
|
4
|
+
import { assertSubagentProviderAvailable } from "../providers/availability.js";
|
|
5
|
+
import { subagentProviderContinuationSupported } from "../providers/continuation.js";
|
|
6
|
+
import { createSubagentSessionStore, } from "./store.js";
|
|
7
|
+
export class SubagentSessionError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "SubagentSessionError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class SubagentSessionManager {
|
|
16
|
+
config;
|
|
17
|
+
launcher;
|
|
18
|
+
store;
|
|
19
|
+
constructor(config, launcher) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.launcher = launcher;
|
|
22
|
+
this.store = createSubagentSessionStore(config);
|
|
23
|
+
}
|
|
24
|
+
list(scope = {}) {
|
|
25
|
+
return this.store.list(scope);
|
|
26
|
+
}
|
|
27
|
+
get(idOrPrefix, scope) {
|
|
28
|
+
return scope ? this.store.getInScope(idOrPrefix, scope) : this.store.get(idOrPrefix);
|
|
29
|
+
}
|
|
30
|
+
async start(input) {
|
|
31
|
+
const profiles = await loadSubagentProfiles(this.config, input.workspaceRoot);
|
|
32
|
+
const target = resolveSubagentTarget(input.target, profiles, input.model, input.thinking);
|
|
33
|
+
if (!target) {
|
|
34
|
+
throw new Error(`Unknown subagent profile or provider: ${input.target}. Available ${formatAvailableSubagentTargets(profiles)}`);
|
|
35
|
+
}
|
|
36
|
+
assertSubagentProviderAvailable(target.provider);
|
|
37
|
+
const runId = newRunId();
|
|
38
|
+
const startedAt = new Date().toISOString();
|
|
39
|
+
const session = this.store.create({
|
|
40
|
+
workspaceId: input.workspaceId,
|
|
41
|
+
workspaceRoot: input.workspaceRoot,
|
|
42
|
+
profileName: target.name,
|
|
43
|
+
provider: target.provider,
|
|
44
|
+
model: target.model,
|
|
45
|
+
thinking: target.thinking,
|
|
46
|
+
activeRun: {
|
|
47
|
+
id: runId,
|
|
48
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
49
|
+
startedAt,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const run = session.activeRun;
|
|
53
|
+
if (!run)
|
|
54
|
+
throw new Error(`Subagent Session ${session.id} did not create an active Run.`);
|
|
55
|
+
this.launcher.launch({
|
|
56
|
+
sessionId: session.id,
|
|
57
|
+
runId,
|
|
58
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
59
|
+
prompt: input.prompt,
|
|
60
|
+
});
|
|
61
|
+
return { session, run };
|
|
62
|
+
}
|
|
63
|
+
resume(input, scope = {}) {
|
|
64
|
+
const existing = this.store.getInScope(input.sessionId, scope);
|
|
65
|
+
if (!existing) {
|
|
66
|
+
throw new SubagentSessionError("subagent.session_not_found", `Unknown Subagent Session in this Workspace: ${input.sessionId}`);
|
|
67
|
+
}
|
|
68
|
+
if (existing.activeRun) {
|
|
69
|
+
throw new SubagentSessionError("subagent.busy", `Subagent Session ${existing.id} already has active Run ${existing.activeRun.id}.`);
|
|
70
|
+
}
|
|
71
|
+
if (!isSubagentProvider(existing.provider)) {
|
|
72
|
+
throw new Error(`Unknown subagent provider for existing session: ${existing.provider}`);
|
|
73
|
+
}
|
|
74
|
+
if (!subagentProviderContinuationSupported(existing.provider)) {
|
|
75
|
+
throw new SubagentSessionError("subagent.continuation_unsupported", `${existing.provider} does not support true Subagent Session continuation.`);
|
|
76
|
+
}
|
|
77
|
+
if (!existing.providerSessionId) {
|
|
78
|
+
throw new SubagentSessionError("subagent.continuation_unavailable", `Subagent Session ${existing.id} has no provider continuation identity.`);
|
|
79
|
+
}
|
|
80
|
+
assertSubagentProviderAvailable(existing.provider);
|
|
81
|
+
const runId = newRunId();
|
|
82
|
+
const startedAt = new Date().toISOString();
|
|
83
|
+
const run = {
|
|
84
|
+
id: runId,
|
|
85
|
+
status: "running",
|
|
86
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
87
|
+
startedAt,
|
|
88
|
+
};
|
|
89
|
+
const session = this.store.update(existing.id, {
|
|
90
|
+
status: "running",
|
|
91
|
+
activeRun: run,
|
|
92
|
+
});
|
|
93
|
+
this.launcher.launch({
|
|
94
|
+
sessionId: session.id,
|
|
95
|
+
runId,
|
|
96
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
97
|
+
prompt: input.prompt,
|
|
98
|
+
});
|
|
99
|
+
return { session, run };
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.store.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function newRunId() {
|
|
106
|
+
return `run_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
107
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function capabilityActivityAuditRequest(input) {
|
|
2
|
+
if (input.name !== "subagent.session") {
|
|
3
|
+
return {
|
|
4
|
+
workspaceId: input.workspaceId,
|
|
5
|
+
name: input.name,
|
|
6
|
+
action: "run",
|
|
7
|
+
arguments: input.arguments,
|
|
8
|
+
file: input.file,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const argumentsValue = isAuditRecord(input.arguments) ? input.arguments : {};
|
|
12
|
+
return {
|
|
13
|
+
workspaceId: input.workspaceId,
|
|
14
|
+
name: input.name,
|
|
15
|
+
action: "run",
|
|
16
|
+
arguments: {
|
|
17
|
+
operation: argumentsValue.operation,
|
|
18
|
+
...(typeof argumentsValue.target === "string" ? { target: argumentsValue.target } : {}),
|
|
19
|
+
...(typeof argumentsValue.sessionId === "string" ? { sessionId: argumentsValue.sessionId } : {}),
|
|
20
|
+
...(typeof argumentsValue.model === "string" ? { model: argumentsValue.model } : {}),
|
|
21
|
+
...(typeof argumentsValue.thinking === "string" ? { thinking: argumentsValue.thinking } : {}),
|
|
22
|
+
...(typeof argumentsValue.prompt === "string" ? { promptLength: argumentsValue.prompt.length } : {}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function capabilityActivityAuditResult(name, result) {
|
|
27
|
+
if (name !== "subagent.session" || !isAuditRecord(result))
|
|
28
|
+
return result;
|
|
29
|
+
const structuredContent = isAuditRecord(result.structuredContent) ? result.structuredContent : undefined;
|
|
30
|
+
const capabilityResult = structuredContent && isAuditRecord(structuredContent.result)
|
|
31
|
+
? structuredContent.result
|
|
32
|
+
: undefined;
|
|
33
|
+
const error = structuredContent && isAuditRecord(structuredContent.error)
|
|
34
|
+
? structuredContent.error
|
|
35
|
+
: undefined;
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
action: "run",
|
|
39
|
+
...(capabilityResult ? { result: summarizeSubagentCapabilityResult(capabilityResult) } : {}),
|
|
40
|
+
...(error
|
|
41
|
+
? {
|
|
42
|
+
error: {
|
|
43
|
+
code: error.code,
|
|
44
|
+
message: error.message,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function isAuditRecord(value) {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function summarizeSubagentCapabilityResult(result) {
|
|
54
|
+
const summary = { operation: result.operation };
|
|
55
|
+
const session = isAuditRecord(result.session) ? result.session : undefined;
|
|
56
|
+
if (session) {
|
|
57
|
+
summary.session = {
|
|
58
|
+
id: session.id,
|
|
59
|
+
status: session.status,
|
|
60
|
+
profileName: session.profileName,
|
|
61
|
+
provider: session.provider,
|
|
62
|
+
model: session.model,
|
|
63
|
+
thinking: session.thinking,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
for (const key of ["run", "activeRun", "latestRun"]) {
|
|
67
|
+
const run = isAuditRecord(result[key]) ? result[key] : undefined;
|
|
68
|
+
if (run)
|
|
69
|
+
summary[key] = { id: run.id, status: run.status };
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(result.sessions)) {
|
|
72
|
+
summary.sessions = result.sessions.flatMap((entry) => {
|
|
73
|
+
if (!isAuditRecord(entry))
|
|
74
|
+
return [];
|
|
75
|
+
return [{
|
|
76
|
+
id: entry.id,
|
|
77
|
+
status: entry.status,
|
|
78
|
+
profileName: entry.profileName,
|
|
79
|
+
provider: entry.provider,
|
|
80
|
+
activeRunId: entry.activeRunId,
|
|
81
|
+
}];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return summary;
|
|
85
|
+
}
|