@sjawhar/opencode-legion-envoy 0.3.2 → 0.4.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/package.json +3 -1
- package/src/server.ts +72 -150
- package/tsconfig.json +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/opencode-legion-envoy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/server.ts",
|
|
6
6
|
"exports": {
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"lint": "bunx biome check src/"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@legion/contracts": "workspace:*",
|
|
26
|
+
"@legion/envoy-client": "workspace:*",
|
|
25
27
|
"@opencode-ai/plugin": "~1.14.46"
|
|
26
28
|
},
|
|
27
29
|
"peerDependencies": {
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { agentSubject } from "@legion/contracts";
|
|
2
|
+
import { envoyDefaultsFromEnvironment } from "@legion/envoy-client/defaults";
|
|
3
|
+
import { envoyToolSpecs } from "@legion/envoy-client/tool-contract";
|
|
4
|
+
import { createEnvoyClient } from "@legion/envoy-client/transport";
|
|
1
5
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
6
|
import { loadEnvoyConfig } from "./config";
|
|
3
7
|
import { buildDispatchMcpEntry, injectEnvoyMcp } from "./dispatch-mcp";
|
|
@@ -5,24 +9,22 @@ import { dispatchSubscriptionTopic } from "./dispatch-subscribe";
|
|
|
5
9
|
import { logger } from "./log";
|
|
6
10
|
import { resolvePort } from "./port";
|
|
7
11
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const text = await res.text();
|
|
19
|
-
if (!res.ok) throw new Error(text || `${res.status}`);
|
|
20
|
-
return text;
|
|
21
|
-
}
|
|
12
|
+
const [
|
|
13
|
+
subscribeSpec,
|
|
14
|
+
unsubscribeSpec,
|
|
15
|
+
listSpec,
|
|
16
|
+
sendSpec,
|
|
17
|
+
publishSpec,
|
|
18
|
+
roleSetSpec,
|
|
19
|
+
whoamiSpec,
|
|
20
|
+
sessionsSpec,
|
|
21
|
+
] = envoyToolSpecs;
|
|
22
22
|
|
|
23
23
|
export default async (input: { serverUrl: URL }) => {
|
|
24
24
|
const cwd = process.cwd();
|
|
25
25
|
const config = await loadEnvoyConfig(cwd);
|
|
26
|
+
const envoyDefaults = envoyDefaultsFromEnvironment(process.env);
|
|
27
|
+
const envoy = createEnvoyClient({ baseUrl: envoyDefaults.envoyUrl, fetch: globalThis.fetch });
|
|
26
28
|
let activeSessionID: string | null = null;
|
|
27
29
|
let activeSessionTitle: string | null = null;
|
|
28
30
|
// All sessions that have become busy in this serve instance. The heartbeat
|
|
@@ -61,7 +63,7 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
61
63
|
const fetchTitle = async (sessionID: string): Promise<string | null> => {
|
|
62
64
|
try {
|
|
63
65
|
const res = await fetch(`${input.serverUrl.href}session/${sessionID}`, {
|
|
64
|
-
signal: AbortSignal.timeout(
|
|
66
|
+
signal: AbortSignal.timeout(5_000),
|
|
65
67
|
});
|
|
66
68
|
if (!res.ok) return null;
|
|
67
69
|
const data = (await res.json()) as { title?: string };
|
|
@@ -92,18 +94,16 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
92
94
|
port: number,
|
|
93
95
|
driving: boolean
|
|
94
96
|
) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
dir: cwd,
|
|
101
|
-
topics: [`notifications.agent.${sessionID}`],
|
|
97
|
+
envoy
|
|
98
|
+
.subscribe({
|
|
99
|
+
sessionID,
|
|
100
|
+
directory: cwd,
|
|
101
|
+
topics: [agentSubject(sessionID)],
|
|
102
102
|
port,
|
|
103
103
|
title: title ?? "",
|
|
104
104
|
driving,
|
|
105
|
-
})
|
|
106
|
-
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {});
|
|
107
107
|
|
|
108
108
|
// A process registers ONLY the sessions it has actually run (see the busy
|
|
109
109
|
// handler below). It must never claim a route for a session it merely has
|
|
@@ -119,11 +119,7 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
119
119
|
// Heartbeat: re-subscribe every tracked session to refresh the envoy_sessions
|
|
120
120
|
// TTL (5-min). Refreshes ALL sessions that have been busy in this serve, not
|
|
121
121
|
// just the most recently active one. Interval is env-tunable for tests/tuning.
|
|
122
|
-
const
|
|
123
|
-
const heartbeatMs =
|
|
124
|
-
Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0
|
|
125
|
-
? Math.max(rawHeartbeatMs, 25)
|
|
126
|
-
: 2 * 60 * 1000;
|
|
122
|
+
const heartbeatMs = envoyDefaults.heartbeatMs;
|
|
127
123
|
const heartbeatInterval = setInterval(() => {
|
|
128
124
|
const port = currentPort();
|
|
129
125
|
if (!port) return;
|
|
@@ -214,11 +210,7 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
214
210
|
activeSessionTitle = null;
|
|
215
211
|
}
|
|
216
212
|
// Best-effort: drop the deleted session's interests so routing stops.
|
|
217
|
-
|
|
218
|
-
method: "POST",
|
|
219
|
-
headers: { "Content-Type": "application/json" },
|
|
220
|
-
body: JSON.stringify({ session_id: deletedID, topics: [] }),
|
|
221
|
-
}).catch(() => {});
|
|
213
|
+
envoy.unsubscribe({ sessionID: deletedID, topics: [] }).catch(() => {});
|
|
222
214
|
}
|
|
223
215
|
}
|
|
224
216
|
},
|
|
@@ -233,18 +225,13 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
233
225
|
const topic = dispatchSubscriptionTopic(input.tool, output.output);
|
|
234
226
|
if (!topic) return;
|
|
235
227
|
try {
|
|
236
|
-
await
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
port: currentPort() ?? 0,
|
|
244
|
-
title: activeSessionTitle ?? "",
|
|
245
|
-
// A tool call runs in this process, so it is the driving holder.
|
|
246
|
-
driving: true,
|
|
247
|
-
}),
|
|
228
|
+
await envoy.subscribe({
|
|
229
|
+
sessionID: input.sessionID,
|
|
230
|
+
directory: cwd,
|
|
231
|
+
topics: [topic],
|
|
232
|
+
port: currentPort() ?? 0,
|
|
233
|
+
title: activeSessionTitle ?? "",
|
|
234
|
+
driving: true,
|
|
248
235
|
});
|
|
249
236
|
} catch (err) {
|
|
250
237
|
logger.warn(
|
|
@@ -259,132 +246,77 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
259
246
|
},
|
|
260
247
|
tool: {
|
|
261
248
|
envoy_subscribe: tool({
|
|
262
|
-
description:
|
|
263
|
-
|
|
264
|
-
args: {
|
|
265
|
-
topics: tool.schema
|
|
266
|
-
.array(tool.schema.string())
|
|
267
|
-
.describe(
|
|
268
|
-
"NATS-style topic patterns to subscribe to. GitHub topics include resource number: notifications.github.owner.repo.pr.123 (PR state), notifications.github.owner.repo.pr.123.comment (PR comments), notifications.github.owner.repo.issue.456.> (all events on issue). Use > wildcard for broad matching. Other examples: notifications.agent.ses_123, notifications.slack.T09FRELLTS8.C0A0DHVU8HE.mention"
|
|
269
|
-
),
|
|
270
|
-
},
|
|
249
|
+
description: subscribeSpec.description,
|
|
250
|
+
args: { topics: tool.schema.array(tool.schema.string()) },
|
|
271
251
|
async execute(args, ctx) {
|
|
272
252
|
ctx.metadata({ title: "Envoy subscribe" });
|
|
273
|
-
return
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
session_id: ctx.sessionID,
|
|
278
|
-
dir: ctx.directory,
|
|
253
|
+
return JSON.stringify(
|
|
254
|
+
await envoy.subscribe({
|
|
255
|
+
sessionID: ctx.sessionID,
|
|
256
|
+
directory: ctx.directory,
|
|
279
257
|
topics: args.topics,
|
|
280
258
|
port: currentPort() ?? 0,
|
|
281
259
|
title: activeSessionTitle ?? "",
|
|
282
260
|
driving: true,
|
|
283
|
-
})
|
|
284
|
-
|
|
261
|
+
})
|
|
262
|
+
);
|
|
285
263
|
},
|
|
286
264
|
}),
|
|
287
265
|
envoy_unsubscribe: tool({
|
|
288
|
-
description:
|
|
289
|
-
|
|
290
|
-
args: {
|
|
291
|
-
topics: tool.schema
|
|
292
|
-
.array(tool.schema.string())
|
|
293
|
-
.optional()
|
|
294
|
-
.describe("Topics to remove, or omit to remove all"),
|
|
295
|
-
},
|
|
266
|
+
description: unsubscribeSpec.description,
|
|
267
|
+
args: { topics: tool.schema.array(tool.schema.string()).optional() },
|
|
296
268
|
async execute(args, ctx) {
|
|
297
269
|
ctx.metadata({ title: "Envoy unsubscribe" });
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
headers: { "Content-Type": "application/json" },
|
|
301
|
-
body: JSON.stringify({
|
|
302
|
-
session_id: ctx.sessionID,
|
|
303
|
-
topics: args.topics ?? [],
|
|
304
|
-
}),
|
|
305
|
-
});
|
|
270
|
+
await envoy.unsubscribe({ sessionID: ctx.sessionID, topics: args.topics ?? [] });
|
|
271
|
+
return "ok";
|
|
306
272
|
},
|
|
307
273
|
}),
|
|
308
274
|
envoy_list: tool({
|
|
309
|
-
description:
|
|
310
|
-
"List the current Envoy topic subscriptions for this session so you can confirm the exact topic shapes that are active.",
|
|
275
|
+
description: listSpec.description,
|
|
311
276
|
args: {},
|
|
312
277
|
async execute(_args, ctx) {
|
|
313
278
|
ctx.metadata({ title: "Envoy list" });
|
|
314
|
-
return
|
|
279
|
+
return JSON.stringify(await envoy.getInterest(ctx.sessionID));
|
|
315
280
|
},
|
|
316
281
|
}),
|
|
317
282
|
envoy_send: tool({
|
|
318
|
-
description:
|
|
319
|
-
|
|
320
|
-
args: {
|
|
321
|
-
target_session: tool.schema
|
|
322
|
-
.string()
|
|
323
|
-
.describe("Target OpenCode session ID, e.g. ses_2e6ca3034ffejVikSZ8mDwk0mR"),
|
|
324
|
-
message: tool.schema
|
|
325
|
-
.string()
|
|
326
|
-
.describe("Message body to deliver to that session as a new user turn/notification"),
|
|
327
|
-
},
|
|
283
|
+
description: sendSpec.description,
|
|
284
|
+
args: { target_session: tool.schema.string(), message: tool.schema.string() },
|
|
328
285
|
async execute(args, ctx) {
|
|
329
286
|
ctx.metadata({ title: "Envoy send" });
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
source_session: ctx.sessionID,
|
|
335
|
-
target_session: args.target_session,
|
|
287
|
+
return JSON.stringify(
|
|
288
|
+
await envoy.send({
|
|
289
|
+
sourceSessionID: ctx.sessionID,
|
|
290
|
+
targetSessionID: args.target_session,
|
|
336
291
|
message: args.message,
|
|
337
|
-
})
|
|
338
|
-
|
|
292
|
+
})
|
|
293
|
+
);
|
|
339
294
|
},
|
|
340
295
|
}),
|
|
341
296
|
envoy_publish: tool({
|
|
342
|
-
description:
|
|
343
|
-
|
|
344
|
-
args: {
|
|
345
|
-
topic: tool.schema
|
|
346
|
-
.string()
|
|
347
|
-
.describe("NATS-style topic to publish to, e.g. notifications.role.legion-controller"),
|
|
348
|
-
message: tool.schema.string().describe("Message body to broadcast"),
|
|
349
|
-
},
|
|
297
|
+
description: publishSpec.description,
|
|
298
|
+
args: { topic: tool.schema.string(), message: tool.schema.string() },
|
|
350
299
|
async execute(args, ctx) {
|
|
351
300
|
ctx.metadata({ title: "Envoy publish" });
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
body: JSON.stringify({
|
|
356
|
-
source_session: ctx.sessionID,
|
|
301
|
+
return JSON.stringify(
|
|
302
|
+
await envoy.publish({
|
|
303
|
+
sourceSessionID: ctx.sessionID,
|
|
357
304
|
topic: args.topic,
|
|
358
305
|
message: args.message,
|
|
359
|
-
})
|
|
360
|
-
|
|
306
|
+
})
|
|
307
|
+
);
|
|
361
308
|
},
|
|
362
309
|
}),
|
|
363
310
|
envoy_role_set: tool({
|
|
364
|
-
description:
|
|
365
|
-
|
|
366
|
-
args: {
|
|
367
|
-
role: tool.schema
|
|
368
|
-
.string()
|
|
369
|
-
.describe(
|
|
370
|
-
"Role name to claim (lowercase alphanumeric, hyphens, underscores). E.g. opencode-dev, legion-controller, legion-po"
|
|
371
|
-
),
|
|
372
|
-
},
|
|
311
|
+
description: roleSetSpec.description,
|
|
312
|
+
args: { role: tool.schema.string() },
|
|
373
313
|
async execute(args, ctx) {
|
|
374
314
|
ctx.metadata({ title: "Set Envoy role" });
|
|
375
|
-
return
|
|
376
|
-
method: "POST",
|
|
377
|
-
headers: { "Content-Type": "application/json" },
|
|
378
|
-
body: JSON.stringify({
|
|
379
|
-
session_id: ctx.sessionID,
|
|
380
|
-
role: args.role,
|
|
381
|
-
}),
|
|
382
|
-
});
|
|
315
|
+
return JSON.stringify(await envoy.setRole({ sessionID: ctx.sessionID, role: args.role }));
|
|
383
316
|
},
|
|
384
317
|
}),
|
|
385
318
|
envoy_whoami: tool({
|
|
386
|
-
description:
|
|
387
|
-
"Returns this session's Envoy identity: session ID, machine ID, port, and directory.",
|
|
319
|
+
description: whoamiSpec.description,
|
|
388
320
|
args: {},
|
|
389
321
|
async execute(_args, ctx) {
|
|
390
322
|
ctx.metadata({ title: "Envoy whoami" });
|
|
@@ -403,25 +335,15 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
403
335
|
},
|
|
404
336
|
}),
|
|
405
337
|
envoy_sessions: tool({
|
|
406
|
-
description:
|
|
407
|
-
|
|
408
|
-
args: {
|
|
409
|
-
machine: tool.schema
|
|
410
|
-
.string()
|
|
411
|
-
.optional()
|
|
412
|
-
.describe(
|
|
413
|
-
"Filter to sessions on this machine ID (e.g. hostname). Omit to list all machines."
|
|
414
|
-
),
|
|
415
|
-
},
|
|
338
|
+
description: sessionsSpec.description,
|
|
339
|
+
args: { machine: tool.schema.string().optional() },
|
|
416
340
|
async execute(args, ctx) {
|
|
417
341
|
ctx.metadata({ title: "Envoy sessions" });
|
|
418
|
-
const
|
|
419
|
-
if (!args.machine) return res;
|
|
420
|
-
const sessions = JSON.parse(res) as Array<{
|
|
421
|
-
machine_id: string;
|
|
422
|
-
}>;
|
|
342
|
+
const sessions = await envoy.listSessions();
|
|
423
343
|
return JSON.stringify(
|
|
424
|
-
|
|
344
|
+
args.machine
|
|
345
|
+
? sessions.filter((session) => session.machine_id === args.machine)
|
|
346
|
+
: sessions,
|
|
425
347
|
null,
|
|
426
348
|
2
|
|
427
349
|
);
|
package/tsconfig.json
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
"target": "ES2022",
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "Bundler",
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@legion/contracts": ["../contracts/src/index.ts"],
|
|
9
|
+
"@legion/envoy-client/defaults": ["../envoy-client/src/defaults.ts"],
|
|
10
|
+
"@legion/envoy-client/tool-contract": ["../envoy-client/src/tool-contract.ts"],
|
|
11
|
+
"@legion/envoy-client/transport": ["../envoy-client/src/transport.ts"]
|
|
12
|
+
},
|
|
6
13
|
"strict": true,
|
|
7
14
|
"noEmit": true,
|
|
8
15
|
"skipLibCheck": true,
|