@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.1] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `subagent.session resume` 现在基于 provider-native continuation 继续已有 Session;Codex、Claude、OpenCode、Pi 支持真实 continuation,Cursor/Copilot 明确保持 start-only。
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Session 固定创建时的 provider/profile/model/thinking,并严格限制在所属 Execution Workspace;busy resume、跨 Workspace 访问和不受支持的 continuation 都返回明确错误。
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Windows restart mailbox 测试在清理临时 SQLite state 前显式关闭 restored server,避免 `EBUSY`。
|
|
20
|
+
|
|
21
|
+
## [0.7.0] - 2026-08-30
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- 新增 first-class `subagent.session` Capability tracer:Host 可通过现有 Capability Gateway 发现并启动 provider-backed Subagent Session、查询 `status`、列出当前 Execution Workspace 拥有的 Session,并通过有界单次 delivery mailbox 接收后台 Run 的最终结果;canonical Core MCP tool 仍保持九个。
|
|
26
|
+
- 为 ForgeRelay Subagent 引入稳定的 Session / Run identity 和 workspace-scoped coordination,使每次 delegated execution 都有明确的 Run 生命周期、Activity 关联和 provider continuation 映射,而无需经由 `bash -> forgerelay agents ...` CLI 间接执行。
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- 将 Claude Code、Codex、OpenCode、Pi 等 provider 的原生 session store 作为 conversation history 真源;ForgeRelay SQLite 停止写入 delegated prompt、profile body、final response、Hook report history 和 provider event/items,仅保留轻量 ownership、provider/profile/model/thinking 与 active/latest Run 协调元数据。
|
|
31
|
+
- Provider adapter contract 收敛为 continuation identity、final response 与必要执行元数据,provider streaming/messages/events 只瞬时消费;profile body 仅在 Session 首次启动时交给 provider,ForgeRelay 不保存正文副本。
|
|
32
|
+
|
|
7
33
|
## [0.6.2] - 2026-08-29
|
|
8
34
|
|
|
9
35
|
### Added
|
|
@@ -1,69 +1,129 @@
|
|
|
1
|
-
# ForgeRelay
|
|
1
|
+
# ForgeRelay Subagent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
当任务需要委派给另一个本地 coding agent、获取第二意见、并行调查,或用户明确点名 Subagent/provider 时读取本指南。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 当前接口
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
启用 Subagent 后,`open_workspace` 会在 `capabilityCatalog` 中公开 `subagent.session`,同时返回紧凑的 provider/profile 元数据。不要为 Subagent 寻找或假设额外的 Core MCP tool;长期可见 Core tool surface 保持不变。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
通过统一 `capability` gateway 调用:
|
|
10
10
|
|
|
11
|
-
```
|
|
12
|
-
|
|
11
|
+
```text
|
|
12
|
+
name = subagent.session
|
|
13
|
+
action = run
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
当前支持四个 operation:
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
- `start`:创建 Subagent Session,并立即启动第一个 Subagent Run;
|
|
19
|
+
- `resume`:在支持真实 continuation 的 provider 上继续现有 Session;
|
|
20
|
+
- `status`:读取当前 Workspace 中一个 Session 的协调状态;
|
|
21
|
+
- `list`:列出当前 Workspace 拥有的 Session 摘要。
|
|
22
|
+
|
|
23
|
+
`subagent.session` 不支持 `batch.execute`。
|
|
24
|
+
|
|
25
|
+
## start
|
|
26
|
+
|
|
27
|
+
使用 profile:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"operation": "start",
|
|
32
|
+
"target": "reviewer",
|
|
33
|
+
"prompt": "检查当前改动的并发与错误处理。"
|
|
34
|
+
}
|
|
20
35
|
```
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
也可以把 ForgeRelay 支持的 provider 名直接作为 `target`。创建新 Session 时可以显式给出 `model` / `thinking`;profile 已有默认值时通常无需重复覆盖。
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
返回值包含:
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
- `session.id`:ForgeRelay 的 Subagent Session identity;
|
|
42
|
+
- `session.status`:`running` 或 `idle`;
|
|
43
|
+
- `run.id`:本次 delegated execution 的 Subagent Run identity;
|
|
44
|
+
- provider/profile/model 等紧凑协调元数据。
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
首次 `start` 使用当时有效的 profile body 作为 provider-native conversation 的初始 instructions。ForgeRelay 不把 profile body 复制进自己的 Session SQLite。
|
|
29
47
|
|
|
30
|
-
##
|
|
48
|
+
## resume
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
已有 Session 完成一次 Run 后,可发送新的 delegated prompt:
|
|
33
51
|
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"operation": "resume",
|
|
55
|
+
"sessionId": "agt_...",
|
|
56
|
+
"prompt": "继续检查测试覆盖。"
|
|
57
|
+
}
|
|
38
58
|
```
|
|
39
59
|
|
|
40
|
-
|
|
60
|
+
`resume` 只接受 `sessionId + prompt`,不能改变 model/thinking。Session 创建时确定的 provider/profile/model/thinking 在整个 Session 生命周期中保持固定;resume 不重新读取或注入当前 profile body,而是使用 provider-native continuation history。
|
|
41
61
|
|
|
42
|
-
- `
|
|
43
|
-
- `run <provider>`:没有合适 profile 时直接启动 ForgeRelay 内建 provider;
|
|
44
|
-
- `run <id>`:向现有 agent session 发送 follow-up;
|
|
45
|
-
- `show <id>`:查看状态和最新响应;仍在运行时可稍后再次调用;
|
|
46
|
-
- shell workspace 环境会把 CLI 操作自动限定到当前 ForgeRelay workspace。
|
|
62
|
+
同一个 Session 最多只有一个 active Run;Session 正在执行时再次 resume 会返回 `subagent.busy`,不会排队或并发执行。Codex、Claude、OpenCode、Pi 支持真实 continuation;Cursor/Copilot 当前明确标记为 start-only,resume 返回 `subagent.continuation_unsupported`,不会伪装成新 provider session。
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
## status / list
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
查询一个 Session:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"operation": "status",
|
|
71
|
+
"sessionId": "agt_..."
|
|
72
|
+
}
|
|
53
73
|
```
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
列出当前 Workspace 的 Session:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"operation": "list"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Session 受实际 Execution Workspace 所有权约束;Session ID 不是跨 Workspace 的访问凭证。`status` / `resume` 都只能访问当前实际 Execution Workspace 拥有的 Session,`list` 也只返回当前 Workspace 的紧凑摘要。
|
|
84
|
+
|
|
85
|
+
Session 返回中会给出 `continuationSupported` 与 `resumable`;`open_workspace` 的 provider metadata 也会明确给出 `continuationSupported`。当前尚未通过 first-class Capability 开放 `stop` 或 `delete`,不要伪造这些 operation,也不要用新的 Core MCP tool 绕过 Capability Gateway。
|
|
86
|
+
|
|
87
|
+
## 后台完成与结果交付
|
|
88
|
+
|
|
89
|
+
`start` 接受执行后会尽快把 Session/Run identity 返回给 Host,Subagent Run 在后台继续执行。
|
|
90
|
+
|
|
91
|
+
Run 完成后:
|
|
92
|
+
|
|
93
|
+
- ForgeRelay 创建一个 linked `subagent_result` Activity,只记录 Session/Run/provider/status 等有界元数据;
|
|
94
|
+
- final response 放入有界 delivery mailbox,等待同一 Workspace 的后续 ForgeRelay 调用领取;
|
|
95
|
+
- 成功领取后 mailbox 条目立即删除,同一个 completion 不重复交付;
|
|
96
|
+
- 未领取 completion 可以跨正常 ForgeRelay 进程重启保留。
|
|
97
|
+
|
|
98
|
+
如果需要等待结果,使用 `status` 进行有节制的后续查询;不要高频短轮询。
|
|
99
|
+
|
|
100
|
+
## 数据所有权
|
|
101
|
+
|
|
102
|
+
Claude Code、Codex、OpenCode、Pi 等 provider 自己的 session store 是 conversation history 的真源。ForgeRelay 只保存恢复、Workspace ownership 和执行协调所需的小型元数据。
|
|
103
|
+
|
|
104
|
+
ForgeRelay 不把以下内容保存进 Subagent Session SQLite 或 Activity Audit:
|
|
105
|
+
|
|
106
|
+
- delegated prompt;
|
|
107
|
+
- profile body;
|
|
108
|
+
- provider transcript/messages/events/items;
|
|
109
|
+
- final response 正文;
|
|
110
|
+
- 完整 Hook report 历史;
|
|
111
|
+
- 完整 Subagent Run 历史。
|
|
112
|
+
|
|
113
|
+
Activity 只保留操作摘要;delivery mailbox 只用于尚未交付的 bounded completion,不是 conversation/history store。
|
|
114
|
+
|
|
115
|
+
## Hooks
|
|
56
116
|
|
|
57
|
-
|
|
117
|
+
`SubagentStart` / `SubagentStop` 在每一次 Subagent Run 生命周期中触发。Hook payload 使用 Session/Run/profile/provider/model/thinking/status 等协调元数据,不包含 delegated prompt 或 final response。
|
|
58
118
|
|
|
59
|
-
##
|
|
119
|
+
## CLI 兼容
|
|
60
120
|
|
|
61
|
-
|
|
121
|
+
`forgerelay agents` 仍可用于本地诊断和兼容性操作,但 Host 正常委派优先使用 `subagent.session` Capability,不要通过 `bash -> forgerelay agents ...` 实现 first-class Subagent。
|
|
62
122
|
|
|
63
|
-
|
|
123
|
+
## 验证责任
|
|
64
124
|
|
|
65
|
-
|
|
66
|
-
- 只读调查:核对关键结论是否有仓库证据;
|
|
67
|
-
- 向用户说明使用了哪个 profile/provider、它给出的结论、你做了哪些验证,以及剩余风险。
|
|
125
|
+
Subagent 返回不是自动验证过的最终结论。收到结果后仍应按照任务类型验证:
|
|
68
126
|
|
|
69
|
-
|
|
127
|
+
- 写入型任务检查真实 diff,并运行相关测试;
|
|
128
|
+
- 只读调查核对关键结论对应的仓库事实;
|
|
129
|
+
- 向用户说明使用的 profile/provider、核心结论、已做验证与剩余风险。
|
|
@@ -32,7 +32,7 @@ export class ActivityLifecycle {
|
|
|
32
32
|
const activityId = executionContext.activityId;
|
|
33
33
|
try {
|
|
34
34
|
const result = await options.operation(executionContext);
|
|
35
|
-
this.finish(activityId, result, options.outcome?.(result) ?? { type: "succeeded" });
|
|
35
|
+
this.finish(activityId, options.auditResult?.(result) ?? result, options.outcome?.(result) ?? { type: "succeeded" });
|
|
36
36
|
return result;
|
|
37
37
|
}
|
|
38
38
|
catch (error) {
|
package/dist/capabilities.js
CHANGED
|
@@ -92,7 +92,7 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
|
|
|
92
92
|
capabilities.push("batch.execute");
|
|
93
93
|
}
|
|
94
94
|
if (config.subagents) {
|
|
95
|
-
capabilities.push("subagent.
|
|
95
|
+
capabilities.push("subagent.session");
|
|
96
96
|
}
|
|
97
97
|
if (config.artifactsEnabled && context.artifactDownloadSupported) {
|
|
98
98
|
capabilities.push("artifact.native-download");
|
|
@@ -130,6 +130,25 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
130
130
|
line: z.number().int(),
|
|
131
131
|
column: z.number().int(),
|
|
132
132
|
};
|
|
133
|
+
const subagentSessionInput = z.discriminatedUnion("operation", [
|
|
134
|
+
z.object({
|
|
135
|
+
operation: z.literal("start"),
|
|
136
|
+
target: z.string().min(1),
|
|
137
|
+
prompt: z.string().min(1),
|
|
138
|
+
model: z.string().min(1).optional(),
|
|
139
|
+
thinking: z.string().min(1).optional(),
|
|
140
|
+
}).strict(),
|
|
141
|
+
z.object({
|
|
142
|
+
operation: z.literal("resume"),
|
|
143
|
+
sessionId: z.string().min(1),
|
|
144
|
+
prompt: z.string().min(1),
|
|
145
|
+
}).strict(),
|
|
146
|
+
z.object({
|
|
147
|
+
operation: z.literal("status"),
|
|
148
|
+
sessionId: z.string().min(1),
|
|
149
|
+
}).strict(),
|
|
150
|
+
z.object({ operation: z.literal("list") }).strict(),
|
|
151
|
+
]);
|
|
133
152
|
const codeIntelligenceInput = z.discriminatedUnion("operation", [
|
|
134
153
|
z.object({ operation: z.literal("definition"), ...positionInput }).strict(),
|
|
135
154
|
z.object({ operation: z.literal("hover"), ...positionInput }).strict(),
|
|
@@ -201,6 +220,21 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
201
220
|
run: async (input, context, options) => dependencies.codeIntelligence.run(input, context, options),
|
|
202
221
|
}]
|
|
203
222
|
: []),
|
|
223
|
+
...(dependencies.subagentSession
|
|
224
|
+
? [{
|
|
225
|
+
name: "subagent.session",
|
|
226
|
+
description: "Coordinate provider-backed Subagent Sessions in the current Execution Workspace.",
|
|
227
|
+
guideName: "subagents",
|
|
228
|
+
readGuideBeforeFirstUse: true,
|
|
229
|
+
batchPolicy: "unsupported",
|
|
230
|
+
inputSchema: subagentSessionInput,
|
|
231
|
+
availability: () => ({
|
|
232
|
+
available: dependencies.subagentSession?.available ?? false,
|
|
233
|
+
reason: dependencies.subagentSession?.unavailableReason,
|
|
234
|
+
}),
|
|
235
|
+
run: async (input, context, options) => dependencies.subagentSession.run(input, context, options),
|
|
236
|
+
}]
|
|
237
|
+
: []),
|
|
204
238
|
...(dependencies.batchExecute
|
|
205
239
|
? [{
|
|
206
240
|
name: "batch.execute",
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { readFile, rm } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,13 +11,12 @@ import * as prompts from "@clack/prompts";
|
|
|
11
11
|
import { getShellConfig } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { satisfies } from "semver";
|
|
13
13
|
import { loadConfig } from "./config.js";
|
|
14
|
-
import { formatVisibleHookReports, HookRunner } from "./hooks.js";
|
|
15
14
|
import { runHooksCommand } from "./hook-cli.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
15
|
+
import { executeSubagentSession } from "./subagents/sessions/execution.js";
|
|
16
|
+
import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.js";
|
|
17
|
+
import { SubagentSessionManager } from "./subagents/sessions/manager.js";
|
|
18
|
+
import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
|
|
19
|
+
import { parseSubagentRunArgs } from "./subagents/cli-target.js";
|
|
21
20
|
import { ensureForgeRelayInstanceId, generateInstanceId, generateOwnerToken, loadDevspaceFiles, removeForgeRelayRemote, renameForgeRelayRemote, resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, writeForgeRelayRemote, } from "./user-config.js";
|
|
22
21
|
import { expandHomePath } from "./roots.js";
|
|
23
22
|
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
@@ -193,7 +192,7 @@ async function serve() {
|
|
|
193
192
|
}
|
|
194
193
|
const { createServer } = await import("./server.js");
|
|
195
194
|
const config = loadConfig();
|
|
196
|
-
const { app, close,
|
|
195
|
+
const { app, close, subagentProviders } = createServer(config);
|
|
197
196
|
const httpServer = app.listen(config.port, config.host, () => {
|
|
198
197
|
console.log(`forgerelay listening on http://${config.host}:${config.port}/mcp`);
|
|
199
198
|
console.log(`public base url: ${config.publicBaseUrl}`);
|
|
@@ -205,7 +204,7 @@ async function serve() {
|
|
|
205
204
|
console.log("auth: Owner password approval required");
|
|
206
205
|
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
207
206
|
if (config.subagents) {
|
|
208
|
-
console.log(`subagent providers: ${
|
|
207
|
+
console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
|
|
209
208
|
}
|
|
210
209
|
});
|
|
211
210
|
let shuttingDown = false;
|
|
@@ -492,92 +491,79 @@ async function runAgentsCommand(args) {
|
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
493
|
async function runAgentsList() {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
494
|
+
const manager = createCliSubagentSessionManager();
|
|
495
|
+
try {
|
|
496
|
+
const agents = manager.list(resolveCurrentWorkspaceScope());
|
|
497
|
+
if (agents.length === 0) {
|
|
498
|
+
console.log("No subagent sessions found for this workspace.");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
for (const agent of agents) {
|
|
502
|
+
console.log(formatAgentLine(agent));
|
|
503
|
+
}
|
|
501
504
|
}
|
|
502
|
-
|
|
503
|
-
|
|
505
|
+
finally {
|
|
506
|
+
manager.close();
|
|
504
507
|
}
|
|
505
508
|
}
|
|
506
509
|
async function runAgentsRun(args) {
|
|
507
|
-
const parsed =
|
|
508
|
-
const config = loadConfig();
|
|
510
|
+
const parsed = parseSubagentRunArgs(args);
|
|
509
511
|
const workspaceRoot = resolveCurrentWorkspaceRoot();
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
throw new Error(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
status: "running",
|
|
530
|
-
model: parsed.model ?? existing.model,
|
|
531
|
-
thinking: parsed.thinking ?? existing.thinking,
|
|
532
|
-
}));
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const profiles = await loadLocalAgentProfiles(config, workspaceRoot);
|
|
536
|
-
const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model, parsed.thinking);
|
|
537
|
-
if (!target) {
|
|
538
|
-
throw new Error(`Unknown subagent profile, provider, or id: ${parsed.target}. Available ${formatAvailableLocalAgentTargets(profiles)}`);
|
|
512
|
+
const manager = createCliSubagentSessionManager();
|
|
513
|
+
try {
|
|
514
|
+
const existing = manager.get(parsed.target);
|
|
515
|
+
if (existing && (parsed.model || parsed.thinking))
|
|
516
|
+
throw new Error("Existing Subagent Sessions cannot override model or thinking.");
|
|
517
|
+
const started = existing
|
|
518
|
+
? manager.resume({ sessionId: existing.id, prompt: parsed.prompt })
|
|
519
|
+
: await manager.start({
|
|
520
|
+
workspaceId: process.env.FORGERELAY_WORKSPACE_ID ?? process.env.DEVSPACE_WORKSPACE_ID,
|
|
521
|
+
workspaceRoot,
|
|
522
|
+
target: parsed.target,
|
|
523
|
+
prompt: parsed.prompt,
|
|
524
|
+
model: parsed.model,
|
|
525
|
+
thinking: parsed.thinking,
|
|
526
|
+
});
|
|
527
|
+
console.log(formatAgentLine(started.session));
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
manager.close();
|
|
539
531
|
}
|
|
540
|
-
assertLocalAgentProviderAvailable(target.provider);
|
|
541
|
-
const promptFile = writeAgentPromptFile(parsed.prompt);
|
|
542
|
-
const record = store.create({
|
|
543
|
-
workspaceId: process.env.FORGERELAY_WORKSPACE_ID ?? process.env.DEVSPACE_WORKSPACE_ID,
|
|
544
|
-
workspaceRoot,
|
|
545
|
-
profileName: target.name,
|
|
546
|
-
provider: target.provider,
|
|
547
|
-
model: target.model,
|
|
548
|
-
thinking: target.thinking,
|
|
549
|
-
});
|
|
550
|
-
spawnAgentWorker(record.id, promptFile);
|
|
551
|
-
console.log(formatAgentLine({ ...record, status: "running" }));
|
|
552
532
|
}
|
|
553
533
|
async function runAgentsShow(args) {
|
|
554
534
|
const [id] = args;
|
|
555
535
|
if (!id)
|
|
556
536
|
throw new Error("Usage: forgerelay agents show <id>");
|
|
557
537
|
const config = loadConfig();
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
538
|
+
const manager = createCliSubagentSessionManager(config);
|
|
539
|
+
try {
|
|
540
|
+
let record = manager.get(id);
|
|
541
|
+
if (!record)
|
|
542
|
+
throw new Error(`Unknown subagent id: ${id}`);
|
|
543
|
+
const deadline = Date.now() + 15_000;
|
|
544
|
+
while (record.status === "running" && Date.now() < deadline) {
|
|
545
|
+
await sleep(500);
|
|
546
|
+
record = manager.get(id) ?? record;
|
|
547
|
+
}
|
|
548
|
+
console.log(formatAgentLine(record));
|
|
549
|
+
if (record.workspaceId) {
|
|
550
|
+
const deliveries = new SubagentDeliveryMailbox(config.stateDir).claimSession(record.workspaceId, record.id);
|
|
551
|
+
for (const delivery of deliveries) {
|
|
552
|
+
const text = delivery.outcome === "succeeded" ? delivery.finalResponse : delivery.error;
|
|
553
|
+
if (text)
|
|
554
|
+
console.log(text);
|
|
555
|
+
}
|
|
556
|
+
if (deliveries.length > 0)
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (record.latestRun)
|
|
560
|
+
console.log(`Latest run ${record.latestRun.id}: ${record.latestRun.status}`);
|
|
561
|
+
if (record.status === "running") {
|
|
562
|
+
console.log(`No final response yet. Call \`forgerelay agents show ${record.id}\` again later.`);
|
|
563
|
+
}
|
|
578
564
|
}
|
|
579
|
-
|
|
580
|
-
|
|
565
|
+
finally {
|
|
566
|
+
manager.close();
|
|
581
567
|
}
|
|
582
568
|
}
|
|
583
569
|
async function runAgentsWorker(args) {
|
|
@@ -586,96 +572,25 @@ async function runAgentsWorker(args) {
|
|
|
586
572
|
throw new Error("Usage: forgerelay agents __worker <id> --prompt-file <path>");
|
|
587
573
|
}
|
|
588
574
|
const config = loadConfig();
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
agentId: record.id,
|
|
599
|
-
profile: record.profileName,
|
|
600
|
-
provider: record.provider,
|
|
601
|
-
model: record.model,
|
|
602
|
-
thinking: record.thinking,
|
|
575
|
+
const prompt = await readFile(promptFile, "utf8");
|
|
576
|
+
await rm(promptFile, { force: true });
|
|
577
|
+
await executeSubagentSession(config, id, prompt);
|
|
578
|
+
}
|
|
579
|
+
function createCliSubagentSessionManager(config = loadConfig()) {
|
|
580
|
+
return new SubagentSessionManager(config, {
|
|
581
|
+
launch(request) {
|
|
582
|
+
const promptFile = writeSubagentPromptFile(request.prompt);
|
|
583
|
+
spawnSubagentWorker(request.sessionId, promptFile);
|
|
603
584
|
},
|
|
604
|
-
};
|
|
605
|
-
store.update(record.id, { status: "running", error: undefined, hookReports: undefined });
|
|
606
|
-
const hookReports = await hooks.run("SubagentStart", hookInvocation);
|
|
607
|
-
store.update(record.id, { hookReports });
|
|
608
|
-
try {
|
|
609
|
-
const profiles = await loadLocalAgentProfiles(config, record.workspaceRoot);
|
|
610
|
-
const profile = profiles.find((candidate) => candidate.name === record.profileName);
|
|
611
|
-
const prompt = await readFile(promptFile, "utf8");
|
|
612
|
-
const result = profile
|
|
613
|
-
? await runLocalAgentProfile(profile, record, prompt)
|
|
614
|
-
: await runRawLocalAgentProvider(record, prompt);
|
|
615
|
-
hookReports.push(...await hooks.run("SubagentStop", {
|
|
616
|
-
...hookInvocation,
|
|
617
|
-
payload: {
|
|
618
|
-
...hookInvocation.payload,
|
|
619
|
-
status: "idle",
|
|
620
|
-
providerSessionId: result.providerSessionId,
|
|
621
|
-
},
|
|
622
|
-
}));
|
|
623
|
-
store.update(record.id, {
|
|
624
|
-
providerSessionId: result.providerSessionId ?? undefined,
|
|
625
|
-
status: "idle",
|
|
626
|
-
latestResponse: result.finalResponse,
|
|
627
|
-
error: undefined,
|
|
628
|
-
hookReports,
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
catch (error) {
|
|
632
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
-
hookReports.push(...await hooks.run("SubagentStop", {
|
|
634
|
-
...hookInvocation,
|
|
635
|
-
payload: {
|
|
636
|
-
...hookInvocation.payload,
|
|
637
|
-
status: "error",
|
|
638
|
-
},
|
|
639
|
-
}));
|
|
640
|
-
store.update(record.id, {
|
|
641
|
-
status: "error",
|
|
642
|
-
error: message,
|
|
643
|
-
hookReports,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
async function runLocalAgentProfile(profile, record, prompt) {
|
|
648
|
-
const body = profile.body.trim();
|
|
649
|
-
const fullPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
|
|
650
|
-
return runLocalAgentProvider(profile.provider, {
|
|
651
|
-
prompt: fullPrompt,
|
|
652
|
-
workspace: record.workspaceRoot,
|
|
653
|
-
providerSessionId: record.providerSessionId,
|
|
654
|
-
writeMode: "allowed",
|
|
655
|
-
model: record.model ?? profile.model,
|
|
656
|
-
thinking: record.thinking ?? profile.thinking,
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
async function runRawLocalAgentProvider(record, prompt) {
|
|
660
|
-
if (record.profileName !== record.provider || !isLocalAgentProvider(record.provider)) {
|
|
661
|
-
throw new Error(`Subagent profile not found: ${record.profileName}`);
|
|
662
|
-
}
|
|
663
|
-
return runLocalAgentProvider(record.provider, {
|
|
664
|
-
prompt,
|
|
665
|
-
workspace: record.workspaceRoot,
|
|
666
|
-
providerSessionId: record.providerSessionId,
|
|
667
|
-
writeMode: "allowed",
|
|
668
|
-
model: record.model,
|
|
669
|
-
thinking: record.thinking,
|
|
670
585
|
});
|
|
671
586
|
}
|
|
672
|
-
function
|
|
587
|
+
function spawnSubagentWorker(sessionId, promptFile) {
|
|
673
588
|
const child = spawn(process.execPath, [
|
|
674
589
|
...process.execArgv,
|
|
675
590
|
fileURLToPath(import.meta.url),
|
|
676
591
|
"agents",
|
|
677
592
|
"__worker",
|
|
678
|
-
|
|
593
|
+
sessionId,
|
|
679
594
|
"--prompt-file",
|
|
680
595
|
promptFile,
|
|
681
596
|
], {
|
|
@@ -685,7 +600,7 @@ function spawnAgentWorker(agentId, promptFile) {
|
|
|
685
600
|
});
|
|
686
601
|
child.unref();
|
|
687
602
|
}
|
|
688
|
-
function
|
|
603
|
+
function writeSubagentPromptFile(prompt) {
|
|
689
604
|
const directory = mkdtempSync(join(tmpdir(), "forgerelay-agent-prompt-"));
|
|
690
605
|
const filePath = join(directory, "prompt.txt");
|
|
691
606
|
writeFileSync(filePath, prompt, { mode: 0o600 });
|
package/dist/db/migrations.js
CHANGED
|
@@ -64,6 +64,11 @@ const migrations = [
|
|
|
64
64
|
name: "activity-host-turn-workspace",
|
|
65
65
|
up: migrateActivityHostTurnWorkspace,
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
version: 14,
|
|
69
|
+
name: "subagent-session-coordination",
|
|
70
|
+
up: migrateSubagentSessionCoordination,
|
|
71
|
+
},
|
|
67
72
|
];
|
|
68
73
|
export function migrateDatabase(sqlite) {
|
|
69
74
|
const migrate = sqlite.transaction(() => {
|
|
@@ -344,6 +349,15 @@ function migrateBashOutputAuditColumns(sqlite) {
|
|
|
344
349
|
addColumnIfMissing(sqlite, "bash_output_streams", "returned", "integer not null default 0");
|
|
345
350
|
addColumnIfMissing(sqlite, "bash_output_streams", "completion_claimed_at", "text");
|
|
346
351
|
}
|
|
352
|
+
function migrateSubagentSessionCoordination(sqlite) {
|
|
353
|
+
migrateLocalAgentSessions(sqlite);
|
|
354
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "active_run_id", "text");
|
|
355
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "active_activity_id", "text");
|
|
356
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "active_run_started_at", "text");
|
|
357
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "latest_run_id", "text");
|
|
358
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "latest_run_outcome", "text");
|
|
359
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "latest_run_finished_at", "text");
|
|
360
|
+
}
|
|
347
361
|
function migrateActivityHostTurnWorkspace(sqlite) {
|
|
348
362
|
migrateActivityHostTurns(sqlite);
|
|
349
363
|
addColumnIfMissing(sqlite, "activity_host_turns", "workspace_id", "text");
|
package/dist/db/schema.js
CHANGED
|
@@ -152,6 +152,12 @@ export const localAgentSessions = sqliteTable("local_agent_sessions", {
|
|
|
152
152
|
thinking: text("thinking"),
|
|
153
153
|
providerSessionId: text("provider_session_id"),
|
|
154
154
|
status: text("status").notNull(),
|
|
155
|
+
activeRunId: text("active_run_id"),
|
|
156
|
+
activeActivityId: text("active_activity_id"),
|
|
157
|
+
activeRunStartedAt: text("active_run_started_at"),
|
|
158
|
+
latestRunId: text("latest_run_id"),
|
|
159
|
+
latestRunOutcome: text("latest_run_outcome"),
|
|
160
|
+
latestRunFinishedAt: text("latest_run_finished_at"),
|
|
155
161
|
latestResponse: text("latest_response"),
|
|
156
162
|
error: text("error"),
|
|
157
163
|
hookReportsJson: text("hook_reports_json"),
|