@akira-tl/forgerelay 0.6.1 → 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 +43 -0
- package/README.md +38 -0
- package/capabilities/subagents/GUIDE.md +100 -40
- package/dist/activity/lifecycle.js +1 -1
- package/dist/activity/mcp-query-tools.js +1 -0
- package/dist/activity/query-service.js +1 -0
- package/dist/capabilities.js +1 -1
- package/dist/capability-registry.js +34 -0
- package/dist/cli.js +80 -165
- package/dist/composite-activity.js +155 -0
- package/dist/composite-workspaces.js +197 -0
- package/dist/db/migrations.js +14 -0
- package/dist/db/schema.js +6 -0
- package/dist/remote-workspace-relay.js +16 -0
- package/dist/server.js +698 -172
- 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/ui/.vite/manifest.json +33 -33
- package/dist/ui/activity-panel-app.html +3 -3
- package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
- package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
- package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
- package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
- package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
- package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
- package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
- package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
- package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
- package/dist/ui/workspace-app.html +4 -4
- package/dist/ui/workspace-lifecycle-app.html +4 -4
- package/dist/workspaces.js +3 -3
- package/docs/chatgpt-coding-workflow.md +2 -9
- package/docs/configuration.md +23 -0
- package/docs/debugging.md +7 -0
- package/docs/roadmap.md +42 -7
- package/package.json +2 -2
- package/scripts/debug/runtime.mjs +23 -1
- package/scripts/debug/runtime.test.mjs +14 -2
- package/scripts/debug/serve.mjs +4 -4
- package/scripts/release/release-gate.test.mjs +2 -2
- package/dist/local-agent-adapters.js +0 -653
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
- package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
- /package/dist/{local-agent-path.js → subagents/providers/path.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SubagentSessionCapability } from "../capability.js";
|
|
2
|
+
export function createSubagentMcpRuntime(config, activityLifecycle, options = {}) {
|
|
3
|
+
const capability = config.subagents
|
|
4
|
+
? new SubagentSessionCapability(config, activityLifecycle, {
|
|
5
|
+
providerRunner: options.subagentProviderRunner,
|
|
6
|
+
})
|
|
7
|
+
: undefined;
|
|
8
|
+
return {
|
|
9
|
+
registryDependencies: capability
|
|
10
|
+
? {
|
|
11
|
+
subagentSession: {
|
|
12
|
+
available: true,
|
|
13
|
+
run: (input, context, runOptions) => capability.run(input, context, runOptions),
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
: {},
|
|
17
|
+
decorateResult: (workspaceId, result) => capability?.decorateResult(workspaceId, result) ?? result,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { openDatabase } from "
|
|
4
|
-
export class
|
|
3
|
+
import { openDatabase } from "../../db/client.js";
|
|
4
|
+
export class SubagentSessionStore {
|
|
5
5
|
database;
|
|
6
6
|
constructor(stateDir) {
|
|
7
7
|
this.database = openDatabase(stateDir);
|
|
@@ -27,7 +27,7 @@ export class LocalAgentStore {
|
|
|
27
27
|
.prepare("select * from local_agent_sessions order by updated_at desc")
|
|
28
28
|
.all();
|
|
29
29
|
}
|
|
30
|
-
return rows.map(
|
|
30
|
+
return rows.map(rowToSubagentSession);
|
|
31
31
|
}
|
|
32
32
|
create(input) {
|
|
33
33
|
const now = new Date().toISOString();
|
|
@@ -39,7 +39,17 @@ export class LocalAgentStore {
|
|
|
39
39
|
provider: input.provider,
|
|
40
40
|
model: input.model,
|
|
41
41
|
thinking: input.thinking,
|
|
42
|
-
status: "
|
|
42
|
+
status: input.activeRun ? "running" : "idle",
|
|
43
|
+
...(input.activeRun
|
|
44
|
+
? {
|
|
45
|
+
activeRun: {
|
|
46
|
+
id: input.activeRun.id,
|
|
47
|
+
status: "running",
|
|
48
|
+
...(input.activeRun.activityId ? { activityId: input.activeRun.activityId } : {}),
|
|
49
|
+
startedAt: input.activeRun.startedAt,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
: {}),
|
|
43
53
|
createdAt: now,
|
|
44
54
|
updatedAt: now,
|
|
45
55
|
};
|
|
@@ -52,11 +62,21 @@ export class LocalAgentStore {
|
|
|
52
62
|
provider,
|
|
53
63
|
model,
|
|
54
64
|
thinking,
|
|
65
|
+
provider_session_id,
|
|
55
66
|
status,
|
|
67
|
+
active_run_id,
|
|
68
|
+
active_activity_id,
|
|
69
|
+
active_run_started_at,
|
|
70
|
+
latest_run_id,
|
|
71
|
+
latest_run_outcome,
|
|
72
|
+
latest_run_finished_at,
|
|
73
|
+
latest_response,
|
|
74
|
+
error,
|
|
75
|
+
hook_reports_json,
|
|
56
76
|
created_at,
|
|
57
77
|
updated_at
|
|
58
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
59
|
-
.run(record.id, record.workspaceId ?? null, record.workspaceRoot, record.profileName, record.provider, record.model ?? null, record.thinking ?? null, record.status, record.createdAt, record.updatedAt);
|
|
78
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null, null, null, ?, ?)`)
|
|
79
|
+
.run(record.id, record.workspaceId ?? null, record.workspaceRoot, record.profileName, record.provider, record.model ?? null, record.thinking ?? null, null, record.status, record.activeRun?.id ?? null, record.activeRun?.activityId ?? null, record.activeRun?.startedAt ?? null, null, null, null, record.createdAt, record.updatedAt);
|
|
60
80
|
return record;
|
|
61
81
|
}
|
|
62
82
|
get(idOrPrefix) {
|
|
@@ -66,13 +86,23 @@ export class LocalAgentStore {
|
|
|
66
86
|
limit 1`)
|
|
67
87
|
.get(idOrPrefix, idOrPrefix);
|
|
68
88
|
if (exact)
|
|
69
|
-
return
|
|
89
|
+
return rowToSubagentSession(exact);
|
|
70
90
|
const matches = this.database.sqlite
|
|
71
91
|
.prepare(`select * from local_agent_sessions
|
|
72
92
|
where id like ? escape '\\' or provider_session_id like ? escape '\\'
|
|
73
93
|
order by updated_at desc`)
|
|
74
94
|
.all(`${escapeLike(idOrPrefix)}%`, `${escapeLike(idOrPrefix)}%`);
|
|
75
|
-
return matches.length === 1 ?
|
|
95
|
+
return matches.length === 1 ? rowToSubagentSession(matches[0]) : undefined;
|
|
96
|
+
}
|
|
97
|
+
getInScope(idOrPrefix, scope) {
|
|
98
|
+
const session = this.get(idOrPrefix);
|
|
99
|
+
if (!session)
|
|
100
|
+
return undefined;
|
|
101
|
+
if (scope.workspaceId && session.workspaceId !== scope.workspaceId)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (scope.workspaceRoot && session.workspaceRoot !== resolve(scope.workspaceRoot))
|
|
104
|
+
return undefined;
|
|
105
|
+
return session;
|
|
76
106
|
}
|
|
77
107
|
update(id, patch) {
|
|
78
108
|
const current = this.getById(id);
|
|
@@ -93,12 +123,18 @@ export class LocalAgentStore {
|
|
|
93
123
|
thinking = ?,
|
|
94
124
|
provider_session_id = ?,
|
|
95
125
|
status = ?,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
active_run_id = ?,
|
|
127
|
+
active_activity_id = ?,
|
|
128
|
+
active_run_started_at = ?,
|
|
129
|
+
latest_run_id = ?,
|
|
130
|
+
latest_run_outcome = ?,
|
|
131
|
+
latest_run_finished_at = ?,
|
|
132
|
+
latest_response = null,
|
|
133
|
+
error = null,
|
|
134
|
+
hook_reports_json = null,
|
|
99
135
|
updated_at = ?
|
|
100
136
|
where id = ?`)
|
|
101
|
-
.run(updated.workspaceId ?? null, resolve(updated.workspaceRoot), updated.profileName, updated.provider, updated.model ?? null, updated.thinking ?? null, updated.providerSessionId ?? null, updated.status, updated.
|
|
137
|
+
.run(updated.workspaceId ?? null, resolve(updated.workspaceRoot), updated.profileName, updated.provider, updated.model ?? null, updated.thinking ?? null, updated.providerSessionId ?? null, updated.status, updated.activeRun?.id ?? null, updated.activeRun?.activityId ?? null, updated.activeRun?.startedAt ?? null, updated.latestRun?.id ?? null, updated.latestRun && updated.latestRun.status !== "running" ? updated.latestRun.status : null, updated.latestRun?.finishedAt ?? null, updated.updatedAt, updated.id);
|
|
102
138
|
return updated;
|
|
103
139
|
}
|
|
104
140
|
close() {
|
|
@@ -108,13 +144,29 @@ export class LocalAgentStore {
|
|
|
108
144
|
const row = this.database.sqlite
|
|
109
145
|
.prepare("select * from local_agent_sessions where id = ?")
|
|
110
146
|
.get(id);
|
|
111
|
-
return row ?
|
|
147
|
+
return row ? rowToSubagentSession(row) : undefined;
|
|
112
148
|
}
|
|
113
149
|
}
|
|
114
|
-
export function
|
|
115
|
-
return new
|
|
150
|
+
export function createSubagentSessionStore(config) {
|
|
151
|
+
return new SubagentSessionStore(config.stateDir);
|
|
116
152
|
}
|
|
117
|
-
function
|
|
153
|
+
function rowToSubagentSession(row) {
|
|
154
|
+
const activeRun = row.active_run_id
|
|
155
|
+
? {
|
|
156
|
+
id: row.active_run_id,
|
|
157
|
+
status: "running",
|
|
158
|
+
...(row.active_activity_id ? { activityId: row.active_activity_id } : {}),
|
|
159
|
+
...(row.active_run_started_at ? { startedAt: row.active_run_started_at } : {}),
|
|
160
|
+
}
|
|
161
|
+
: undefined;
|
|
162
|
+
const latestOutcome = readOutcome(row.latest_run_outcome);
|
|
163
|
+
const latestRun = row.latest_run_id && latestOutcome
|
|
164
|
+
? {
|
|
165
|
+
id: row.latest_run_id,
|
|
166
|
+
status: latestOutcome,
|
|
167
|
+
...(row.latest_run_finished_at ? { finishedAt: row.latest_run_finished_at } : {}),
|
|
168
|
+
}
|
|
169
|
+
: undefined;
|
|
118
170
|
return {
|
|
119
171
|
id: row.id,
|
|
120
172
|
workspaceId: row.workspace_id ?? undefined,
|
|
@@ -124,34 +176,18 @@ function rowToLocalAgentRecord(row) {
|
|
|
124
176
|
model: row.model ?? undefined,
|
|
125
177
|
thinking: row.thinking ?? undefined,
|
|
126
178
|
providerSessionId: row.provider_session_id ?? undefined,
|
|
127
|
-
status:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
hookReports: parseHookReports(row.hook_reports_json),
|
|
179
|
+
status: activeRun || row.status === "starting" || row.status === "running" ? "running" : "idle",
|
|
180
|
+
...(activeRun ? { activeRun } : {}),
|
|
181
|
+
...(latestRun ? { latestRun } : {}),
|
|
131
182
|
createdAt: row.created_at,
|
|
132
183
|
updatedAt: row.updated_at,
|
|
133
184
|
};
|
|
134
185
|
}
|
|
135
|
-
function
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
138
|
-
try {
|
|
139
|
-
const parsed = JSON.parse(value);
|
|
140
|
-
return Array.isArray(parsed) ? parsed : undefined;
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function readStatus(status) {
|
|
147
|
-
if (status === "starting" ||
|
|
148
|
-
status === "running" ||
|
|
149
|
-
status === "idle" ||
|
|
150
|
-
status === "error" ||
|
|
151
|
-
status === "stopped") {
|
|
152
|
-
return status;
|
|
186
|
+
function readOutcome(value) {
|
|
187
|
+
if (value === "succeeded" || value === "failed" || value === "cancelled" || value === "interrupted") {
|
|
188
|
+
return value;
|
|
153
189
|
}
|
|
154
|
-
return
|
|
190
|
+
return undefined;
|
|
155
191
|
}
|
|
156
192
|
function escapeLike(value) {
|
|
157
193
|
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|