agent-anywhere-gateway 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/README.md +46 -0
- package/bin/agent-anywhere-gateway.js +13 -0
- package/config/cloudflare.gateway.env.example +9 -0
- package/package.json +29 -0
- package/src/adapters/local-agent-adapter.js +131 -0
- package/src/gateway/client.js +422 -0
- package/src/gateway/main.js +224 -0
- package/src/gateway/providers.js +28 -0
- package/src/gateway/runner.js +337 -0
- package/src/gateway.js +7 -0
- package/src/lib/capabilities.js +1 -0
- package/src/lib/local-discovery.js +322 -0
- package/src/lib/path-policy.js +1 -0
- package/src/runtimes/claude-code-headless-runtime.js +547 -0
- package/src/runtimes/claude-code-runtime.js +984 -0
- package/src/runtimes/codex-app-server-client.js +157 -0
- package/src/runtimes/codex-app-server-runtime.js +790 -0
- package/src/runtimes/codex-runtime.js +418 -0
- package/src/runtimes/mock-runtime.js +140 -0
- package/src/shared/capabilities.js +175 -0
- package/src/shared/gateway-protocol.js +26 -0
- package/src/shared/http-utils.js +78 -0
- package/src/shared/image-attachments.js +269 -0
- package/src/shared/path-policy.js +110 -0
- package/src/shared/project-files.js +119 -0
- package/src/shared/providers.js +27 -0
- package/src/shared/runtime-environment.js +32 -0
- package/src/shared/websocket.js +258 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const {
|
|
2
|
+
gatewayId,
|
|
3
|
+
registerMachine,
|
|
4
|
+
registerUrl,
|
|
5
|
+
serverWsUrl,
|
|
6
|
+
startGateway
|
|
7
|
+
} = require("./client");
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
10
|
+
const options = {};
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const arg = argv[index];
|
|
13
|
+
const readValue = (name) => {
|
|
14
|
+
const value = argv[index + 1];
|
|
15
|
+
if (!value || value.startsWith("--")) {
|
|
16
|
+
throw new Error(`${name} requires a value`);
|
|
17
|
+
}
|
|
18
|
+
index += 1;
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (arg === "--help" || arg === "-h") {
|
|
23
|
+
options.help = true;
|
|
24
|
+
} else if (arg === "--server" || arg === "--worker-url") {
|
|
25
|
+
options.server = readValue(arg);
|
|
26
|
+
} else if (arg === "--server-ws-url") {
|
|
27
|
+
options.serverWsUrl = readValue(arg);
|
|
28
|
+
} else if (arg === "--id") {
|
|
29
|
+
options.id = readValue(arg);
|
|
30
|
+
} else if (arg === "--name") {
|
|
31
|
+
options.name = readValue(arg);
|
|
32
|
+
} else if (arg === "--token") {
|
|
33
|
+
options.token = readValue(arg);
|
|
34
|
+
} else if (arg === "--allowed-roots") {
|
|
35
|
+
options.allowedRoots = readValue(arg);
|
|
36
|
+
} else if (arg === "--provider") {
|
|
37
|
+
options.provider = readValue(arg);
|
|
38
|
+
} else if (arg === "--providers") {
|
|
39
|
+
options.providers = readValue(arg);
|
|
40
|
+
} else if (arg === "--data-dir") {
|
|
41
|
+
options.dataDir = readValue(arg);
|
|
42
|
+
} else if (arg === "--replay-dir") {
|
|
43
|
+
options.replayDir = readValue(arg);
|
|
44
|
+
} else if (arg === "--reconnect-min-ms") {
|
|
45
|
+
options.reconnectMinMs = readValue(arg);
|
|
46
|
+
} else if (arg === "--reconnect-max-ms") {
|
|
47
|
+
options.reconnectMaxMs = readValue(arg);
|
|
48
|
+
} else if (arg === "--dry-run") {
|
|
49
|
+
options.dryRun = true;
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return options;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeGatewayId(value) {
|
|
58
|
+
return String(value || gatewayId())
|
|
59
|
+
.trim()
|
|
60
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-")
|
|
61
|
+
.replace(/^-+|-+$/g, "") || "gateway";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toGatewayWsUrl(serverUrl, id) {
|
|
65
|
+
const url = new URL(serverUrl);
|
|
66
|
+
if (url.protocol === "http:") {
|
|
67
|
+
url.protocol = "ws:";
|
|
68
|
+
} else if (url.protocol === "https:") {
|
|
69
|
+
url.protocol = "wss:";
|
|
70
|
+
}
|
|
71
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
72
|
+
throw new Error("--server must be an http(s) or ws(s) URL");
|
|
73
|
+
}
|
|
74
|
+
if (!url.pathname || url.pathname === "/") {
|
|
75
|
+
url.pathname = `/api/gateways/${encodeURIComponent(id)}/ws`;
|
|
76
|
+
}
|
|
77
|
+
return url.toString();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveServerWsUrl(env, id) {
|
|
81
|
+
const raw = env.AGENT_ANYWHERE_SERVER_WS_URL || `ws://localhost:8787/api/gateways/${encodeURIComponent(id)}/ws`;
|
|
82
|
+
const url = new URL(raw.replace("{gateway_id}", encodeURIComponent(id)));
|
|
83
|
+
if (!url.searchParams.has("token") && env.AGENT_ANYWHERE_GATEWAY_TOKEN) {
|
|
84
|
+
url.searchParams.set("token", env.AGENT_ANYWHERE_GATEWAY_TOKEN);
|
|
85
|
+
}
|
|
86
|
+
return url.toString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function applyCliOptions(options = {}, env = process.env) {
|
|
90
|
+
const id = normalizeGatewayId(options.id || env.AGENT_ANYWHERE_GATEWAY_ID);
|
|
91
|
+
if (options.id) {
|
|
92
|
+
env.AGENT_ANYWHERE_GATEWAY_ID = id;
|
|
93
|
+
}
|
|
94
|
+
if (options.name) {
|
|
95
|
+
env.AGENT_ANYWHERE_MACHINE_NAME = options.name;
|
|
96
|
+
}
|
|
97
|
+
if (options.token) {
|
|
98
|
+
env.AGENT_ANYWHERE_GATEWAY_TOKEN = options.token;
|
|
99
|
+
}
|
|
100
|
+
if (options.allowedRoots) {
|
|
101
|
+
env.AGENT_ANYWHERE_ALLOWED_ROOTS = options.allowedRoots;
|
|
102
|
+
}
|
|
103
|
+
if (options.providers) {
|
|
104
|
+
env.AGENT_PROVIDERS = options.providers;
|
|
105
|
+
}
|
|
106
|
+
if (options.provider) {
|
|
107
|
+
if (options.provider.includes(",")) {
|
|
108
|
+
env.AGENT_PROVIDERS = options.provider;
|
|
109
|
+
} else {
|
|
110
|
+
env.AGENT_PROVIDER = options.provider;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (options.dataDir) {
|
|
114
|
+
env.AGENT_ANYWHERE_DATA_DIR = options.dataDir;
|
|
115
|
+
}
|
|
116
|
+
if (options.replayDir) {
|
|
117
|
+
env.AGENT_ANYWHERE_GATEWAY_REPLAY_DIR = options.replayDir;
|
|
118
|
+
}
|
|
119
|
+
if (options.reconnectMinMs) {
|
|
120
|
+
env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MIN_MS = options.reconnectMinMs;
|
|
121
|
+
}
|
|
122
|
+
if (options.reconnectMaxMs) {
|
|
123
|
+
env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MAX_MS = options.reconnectMaxMs;
|
|
124
|
+
}
|
|
125
|
+
if (options.serverWsUrl) {
|
|
126
|
+
env.AGENT_ANYWHERE_SERVER_WS_URL = options.serverWsUrl;
|
|
127
|
+
} else if (options.server) {
|
|
128
|
+
env.AGENT_ANYWHERE_SERVER_WS_URL = toGatewayWsUrl(options.server, id);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
name: env.AGENT_ANYWHERE_MACHINE_NAME || id,
|
|
133
|
+
provider: env.AGENT_PROVIDERS || env.AGENT_ANYWHERE_PROVIDERS || env.AGENT_PROVIDER || "mock",
|
|
134
|
+
allowedRoots: env.AGENT_ANYWHERE_ALLOWED_ROOTS || "",
|
|
135
|
+
serverWsUrl: resolveServerWsUrl(env, id),
|
|
136
|
+
hasToken: Boolean(env.AGENT_ANYWHERE_GATEWAY_TOKEN)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function helpText() {
|
|
141
|
+
return [
|
|
142
|
+
"Usage: agent-anywhere-gateway [options]",
|
|
143
|
+
"",
|
|
144
|
+
"Starts an Agent Anywhere Gateway on the controlled machine.",
|
|
145
|
+
"",
|
|
146
|
+
"Options:",
|
|
147
|
+
" --server <url> Control Server or Worker URL, http(s) or ws(s)",
|
|
148
|
+
" --server-ws-url <url> Explicit gateway WebSocket URL",
|
|
149
|
+
" --id <id> Stable gateway machine id",
|
|
150
|
+
" --name <name> Human-readable machine name",
|
|
151
|
+
" --token <token> Shared gateway token",
|
|
152
|
+
" --allowed-roots <paths> Comma-separated allowed project roots",
|
|
153
|
+
" --provider <provider> Local provider, default mock; comma-separated values enable multiple providers",
|
|
154
|
+
" --providers <providers> Comma-separated local providers for one gateway",
|
|
155
|
+
" --data-dir <path> Gateway data directory",
|
|
156
|
+
" --replay-dir <path> Offline stream replay directory",
|
|
157
|
+
" --reconnect-min-ms <ms> Minimum reconnect delay",
|
|
158
|
+
" --reconnect-max-ms <ms> Maximum reconnect delay",
|
|
159
|
+
" --dry-run Print resolved config and exit",
|
|
160
|
+
" --help Show this help",
|
|
161
|
+
"",
|
|
162
|
+
"Environment:",
|
|
163
|
+
" AGENT_ANYWHERE_SERVER_WS_URL Control Server WebSocket URL",
|
|
164
|
+
" AGENT_ANYWHERE_GATEWAY_ID Stable gateway machine id",
|
|
165
|
+
" AGENT_ANYWHERE_GATEWAY_TOKEN Shared gateway token",
|
|
166
|
+
" AGENT_ANYWHERE_ALLOWED_ROOTS Comma-separated allowed project roots",
|
|
167
|
+
" AGENT_PROVIDER Local provider, default mock",
|
|
168
|
+
" AGENT_PROVIDERS Comma-separated local providers for one gateway"
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function printPlan(plan, log = console.log) {
|
|
173
|
+
log(`Gateway id: ${plan.id}`);
|
|
174
|
+
log(`Machine name: ${plan.name}`);
|
|
175
|
+
log(`Server WS URL: ${plan.serverWsUrl}`);
|
|
176
|
+
log(`Allowed roots: ${plan.allowedRoots || "(default)"}`);
|
|
177
|
+
log(`Provider: ${plan.provider}`);
|
|
178
|
+
log(`Gateway token: ${plan.hasToken ? "(set)" : "(default or missing)"}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function runCli(argv = process.argv.slice(2), env = process.env) {
|
|
182
|
+
const options = parseArgs(argv);
|
|
183
|
+
if (options.help) {
|
|
184
|
+
console.log(helpText());
|
|
185
|
+
return Promise.resolve();
|
|
186
|
+
}
|
|
187
|
+
const plan = applyCliOptions(options, env);
|
|
188
|
+
if (options.dryRun) {
|
|
189
|
+
printPlan(plan);
|
|
190
|
+
return Promise.resolve();
|
|
191
|
+
}
|
|
192
|
+
return startGateway().catch((error) => {
|
|
193
|
+
console.error(error.stack || error.message);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (require.main === module) {
|
|
199
|
+
try {
|
|
200
|
+
Promise.resolve(runCli()).catch((error) => {
|
|
201
|
+
console.error(error.stack || error.message);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(error.stack || error.message);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
applyCliOptions,
|
|
212
|
+
gatewayId,
|
|
213
|
+
helpText,
|
|
214
|
+
normalizeGatewayId,
|
|
215
|
+
parseArgs,
|
|
216
|
+
printPlan,
|
|
217
|
+
registerMachine,
|
|
218
|
+
registerUrl,
|
|
219
|
+
resolveServerWsUrl,
|
|
220
|
+
runCli,
|
|
221
|
+
serverWsUrl,
|
|
222
|
+
startGateway,
|
|
223
|
+
toGatewayWsUrl
|
|
224
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { normalizeProviderName, parseProviderList } = require("../shared/providers");
|
|
2
|
+
|
|
3
|
+
function providerNames(env = process.env) {
|
|
4
|
+
return parseProviderList(env.AGENT_ANYWHERE_PROVIDERS || env.AGENT_PROVIDERS || env.AGENT_PROVIDER || "mock");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function defaultProviderName(env = process.env) {
|
|
8
|
+
return providerNames(env)[0] || "mock";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function selectProviderForPayload(payload = {}, env = process.env) {
|
|
12
|
+
const providers = providerNames(env);
|
|
13
|
+
const requested = normalizeProviderName(payload.provider || payload.session?.provider || defaultProviderName(env));
|
|
14
|
+
if (providers.includes(requested)) {
|
|
15
|
+
return requested;
|
|
16
|
+
}
|
|
17
|
+
const error = new Error(`gateway 不支持 provider:${requested}`);
|
|
18
|
+
error.statusCode = 400;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
defaultProviderName,
|
|
24
|
+
normalizeProviderName,
|
|
25
|
+
parseProviderList,
|
|
26
|
+
providerNames,
|
|
27
|
+
selectProviderForPayload
|
|
28
|
+
};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { createLocalAgentAdapter } = require("../adapters/local-agent-adapter");
|
|
4
|
+
const { buildCapabilities, capabilitiesForProvider, normalizeAgentSettings } = require("../shared/capabilities");
|
|
5
|
+
const { GatewayMessageType, GatewayRequestMethod } = require("../shared/gateway-protocol");
|
|
6
|
+
const { defaultProviderName, selectProviderForPayload } = require("./providers");
|
|
7
|
+
const {
|
|
8
|
+
imageAttachmentsOnly,
|
|
9
|
+
materializeImageAttachments,
|
|
10
|
+
normalizeAttachments
|
|
11
|
+
} = require("../shared/image-attachments");
|
|
12
|
+
const { discoverGitProjects } = require("../lib/local-discovery");
|
|
13
|
+
const { parseAllowedRoots, resolveProjectPath } = require("../shared/path-policy");
|
|
14
|
+
const { readProjectFile } = require("../shared/project-files");
|
|
15
|
+
|
|
16
|
+
const pendingApprovalDecisions = new Map();
|
|
17
|
+
|
|
18
|
+
function defaultGatewayId() {
|
|
19
|
+
return process.env.AGENT_ANYWHERE_GATEWAY_ID || os.hostname() || "gateway";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function providerName() {
|
|
23
|
+
return defaultProviderName();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function attachmentBaseDir() {
|
|
27
|
+
return process.env.AGENT_ANYWHERE_DATA_DIR || path.join(process.cwd(), ".data");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function allowedRootPaths() {
|
|
31
|
+
return parseAllowedRoots(process.env.AGENT_ANYWHERE_ALLOWED_ROOTS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function localCapabilities(adapter, provider) {
|
|
35
|
+
try {
|
|
36
|
+
return await adapter.discoverCapabilities();
|
|
37
|
+
} catch {
|
|
38
|
+
return buildCapabilities(provider);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runRequest(payload, send, { gatewayId = defaultGatewayId() } = {}) {
|
|
43
|
+
const provider = selectProviderForPayload(payload);
|
|
44
|
+
const adapter = createLocalAgentAdapter(provider);
|
|
45
|
+
const projectPath = resolveProjectPath(payload.project_path, allowedRootPaths());
|
|
46
|
+
const session = { ...(payload.session || { id: "gateway-session" }), provider };
|
|
47
|
+
const project = { id: "gateway-project", path: projectPath };
|
|
48
|
+
const capabilities = await localCapabilities(adapter, provider);
|
|
49
|
+
const settings = normalizeAgentSettings(payload.settings || {}, session, capabilitiesForProvider(provider, capabilities));
|
|
50
|
+
const normalizedAttachments = normalizeAttachments(payload.attachments || []);
|
|
51
|
+
const attachments = materializeImageAttachments(imageAttachmentsOnly(normalizedAttachments), {
|
|
52
|
+
baseDir: attachmentBaseDir(),
|
|
53
|
+
sessionId: session.id || "gateway-session",
|
|
54
|
+
turnId: "gateway-turn"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
for await (const event of adapter.runTurn({
|
|
58
|
+
machine: { id: gatewayId, host: "" },
|
|
59
|
+
session,
|
|
60
|
+
project,
|
|
61
|
+
message: String(payload.message || ""),
|
|
62
|
+
attachments,
|
|
63
|
+
settings,
|
|
64
|
+
requestApproval: (approvalPayload) => {
|
|
65
|
+
const approvalRequestId = `approval_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
66
|
+
send({
|
|
67
|
+
type: GatewayMessageType.STREAM_EVENT,
|
|
68
|
+
payload: {
|
|
69
|
+
type: "approval_request",
|
|
70
|
+
payload: {
|
|
71
|
+
...approvalPayload,
|
|
72
|
+
gateway_approval_request_id: approvalRequestId
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const timeout = setTimeout(() => {
|
|
78
|
+
pendingApprovalDecisions.delete(approvalRequestId);
|
|
79
|
+
reject(new Error("gateway approval decision timed out"));
|
|
80
|
+
}, 5 * 60 * 1000);
|
|
81
|
+
pendingApprovalDecisions.set(approvalRequestId, { resolve, reject, timeout });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
})) {
|
|
85
|
+
send({
|
|
86
|
+
type: GatewayMessageType.STREAM_EVENT,
|
|
87
|
+
payload: event
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function controlTurnRequest(payload, action) {
|
|
93
|
+
const provider = selectProviderForPayload(payload);
|
|
94
|
+
const adapter = createLocalAgentAdapter(provider);
|
|
95
|
+
const session = { ...(payload.session || { id: "gateway-session" }), provider };
|
|
96
|
+
if (action === "cancel") {
|
|
97
|
+
return adapter.cancelTurn({
|
|
98
|
+
session,
|
|
99
|
+
turnId: payload.turn_id,
|
|
100
|
+
reason: payload.reason
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const normalizedAttachments = normalizeAttachments(payload.attachments || []);
|
|
104
|
+
return adapter.steerTurn({
|
|
105
|
+
session,
|
|
106
|
+
turnId: payload.turn_id,
|
|
107
|
+
message: String(payload.message || ""),
|
|
108
|
+
attachments: materializeImageAttachments(imageAttachmentsOnly(normalizedAttachments), {
|
|
109
|
+
baseDir: attachmentBaseDir(),
|
|
110
|
+
sessionId: session.id || "gateway-session",
|
|
111
|
+
turnId: payload.turn_id || "steer"
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function listRuntimeSessionsRequest(payload) {
|
|
117
|
+
const provider = payload.provider ? selectProviderForPayload(payload) : providerName();
|
|
118
|
+
const adapter = createLocalAgentAdapter(provider);
|
|
119
|
+
const projectPath = resolveProjectPath(payload.project_path, allowedRootPaths());
|
|
120
|
+
const sessions = await adapter.listRuntimeSessions({
|
|
121
|
+
project: { id: "gateway-project", path: projectPath },
|
|
122
|
+
limit: Number(payload.limit || 50)
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
sessions: await enrichRuntimeSessions(adapter, sessions)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function enrichRuntimeSessions(adapter, sessions) {
|
|
130
|
+
const enriched = [];
|
|
131
|
+
for (const session of sessions || []) {
|
|
132
|
+
const runtimeSessionId = runtimeSessionIdOf(session);
|
|
133
|
+
if (!runtimeSessionId || hasDisplayTitle(session)) {
|
|
134
|
+
enriched.push(normalizeRuntimeSession(session));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const detail = await adapter.readRuntimeSession({
|
|
139
|
+
runtimeSessionId,
|
|
140
|
+
includeTurns: true
|
|
141
|
+
});
|
|
142
|
+
enriched.push(normalizeRuntimeSession({
|
|
143
|
+
...session,
|
|
144
|
+
...threadMetadata(detail?.thread),
|
|
145
|
+
first_user_message: session.first_user_message || firstTurnPrompt(runtimeDetailTurns(detail)),
|
|
146
|
+
turns: undefined
|
|
147
|
+
}));
|
|
148
|
+
} catch {
|
|
149
|
+
enriched.push(normalizeRuntimeSession(session));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return enriched;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function runtimeSessionIdOf(session = {}) {
|
|
156
|
+
return session.runtime_session_id || session.id || session.thread?.id || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hasDisplayTitle(session = {}) {
|
|
160
|
+
const id = runtimeSessionIdOf(session);
|
|
161
|
+
const title = displayTitleOf(session);
|
|
162
|
+
return Boolean(title && title !== id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeRuntimeSession(session = {}) {
|
|
166
|
+
const runtimeSessionId = runtimeSessionIdOf(session);
|
|
167
|
+
return {
|
|
168
|
+
...session,
|
|
169
|
+
id: session.id || runtimeSessionId,
|
|
170
|
+
runtime_session_id: runtimeSessionId,
|
|
171
|
+
title: displayTitleOf(session) || runtimeSessionId,
|
|
172
|
+
provider: session.provider || providerName()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function threadMetadata(thread = {}) {
|
|
177
|
+
return {
|
|
178
|
+
id: thread.id,
|
|
179
|
+
runtime_session_id: thread.id,
|
|
180
|
+
title: displayTitleOf(thread),
|
|
181
|
+
name: thread.name,
|
|
182
|
+
preview: thread.preview,
|
|
183
|
+
first_user_message: thread.first_user_message,
|
|
184
|
+
model: thread.model,
|
|
185
|
+
reasoning_effort: thread.reasoning_effort,
|
|
186
|
+
approval_policy: thread.approval_policy,
|
|
187
|
+
mode: thread.mode,
|
|
188
|
+
created_at: thread.created_at || thread.createdAt,
|
|
189
|
+
updated_at: thread.updated_at || thread.updatedAt
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function displayTitleOf(value = {}) {
|
|
194
|
+
return value.title || value.name || value.preview || value.first_user_message || value.thread?.title ||
|
|
195
|
+
value.thread?.name || value.thread?.preview || value.thread?.first_user_message || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function firstTurnPrompt(turns) {
|
|
199
|
+
for (const turn of turns || []) {
|
|
200
|
+
const prompt = turn?.prompt || turn?.message || turn?.user_message ||
|
|
201
|
+
textFromInput(turn?.input) || textFromThreadItems(turn?.items);
|
|
202
|
+
if (prompt) {
|
|
203
|
+
return String(prompt);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function runtimeDetailTurns(detail = {}) {
|
|
210
|
+
if (Array.isArray(detail.turns)) {
|
|
211
|
+
return detail.turns;
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(detail.thread?.turns)) {
|
|
214
|
+
return detail.thread.turns;
|
|
215
|
+
}
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function textFromInput(input) {
|
|
220
|
+
if (typeof input === "string") {
|
|
221
|
+
return input;
|
|
222
|
+
}
|
|
223
|
+
if (!Array.isArray(input)) {
|
|
224
|
+
return input?.text || null;
|
|
225
|
+
}
|
|
226
|
+
return input
|
|
227
|
+
.map((item) => typeof item === "string" ? item : item?.text)
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join("\n") || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function textFromThreadItems(items) {
|
|
233
|
+
if (!Array.isArray(items)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
for (const item of items) {
|
|
237
|
+
if (item?.type !== "userMessage") {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const text = textFromInput(item.content);
|
|
241
|
+
if (text) {
|
|
242
|
+
return text;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function readRuntimeSessionRequest(payload) {
|
|
249
|
+
const provider = payload.provider ? selectProviderForPayload(payload) : providerName();
|
|
250
|
+
const adapter = createLocalAgentAdapter(provider);
|
|
251
|
+
return adapter.readRuntimeSession({
|
|
252
|
+
runtimeSessionId: payload.runtime_session_id,
|
|
253
|
+
includeTurns: payload.include_turns !== false
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function discoverProjectsRequest() {
|
|
258
|
+
return {
|
|
259
|
+
projects: discoverGitProjects(allowedRootPaths())
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function readProjectFileRequest(payload) {
|
|
264
|
+
return readProjectFile({
|
|
265
|
+
projectPath: payload.project_path,
|
|
266
|
+
requestedPath: payload.path,
|
|
267
|
+
allowedRoots: allowedRootPaths()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleApprovalDecision(message) {
|
|
272
|
+
const approval = pendingApprovalDecisions.get(message.approval_request_id);
|
|
273
|
+
if (!approval) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
clearTimeout(approval.timeout);
|
|
277
|
+
pendingApprovalDecisions.delete(message.approval_request_id);
|
|
278
|
+
if (message.error) {
|
|
279
|
+
approval.reject(new Error(message.error));
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
approval.resolve(message.payload || {});
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function handleRequest(message, send, options = {}) {
|
|
287
|
+
try {
|
|
288
|
+
if (message.method === GatewayRequestMethod.RUN) {
|
|
289
|
+
await runRequest(message.payload || {}, send, options);
|
|
290
|
+
send({ type: GatewayMessageType.STREAM_COMPLETE });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (message.method === GatewayRequestMethod.DISCOVER_PROJECTS) {
|
|
294
|
+
send({ type: GatewayMessageType.RESPONSE, payload: discoverProjectsRequest() });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (message.method === GatewayRequestMethod.CANCEL_TURN) {
|
|
298
|
+
send({ type: GatewayMessageType.RESPONSE, payload: await controlTurnRequest(message.payload || {}, "cancel") });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (message.method === GatewayRequestMethod.STEER_TURN) {
|
|
302
|
+
send({ type: GatewayMessageType.RESPONSE, payload: await controlTurnRequest(message.payload || {}, "steer") });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (message.method === GatewayRequestMethod.LIST_RUNTIME_SESSIONS) {
|
|
306
|
+
send({ type: GatewayMessageType.RESPONSE, payload: await listRuntimeSessionsRequest(message.payload || {}) });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (message.method === GatewayRequestMethod.READ_RUNTIME_SESSION) {
|
|
310
|
+
send({ type: GatewayMessageType.RESPONSE, payload: await readRuntimeSessionRequest(message.payload || {}) });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (message.method === GatewayRequestMethod.READ_PROJECT_FILE) {
|
|
314
|
+
send({ type: GatewayMessageType.RESPONSE, payload: readProjectFileRequest(message.payload || {}) });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
send({ type: GatewayMessageType.ERROR, error: `未知 gateway 方法:${message.method}` });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
send({
|
|
320
|
+
type: message.method === GatewayRequestMethod.RUN ? GatewayMessageType.STREAM_ERROR : GatewayMessageType.ERROR,
|
|
321
|
+
error: error.message
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
controlTurnRequest,
|
|
328
|
+
discoverProjectsRequest,
|
|
329
|
+
enrichRuntimeSessions,
|
|
330
|
+
handleApprovalDecision,
|
|
331
|
+
handleRequest,
|
|
332
|
+
listRuntimeSessionsRequest,
|
|
333
|
+
runtimeDetailTurns,
|
|
334
|
+
readProjectFileRequest,
|
|
335
|
+
readRuntimeSessionRequest,
|
|
336
|
+
runRequest
|
|
337
|
+
};
|
package/src/gateway.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("../shared/capabilities");
|