agent-relay-runner 0.121.5 → 0.122.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 +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
- package/src/adapter.ts +53 -18
- package/src/adapters/claude-delivery.ts +6 -20
- package/src/config.ts +1 -4
- package/src/message-body-config/config.ts +3 -0
- package/src/message-body-config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.122.1",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"agent-relay-providers": "0.104.3",
|
|
24
|
-
"agent-relay-sdk": "0.2.
|
|
24
|
+
"agent-relay-sdk": "0.2.114",
|
|
25
25
|
"callmux": "0.23.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
@@ -3,922 +3,8 @@
|
|
|
3
3
|
function isRecord(value) {
|
|
4
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
5
|
}
|
|
6
|
-
// sdk/src/types/constants.ts
|
|
7
|
-
var DEFAULT_RELAY_PORT = 4850;
|
|
8
|
-
var DEFAULT_RELAY_URL = `http://127.0.0.1:${DEFAULT_RELAY_PORT}`;
|
|
9
|
-
// providers/registry.ts
|
|
10
|
-
var PROVIDER_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
11
|
-
var manifests = new Map;
|
|
12
|
-
function registerProvider(manifest) {
|
|
13
|
-
if (!PROVIDER_ID_RE.test(manifest.id)) {
|
|
14
|
-
throw new Error(`invalid provider id: ${manifest.id}`);
|
|
15
|
-
}
|
|
16
|
-
if (!manifest.label.trim()) {
|
|
17
|
-
throw new Error(`provider ${manifest.id} is missing a label`);
|
|
18
|
-
}
|
|
19
|
-
if (!manifest.icon.trim()) {
|
|
20
|
-
throw new Error(`provider ${manifest.id} is missing an icon`);
|
|
21
|
-
}
|
|
22
|
-
if (manifests.has(manifest.id)) {
|
|
23
|
-
throw new Error(`provider already registered: ${manifest.id}`);
|
|
24
|
-
}
|
|
25
|
-
manifests.set(manifest.id, manifest);
|
|
26
|
-
}
|
|
27
|
-
function getAllManifests() {
|
|
28
|
-
return [...manifests.values()];
|
|
29
|
-
}
|
|
30
|
-
var accountKeyAliasResolvers = new Map;
|
|
31
|
-
|
|
32
|
-
// providers/claude.ts
|
|
33
|
-
var CLAUDE_LOW_TO_MAX = ["low", "medium", "high", "max"];
|
|
34
|
-
var CLAUDE_LOW_TO_XHIGH_MAX = ["low", "medium", "high", "xhigh", "max"];
|
|
35
|
-
var CONTEXT_200K = { value: 200000, source: "catalog", confidence: "declared" };
|
|
36
|
-
var CONTEXT_1M = { value: 1e6, source: "catalog", confidence: "declared" };
|
|
37
|
-
var CODE_MODEL_CAPABILITIES = {
|
|
38
|
-
modalities: {
|
|
39
|
-
input: { text: true, image: true },
|
|
40
|
-
output: { text: true }
|
|
41
|
-
},
|
|
42
|
-
tools: {
|
|
43
|
-
code: true,
|
|
44
|
-
review: true,
|
|
45
|
-
debug: true,
|
|
46
|
-
refactor: true
|
|
47
|
-
},
|
|
48
|
-
source: "catalog",
|
|
49
|
-
confidence: "declared"
|
|
50
|
-
};
|
|
51
|
-
var quotaWindows = [
|
|
52
|
-
{ name: "five_hour", unit: "%", reset: true },
|
|
53
|
-
{ name: "seven_day", unit: "%", reset: true }
|
|
54
|
-
];
|
|
55
|
-
function codeModel(limits) {
|
|
56
|
-
return {
|
|
57
|
-
...limits ? { limits } : {},
|
|
58
|
-
capabilities: CODE_MODEL_CAPABILITIES
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
var claudeProviderManifest = {
|
|
62
|
-
id: "claude",
|
|
63
|
-
label: "Claude",
|
|
64
|
-
icon: "\uD83D\uDFE0",
|
|
65
|
-
launch: {
|
|
66
|
-
managedPolicyPrompt: {
|
|
67
|
-
alwaysOn: "append-system-prompt"
|
|
68
|
-
},
|
|
69
|
-
taskHydration: {
|
|
70
|
-
channel: "append-system-prompt"
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
catalog: {
|
|
74
|
-
defaultModel: "sonnet-5",
|
|
75
|
-
models: [
|
|
76
|
-
{ alias: "fable-5", label: "Fable 5", providerModel: "claude-fable-5", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", unavailable: "suspended by export-control directive, 2026-06-12", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
77
|
-
{ alias: "opus-4.8", label: "Opus 4.8", providerModel: "claude-opus-4-8", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
78
|
-
{ alias: "opus-4.8[1m]", label: "Opus 4.8 1M", providerModel: "claude-opus-4-8[1m]", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
79
|
-
{ alias: "opus-4.7", label: "Opus 4.7", providerModel: "claude-opus-4-7", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
80
|
-
{ alias: "opus-4.7[1m]", label: "Opus 4.7 1M", providerModel: "claude-opus-4-7[1m]", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
81
|
-
{ alias: "opus-4.6", label: "Opus 4.6", providerModel: "claude-opus-4-6", efforts: CLAUDE_LOW_TO_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
82
|
-
{ alias: "opus-4.6[1m]", label: "Opus 4.6 1M", providerModel: "claude-opus-4-6[1m]", efforts: CLAUDE_LOW_TO_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
83
|
-
{ alias: "sonnet-5", label: "Sonnet 5", providerModel: "claude-sonnet-5", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
84
|
-
{ alias: "sonnet-5[1m]", label: "Sonnet 5 1M", providerModel: "claude-sonnet-5[1m]", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
85
|
-
{ alias: "haiku", label: "Haiku", providerModel: "haiku", efforts: [], ...codeModel() }
|
|
86
|
-
]
|
|
87
|
-
},
|
|
88
|
-
quotaWindows: [...quotaWindows],
|
|
89
|
-
capabilityDefaults: {
|
|
90
|
-
lifecycle: {
|
|
91
|
-
managed: true,
|
|
92
|
-
shutdownHard: { when: "headless", value: true, otherwise: false },
|
|
93
|
-
restartHard: { when: "headless", value: true, otherwise: false },
|
|
94
|
-
semanticStatus: true,
|
|
95
|
-
reconnect: true
|
|
96
|
-
},
|
|
97
|
-
session: {
|
|
98
|
-
fileRead: true,
|
|
99
|
-
fileWrite: { when: "approval:not-read-only", value: true, otherwise: false },
|
|
100
|
-
shell: true,
|
|
101
|
-
shellMode: { open: "unrestricted", guarded: "guarded", readOnly: "read-only-guarded" }
|
|
102
|
-
},
|
|
103
|
-
context: {
|
|
104
|
-
managed: "headless",
|
|
105
|
-
compact: { when: "headless", value: true, otherwise: false },
|
|
106
|
-
clear: { when: "headless", value: true, otherwise: false },
|
|
107
|
-
inject: true,
|
|
108
|
-
resumePreference: ["native", "clear-inject", "none"]
|
|
109
|
-
},
|
|
110
|
-
quota: {
|
|
111
|
-
supported: true,
|
|
112
|
-
source: "probe",
|
|
113
|
-
unit: "%",
|
|
114
|
-
windows: [...quotaWindows]
|
|
115
|
-
},
|
|
116
|
-
terminal: {
|
|
117
|
-
mode: "live",
|
|
118
|
-
condition: "headless",
|
|
119
|
-
live: { read: true, write: true }
|
|
120
|
-
},
|
|
121
|
-
liveSession: {
|
|
122
|
-
capture: true,
|
|
123
|
-
inject: true,
|
|
124
|
-
interrupt: true,
|
|
125
|
-
promptEcho: true,
|
|
126
|
-
reasoning: true,
|
|
127
|
-
slashCommands: true
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
slashCommands: ["/clear", "/compact", "/model", "/help"],
|
|
131
|
-
resume: { supported: true, idFormat: "uuid-v4", idPattern: "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" },
|
|
132
|
-
home: { configDir: ".claude", envPrefix: "CLAUDE", configDirEnvVar: "CLAUDE_CONFIG_DIR", quotaCredentialFile: ".credentials.json", authItems: [".credentials.json", "statsig"], globalInstructionFiles: ["CLAUDE.md"] },
|
|
133
|
-
provisioning: {
|
|
134
|
-
pluginManifestPath: ".claude-plugin/plugin.json",
|
|
135
|
-
bundledPlugin: {
|
|
136
|
-
ref: "agent-relay-claude-plugin",
|
|
137
|
-
dir: "runner/plugins/claude",
|
|
138
|
-
title: "Agent Relay Claude plugin",
|
|
139
|
-
description: "Bundled Agent Relay Claude plugin, materialized whole through the provisioning registry.",
|
|
140
|
-
omitFiles: ["monitors/relay-monitor.provisioned.mjs"],
|
|
141
|
-
fileOverrides: [{ path: "monitors/relay-monitor.ts", sourcePath: "monitors/relay-monitor.provisioned.mjs" }]
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
upgrade: {
|
|
145
|
-
packages: ["agent-relay-runner", "agent-relay-plugin"],
|
|
146
|
-
detectPackages: ["agent-relay-runner"],
|
|
147
|
-
detectCommands: ["claude"]
|
|
148
|
-
},
|
|
149
|
-
probe: {
|
|
150
|
-
command: "claude",
|
|
151
|
-
args: ["--version"],
|
|
152
|
-
healthCheck: "claude --version",
|
|
153
|
-
featureChecks: [{ name: "rig", command: "claude-rig", homeDir: ".claude-rig" }]
|
|
154
|
-
},
|
|
155
|
-
quotaPoll: { strategy: "none" },
|
|
156
|
-
shim: { kind: "claude-relay" }
|
|
157
|
-
};
|
|
158
|
-
registerProvider(claudeProviderManifest);
|
|
159
|
-
|
|
160
|
-
// providers/codex.ts
|
|
161
|
-
var CODEX_REASONING = ["low", "medium", "high", "xhigh"];
|
|
162
|
-
var CONTEXT_200K2 = { value: 200000, source: "catalog", confidence: "declared" };
|
|
163
|
-
var CODE_MODEL_CAPABILITIES2 = {
|
|
164
|
-
modalities: {
|
|
165
|
-
input: { text: true, image: true },
|
|
166
|
-
output: { text: true }
|
|
167
|
-
},
|
|
168
|
-
tools: {
|
|
169
|
-
code: true,
|
|
170
|
-
review: true,
|
|
171
|
-
debug: true,
|
|
172
|
-
refactor: true
|
|
173
|
-
},
|
|
174
|
-
source: "catalog",
|
|
175
|
-
confidence: "declared"
|
|
176
|
-
};
|
|
177
|
-
var quotaWindows2 = [
|
|
178
|
-
{ name: "primary", unit: "%", reset: true },
|
|
179
|
-
{ name: "secondary", unit: "%", reset: true }
|
|
180
|
-
];
|
|
181
|
-
function codeModel2(limits) {
|
|
182
|
-
return {
|
|
183
|
-
...limits ? { limits } : {},
|
|
184
|
-
capabilities: CODE_MODEL_CAPABILITIES2
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
var codexProviderManifest = {
|
|
188
|
-
id: "codex",
|
|
189
|
-
label: "Codex",
|
|
190
|
-
icon: "\u2328\uFE0F",
|
|
191
|
-
launch: {
|
|
192
|
-
managedPolicyPrompt: {
|
|
193
|
-
alwaysOn: "prompt"
|
|
194
|
-
},
|
|
195
|
-
taskHydration: {
|
|
196
|
-
channel: "prompt"
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
catalog: {
|
|
200
|
-
defaultModel: "gpt-5.5",
|
|
201
|
-
models: [
|
|
202
|
-
{ alias: "gpt-5.5", label: "GPT-5.5", providerModel: "gpt-5.5", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
203
|
-
{ alias: "gpt-5.4", label: "GPT-5.4", providerModel: "gpt-5.4", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
204
|
-
{ alias: "gpt-5.4-mini", label: "GPT-5.4 Mini", providerModel: "gpt-5.4-mini", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
205
|
-
{ alias: "gpt-5.3-codex", label: "GPT-5.3 Codex", providerModel: "gpt-5.3-codex", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
206
|
-
{ alias: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark", providerModel: "gpt-5.3-codex-spark", efforts: CODEX_REASONING, defaultEffort: "high", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
207
|
-
{ alias: "gpt-5.2", label: "GPT-5.2", providerModel: "gpt-5.2", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) }
|
|
208
|
-
]
|
|
209
|
-
},
|
|
210
|
-
quotaWindows: [...quotaWindows2],
|
|
211
|
-
capabilityDefaults: {
|
|
212
|
-
lifecycle: {
|
|
213
|
-
managed: true,
|
|
214
|
-
shutdownHard: true,
|
|
215
|
-
restartHard: true,
|
|
216
|
-
semanticStatus: true,
|
|
217
|
-
reconnect: true
|
|
218
|
-
},
|
|
219
|
-
session: {
|
|
220
|
-
fileRead: true,
|
|
221
|
-
fileWrite: { when: "approval:not-read-only", value: true, otherwise: false },
|
|
222
|
-
shell: { when: "approval:not-read-only", value: true, otherwise: false },
|
|
223
|
-
shellMode: { open: "unrestricted", guarded: "guarded", readOnly: "none" }
|
|
224
|
-
},
|
|
225
|
-
context: {
|
|
226
|
-
managed: "always",
|
|
227
|
-
compact: true,
|
|
228
|
-
clear: true,
|
|
229
|
-
inject: true,
|
|
230
|
-
resumePreference: ["native", "clear-inject", "none"]
|
|
231
|
-
},
|
|
232
|
-
quota: {
|
|
233
|
-
supported: true,
|
|
234
|
-
source: "probe",
|
|
235
|
-
unit: "%",
|
|
236
|
-
windows: [...quotaWindows2]
|
|
237
|
-
},
|
|
238
|
-
terminal: {
|
|
239
|
-
mode: "attach",
|
|
240
|
-
attach: {
|
|
241
|
-
create: true,
|
|
242
|
-
read: true,
|
|
243
|
-
write: true,
|
|
244
|
-
detach: true
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
liveSession: {
|
|
248
|
-
capture: true,
|
|
249
|
-
inject: true,
|
|
250
|
-
interrupt: true,
|
|
251
|
-
promptEcho: true,
|
|
252
|
-
reasoning: true,
|
|
253
|
-
slashCommands: true
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
slashCommands: ["/compact", "/new"],
|
|
257
|
-
resume: { supported: true, idFormat: "codex-thread-id", idPattern: "[\\w-]+" },
|
|
258
|
-
home: { configDir: ".codex", envPrefix: "CODEX", configDirEnvVar: "CODEX_HOME", quotaCredentialFile: "auth.json", authItems: ["auth.json", "installation_id"], globalInstructionFiles: ["AGENTS.md"] },
|
|
259
|
-
provisioning: {
|
|
260
|
-
pluginManifestPath: ".codex-plugin/plugin.json",
|
|
261
|
-
bundledSkills: {
|
|
262
|
-
refPrefix: "agent-relay-codex-",
|
|
263
|
-
dir: "runner/plugins/codex/skills",
|
|
264
|
-
titlePrefix: "Agent Relay Codex skill: ",
|
|
265
|
-
description: "Bundled Agent Relay Codex skill, materialized through the provisioning registry.",
|
|
266
|
-
entryFile: "SKILL.md"
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
upgrade: {
|
|
270
|
-
packages: ["agent-relay-runner", "agent-relay-codex"],
|
|
271
|
-
detectPackages: ["agent-relay-runner", "agent-relay-codex"],
|
|
272
|
-
detectCommands: ["codex-relay", "agent-relay-runner"]
|
|
273
|
-
},
|
|
274
|
-
probe: {
|
|
275
|
-
command: "codex",
|
|
276
|
-
args: ["--version"],
|
|
277
|
-
healthCheck: "codex --version"
|
|
278
|
-
},
|
|
279
|
-
quotaPoll: { strategy: "codex-app-server" },
|
|
280
|
-
shim: { kind: "codex-relay", extraBinPaths: ["codex/bin/codex"], cleanLegacyHooks: true }
|
|
281
|
-
};
|
|
282
|
-
registerProvider(codexProviderManifest);
|
|
283
|
-
// sdk/src/types/agents.ts
|
|
284
|
-
var SPAWN_PROVIDERS = getAllManifests().map((manifest) => manifest.id);
|
|
285
|
-
// sdk/src/provider-catalog.ts
|
|
286
|
-
function manifestToCatalogEntry(manifest) {
|
|
287
|
-
return {
|
|
288
|
-
provider: manifest.id,
|
|
289
|
-
label: manifest.label,
|
|
290
|
-
defaultModel: manifest.catalog.defaultModel,
|
|
291
|
-
models: manifest.catalog.models
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
var PROVIDER_CATALOG = Object.fromEntries(getAllManifests().map((manifest) => [manifest.id, manifestToCatalogEntry(manifest)]));
|
|
295
|
-
// sdk/src/reconnect.ts
|
|
296
|
-
import { setTimeout as delay } from "timers/promises";
|
|
297
|
-
var DEFAULTS = {
|
|
298
|
-
initialMs: 1000,
|
|
299
|
-
maxMs: 30000,
|
|
300
|
-
jitterMs: 500
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
class ReconnectionManager {
|
|
304
|
-
attempts = 0;
|
|
305
|
-
abort;
|
|
306
|
-
options;
|
|
307
|
-
constructor(options = {}) {
|
|
308
|
-
this.options = {
|
|
309
|
-
initialMs: options.initialMs ?? DEFAULTS.initialMs,
|
|
310
|
-
maxMs: options.maxMs ?? DEFAULTS.maxMs,
|
|
311
|
-
jitterMs: options.jitterMs ?? DEFAULTS.jitterMs
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
get attempt() {
|
|
315
|
-
return this.attempts;
|
|
316
|
-
}
|
|
317
|
-
nextDelay() {
|
|
318
|
-
const base = Math.min(this.options.maxMs, this.options.initialMs * 2 ** this.attempts);
|
|
319
|
-
this.attempts += 1;
|
|
320
|
-
const jitter = this.options.jitterMs > 0 ? Math.floor(Math.random() * (this.options.jitterMs + 1)) : 0;
|
|
321
|
-
return Math.min(this.options.maxMs, base + jitter);
|
|
322
|
-
}
|
|
323
|
-
reset() {
|
|
324
|
-
this.attempts = 0;
|
|
325
|
-
this.cancel();
|
|
326
|
-
}
|
|
327
|
-
async schedule() {
|
|
328
|
-
this.cancel();
|
|
329
|
-
this.abort = new AbortController;
|
|
330
|
-
const ms = this.nextDelay();
|
|
331
|
-
await delay(ms, undefined, { signal: this.abort.signal }).catch((error) => {
|
|
332
|
-
if (error.name !== "AbortError")
|
|
333
|
-
throw error;
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
cancel() {
|
|
337
|
-
this.abort?.abort();
|
|
338
|
-
this.abort = undefined;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// sdk/src/config.ts
|
|
342
|
-
function relayTokenFromEnv() {
|
|
343
|
-
return process.env.AGENT_RELAY_TOKEN;
|
|
344
|
-
}
|
|
345
|
-
// sdk/src/bus-client.ts
|
|
346
|
-
import { EventEmitter } from "events";
|
|
347
|
-
function unrefTimer(timer) {
|
|
348
|
-
timer?.unref?.();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
class MemoryCursorStore {
|
|
352
|
-
value = null;
|
|
353
|
-
async load() {
|
|
354
|
-
return this.value;
|
|
355
|
-
}
|
|
356
|
-
async save(cursor) {
|
|
357
|
-
this.value = cursor;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
class RelayBusClient extends EventEmitter {
|
|
362
|
-
options;
|
|
363
|
-
ws = null;
|
|
364
|
-
registeredEpoch = 0;
|
|
365
|
-
lastCursor = 0;
|
|
366
|
-
serverCursor = 0;
|
|
367
|
-
closed = false;
|
|
368
|
-
connecting = null;
|
|
369
|
-
heartbeat;
|
|
370
|
-
heartbeatWatchdog;
|
|
371
|
-
pendingHeartbeatId;
|
|
372
|
-
pendingHeartbeatSentAt;
|
|
373
|
-
lastServerFrameAt = 0;
|
|
374
|
-
lastHeartbeatAckAt = 0;
|
|
375
|
-
heartbeatAckSupported = false;
|
|
376
|
-
semanticStatus;
|
|
377
|
-
transport = { connected: false, reconnecting: false };
|
|
378
|
-
reconnect;
|
|
379
|
-
cursorStore;
|
|
380
|
-
subscriptions = new Set(["message.*", "agent.status", "task.*"]);
|
|
381
|
-
pending = new Map;
|
|
382
|
-
constructor(options) {
|
|
383
|
-
super();
|
|
384
|
-
this.options = options;
|
|
385
|
-
this.reconnect = new ReconnectionManager({
|
|
386
|
-
initialMs: options.reconnect?.initialMs,
|
|
387
|
-
maxMs: options.reconnect?.maxMs,
|
|
388
|
-
jitterMs: options.reconnect?.jitterMs
|
|
389
|
-
});
|
|
390
|
-
this.cursorStore = options.cursorStore ?? new MemoryCursorStore;
|
|
391
|
-
this.semanticStatus = options.initialStatus ?? "idle";
|
|
392
|
-
}
|
|
393
|
-
get connected() {
|
|
394
|
-
return this.ws?.readyState === WebSocket.OPEN && this.registeredEpoch > 0;
|
|
395
|
-
}
|
|
396
|
-
get epoch() {
|
|
397
|
-
return this.registeredEpoch;
|
|
398
|
-
}
|
|
399
|
-
get cursor() {
|
|
400
|
-
return this.lastCursor;
|
|
401
|
-
}
|
|
402
|
-
get transportState() {
|
|
403
|
-
return { ...this.transport };
|
|
404
|
-
}
|
|
405
|
-
async connect() {
|
|
406
|
-
if (this.connecting)
|
|
407
|
-
return this.connecting;
|
|
408
|
-
this.closed = false;
|
|
409
|
-
this.connecting = this.connectLoop().finally(() => {
|
|
410
|
-
this.connecting = null;
|
|
411
|
-
});
|
|
412
|
-
return this.connecting;
|
|
413
|
-
}
|
|
414
|
-
async close() {
|
|
415
|
-
this.closed = true;
|
|
416
|
-
this.stopHeartbeat();
|
|
417
|
-
this.stopHeartbeatWatchdog();
|
|
418
|
-
this.reconnect.cancel();
|
|
419
|
-
for (const pending of this.pending.values())
|
|
420
|
-
pending.reject(new Error("bus client closed"));
|
|
421
|
-
this.pending.clear();
|
|
422
|
-
if (this.ws && this.ws.readyState <= WebSocket.OPEN)
|
|
423
|
-
this.ws.close();
|
|
424
|
-
this.ws = null;
|
|
425
|
-
}
|
|
426
|
-
setToken(token) {
|
|
427
|
-
this.options.token = token;
|
|
428
|
-
}
|
|
429
|
-
reconnectTransport(reason = "client requested reconnect") {
|
|
430
|
-
this.forceReconnect(reason, { emitError: false, code: 4001 });
|
|
431
|
-
}
|
|
432
|
-
status(update) {
|
|
433
|
-
if (update.agentStatus === "idle" || update.agentStatus === "busy")
|
|
434
|
-
this.semanticStatus = update.agentStatus;
|
|
435
|
-
this.send({
|
|
436
|
-
type: "status",
|
|
437
|
-
payload: {
|
|
438
|
-
agentStatus: update.agentStatus,
|
|
439
|
-
ready: update.ready,
|
|
440
|
-
meta: update.meta
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
async statusAsync(update) {
|
|
445
|
-
this.status(update);
|
|
446
|
-
await Promise.resolve();
|
|
447
|
-
}
|
|
448
|
-
setSemanticStatus(status) {
|
|
449
|
-
this.semanticStatus = status;
|
|
450
|
-
}
|
|
451
|
-
updateCommand(commandId, params) {
|
|
452
|
-
return this.command("command.update", commandId, params);
|
|
453
|
-
}
|
|
454
|
-
command(type, target, params) {
|
|
455
|
-
const id = crypto.randomUUID();
|
|
456
|
-
this.send({ type: "command", id, payload: { commandType: type, target, params } });
|
|
457
|
-
return new Promise((resolve, reject) => {
|
|
458
|
-
this.pending.set(id, { resolve, reject });
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
async subscribe(events, scopes) {
|
|
462
|
-
for (const event of events)
|
|
463
|
-
this.subscriptions.add(event);
|
|
464
|
-
const id = crypto.randomUUID();
|
|
465
|
-
this.send({ type: "subscribe", id, payload: { events, scopes } });
|
|
466
|
-
await Promise.resolve();
|
|
467
|
-
}
|
|
468
|
-
on(event, listener) {
|
|
469
|
-
return super.on(event, listener);
|
|
470
|
-
}
|
|
471
|
-
off(event, listener) {
|
|
472
|
-
return super.off(event, listener);
|
|
473
|
-
}
|
|
474
|
-
once(event, listener) {
|
|
475
|
-
return super.once(event, listener);
|
|
476
|
-
}
|
|
477
|
-
async connectLoop() {
|
|
478
|
-
while (!this.closed) {
|
|
479
|
-
try {
|
|
480
|
-
await this.openOnce();
|
|
481
|
-
this.reconnect.reset();
|
|
482
|
-
return;
|
|
483
|
-
} catch (error) {
|
|
484
|
-
if (this.closed)
|
|
485
|
-
throw error;
|
|
486
|
-
const delayMs = this.reconnect.nextDelay();
|
|
487
|
-
this.transport = { ...this.transport, connected: false, reconnecting: true };
|
|
488
|
-
this.emit("reconnecting", this.reconnect.attempt, delayMs);
|
|
489
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
async openOnce() {
|
|
494
|
-
const cursor = await this.cursorStore.load();
|
|
495
|
-
if (cursor !== null)
|
|
496
|
-
this.lastCursor = cursor;
|
|
497
|
-
await new Promise((resolve, reject) => {
|
|
498
|
-
const ws = new WebSocket(this.wsUrl());
|
|
499
|
-
this.ws = ws;
|
|
500
|
-
let resolved = false;
|
|
501
|
-
ws.onopen = () => {
|
|
502
|
-
if (this.ws !== ws)
|
|
503
|
-
return;
|
|
504
|
-
const now = Date.now();
|
|
505
|
-
this.lastServerFrameAt = now;
|
|
506
|
-
this.transport = { ...this.transport, connected: true, reconnecting: false, lastConnectedAt: now, lastServerFrameAt: now, reconnectReason: undefined };
|
|
507
|
-
this.emit("connected");
|
|
508
|
-
this.sendRegister();
|
|
509
|
-
};
|
|
510
|
-
ws.onmessage = (message) => {
|
|
511
|
-
if (this.ws !== ws)
|
|
512
|
-
return;
|
|
513
|
-
this.handleMessage(message.data, () => {
|
|
514
|
-
if (!resolved) {
|
|
515
|
-
resolved = true;
|
|
516
|
-
resolve();
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
};
|
|
520
|
-
ws.onerror = () => {
|
|
521
|
-
if (this.ws !== ws)
|
|
522
|
-
return;
|
|
523
|
-
if (!resolved)
|
|
524
|
-
reject(new Error("bus connection failed"));
|
|
525
|
-
};
|
|
526
|
-
ws.onclose = (event) => {
|
|
527
|
-
if (this.ws !== ws)
|
|
528
|
-
return;
|
|
529
|
-
this.stopHeartbeat();
|
|
530
|
-
this.stopHeartbeatWatchdog();
|
|
531
|
-
this.registeredEpoch = 0;
|
|
532
|
-
this.heartbeatAckSupported = false;
|
|
533
|
-
this.transport = { ...this.transport, connected: false, reconnecting: !this.closed, lastDisconnectedAt: Date.now() };
|
|
534
|
-
const reason = event.reason || `closed:${event.code}`;
|
|
535
|
-
this.emit("disconnected", reason);
|
|
536
|
-
if (!resolved) {
|
|
537
|
-
resolved = true;
|
|
538
|
-
reject(new Error(reason));
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (!this.closed)
|
|
542
|
-
this.connect();
|
|
543
|
-
};
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
sendRegister() {
|
|
547
|
-
this.send({
|
|
548
|
-
type: "register",
|
|
549
|
-
id: crypto.randomUUID(),
|
|
550
|
-
payload: {
|
|
551
|
-
role: this.options.role,
|
|
552
|
-
componentId: this.options.componentId,
|
|
553
|
-
agentId: this.options.agentId,
|
|
554
|
-
instanceId: this.options.instanceId,
|
|
555
|
-
capabilities: this.options.capabilities ?? [],
|
|
556
|
-
tags: this.options.tags ?? [],
|
|
557
|
-
machine: this.options.machine ?? "unknown",
|
|
558
|
-
meta: this.registrationMeta()
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
async handleMessage(data, onRegistered) {
|
|
563
|
-
this.recordServerFrame();
|
|
564
|
-
let frame;
|
|
565
|
-
try {
|
|
566
|
-
frame = JSON.parse(typeof data === "string" ? data : String(data));
|
|
567
|
-
} catch {
|
|
568
|
-
this.emit("error", "INVALID_JSON", "server sent invalid JSON");
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
if (frame.type === "registered") {
|
|
572
|
-
const payload = frame.payload;
|
|
573
|
-
const resumeFrom = this.lastCursor;
|
|
574
|
-
this.registeredEpoch = payload.epoch;
|
|
575
|
-
this.serverCursor = payload.cursor;
|
|
576
|
-
this.heartbeatAckSupported = payload.features?.heartbeatAck === true;
|
|
577
|
-
this.emit("registered", payload.epoch, payload.cursor);
|
|
578
|
-
this.send({ type: "subscribe", id: crypto.randomUUID(), payload: { events: [...this.subscriptions] } });
|
|
579
|
-
if (resumeFrom > 0 && resumeFrom < payload.cursor) {
|
|
580
|
-
this.send({ type: "resume", id: crypto.randomUUID(), payload: { since: resumeFrom } });
|
|
581
|
-
} else {
|
|
582
|
-
this.lastCursor = payload.cursor;
|
|
583
|
-
await this.cursorStore.save(this.lastCursor);
|
|
584
|
-
}
|
|
585
|
-
this.startHeartbeat();
|
|
586
|
-
if (this.heartbeatAckSupported)
|
|
587
|
-
this.startHeartbeatWatchdog();
|
|
588
|
-
else
|
|
589
|
-
this.stopHeartbeatWatchdog();
|
|
590
|
-
onRegistered();
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
if (frame.type === "ack") {
|
|
594
|
-
if (frame.payload.frameId === this.pendingHeartbeatId) {
|
|
595
|
-
this.lastHeartbeatAckAt = Date.now();
|
|
596
|
-
this.pendingHeartbeatId = undefined;
|
|
597
|
-
this.pendingHeartbeatSentAt = undefined;
|
|
598
|
-
this.transport = { ...this.transport, lastHeartbeatAckAt: this.lastHeartbeatAckAt };
|
|
599
|
-
}
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
if (frame.type === "event") {
|
|
603
|
-
await this.handleEvent(frame.payload);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
if (frame.type === "resumed") {
|
|
607
|
-
for (const event of frame.payload.events)
|
|
608
|
-
await this.handleEvent(event);
|
|
609
|
-
if (frame.payload.truncated) {
|
|
610
|
-
const nextSince = typeof frame.payload.nextSince === "number" ? frame.payload.nextSince : frame.payload.toSeq;
|
|
611
|
-
this.send({ type: "resume", id: crypto.randomUUID(), payload: { since: nextSince } });
|
|
612
|
-
}
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
if (frame.type === "command.result") {
|
|
616
|
-
const pending = this.pending.get(frame.payload.commandId);
|
|
617
|
-
if (!pending)
|
|
618
|
-
return;
|
|
619
|
-
this.pending.delete(frame.payload.commandId);
|
|
620
|
-
if (frame.payload.status === "succeeded")
|
|
621
|
-
pending.resolve(frame.payload.result ?? null);
|
|
622
|
-
else
|
|
623
|
-
pending.reject(new Error(frame.payload.error || frame.payload.status));
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
if (frame.type === "error") {
|
|
627
|
-
const error = frame;
|
|
628
|
-
if (error.payload.code === "REPLAY_GAP") {
|
|
629
|
-
await this.options.reconcile?.();
|
|
630
|
-
this.lastCursor = this.serverCursor;
|
|
631
|
-
await this.cursorStore.save(this.lastCursor);
|
|
632
|
-
}
|
|
633
|
-
this.emit("error", error.payload.code, error.payload.message);
|
|
634
|
-
if (error.payload.frameId) {
|
|
635
|
-
const pending = this.pending.get(error.payload.frameId);
|
|
636
|
-
if (pending) {
|
|
637
|
-
this.pending.delete(error.payload.frameId);
|
|
638
|
-
pending.reject(new Error(error.payload.message));
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
async handleEvent(event) {
|
|
644
|
-
this.lastCursor = Math.max(this.lastCursor, event.seq);
|
|
645
|
-
await this.cursorStore.save(this.lastCursor);
|
|
646
|
-
this.emit("event", event.eventType, event.data, event.seq);
|
|
647
|
-
if (event.eventType === "command.requested") {
|
|
648
|
-
const command = isRecord(event.data.command) ? event.data.command : undefined;
|
|
649
|
-
if (command) {
|
|
650
|
-
const commandType = typeof command.type === "string" ? command.type : "";
|
|
651
|
-
const commandId = typeof command.id === "string" ? command.id : "";
|
|
652
|
-
const params = isRecord(command.params) ? command.params : {};
|
|
653
|
-
this.emit("command", commandType, params, commandId, command);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
this.emit(event.eventType, event.data);
|
|
657
|
-
if (event.eventType.startsWith("task."))
|
|
658
|
-
this.emit("task.changed", event.data);
|
|
659
|
-
}
|
|
660
|
-
startHeartbeat() {
|
|
661
|
-
this.stopHeartbeat();
|
|
662
|
-
this.sendHeartbeat();
|
|
663
|
-
this.heartbeat = setInterval(() => {
|
|
664
|
-
this.sendHeartbeat();
|
|
665
|
-
}, this.options.heartbeatIntervalMs ?? 30000);
|
|
666
|
-
unrefTimer(this.heartbeat);
|
|
667
|
-
}
|
|
668
|
-
stopHeartbeat() {
|
|
669
|
-
if (this.heartbeat)
|
|
670
|
-
clearInterval(this.heartbeat);
|
|
671
|
-
this.heartbeat = undefined;
|
|
672
|
-
this.pendingHeartbeatId = undefined;
|
|
673
|
-
this.pendingHeartbeatSentAt = undefined;
|
|
674
|
-
}
|
|
675
|
-
startHeartbeatWatchdog() {
|
|
676
|
-
this.stopHeartbeatWatchdog();
|
|
677
|
-
const intervalMs = Math.max(100, Math.min(5000, this.heartbeatIntervalMs()));
|
|
678
|
-
this.heartbeatWatchdog = setInterval(() => this.checkHeartbeatAck(), intervalMs);
|
|
679
|
-
unrefTimer(this.heartbeatWatchdog);
|
|
680
|
-
}
|
|
681
|
-
stopHeartbeatWatchdog() {
|
|
682
|
-
if (this.heartbeatWatchdog)
|
|
683
|
-
clearInterval(this.heartbeatWatchdog);
|
|
684
|
-
this.heartbeatWatchdog = undefined;
|
|
685
|
-
}
|
|
686
|
-
sendHeartbeat() {
|
|
687
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.registeredEpoch)
|
|
688
|
-
return;
|
|
689
|
-
const now = Date.now();
|
|
690
|
-
const expectAck = this.heartbeatAckSupported;
|
|
691
|
-
if (expectAck && this.pendingHeartbeatId && this.pendingHeartbeatSentAt && now - this.pendingHeartbeatSentAt < this.heartbeatIntervalMs())
|
|
692
|
-
return;
|
|
693
|
-
const id = expectAck ? crypto.randomUUID() : undefined;
|
|
694
|
-
const meta = this.heartbeatMeta();
|
|
695
|
-
if (expectAck && id) {
|
|
696
|
-
this.pendingHeartbeatId = id;
|
|
697
|
-
if (!this.pendingHeartbeatSentAt)
|
|
698
|
-
this.pendingHeartbeatSentAt = now;
|
|
699
|
-
}
|
|
700
|
-
this.send({ type: "heartbeat", ...id ? { id } : {}, payload: { status: this.semanticStatus, ...meta ? { meta } : {} } });
|
|
701
|
-
}
|
|
702
|
-
checkHeartbeatAck() {
|
|
703
|
-
if (!this.pendingHeartbeatSentAt || !this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
704
|
-
return;
|
|
705
|
-
const ageMs = Date.now() - this.pendingHeartbeatSentAt;
|
|
706
|
-
if (ageMs <= this.heartbeatAckTimeoutMs())
|
|
707
|
-
return;
|
|
708
|
-
this.forceReconnect(`heartbeat ack timeout after ${ageMs}ms`, { emitError: true, code: 4000 });
|
|
709
|
-
}
|
|
710
|
-
forceReconnect(reason, opts) {
|
|
711
|
-
if (this.closed)
|
|
712
|
-
return;
|
|
713
|
-
this.transport = { ...this.transport, connected: false, reconnecting: true, lastDisconnectedAt: Date.now(), reconnectReason: reason };
|
|
714
|
-
if (opts.emitError)
|
|
715
|
-
this.emit("error", "HEARTBEAT_TIMEOUT", reason);
|
|
716
|
-
this.stopHeartbeat();
|
|
717
|
-
this.stopHeartbeatWatchdog();
|
|
718
|
-
const ws = this.ws;
|
|
719
|
-
this.ws = null;
|
|
720
|
-
this.registeredEpoch = 0;
|
|
721
|
-
this.heartbeatAckSupported = false;
|
|
722
|
-
if (ws && ws.readyState <= WebSocket.OPEN)
|
|
723
|
-
ws.close(opts.code, reason.slice(0, 120));
|
|
724
|
-
this.connect();
|
|
725
|
-
}
|
|
726
|
-
heartbeatIntervalMs() {
|
|
727
|
-
return this.options.heartbeatIntervalMs ?? 30000;
|
|
728
|
-
}
|
|
729
|
-
heartbeatAckTimeoutMs() {
|
|
730
|
-
return this.options.heartbeatAckTimeoutMs ?? Math.max(1e4, Math.ceil(this.heartbeatIntervalMs() * 1.5));
|
|
731
|
-
}
|
|
732
|
-
recordServerFrame() {
|
|
733
|
-
this.lastServerFrameAt = Date.now();
|
|
734
|
-
this.transport = { ...this.transport, lastServerFrameAt: this.lastServerFrameAt };
|
|
735
|
-
}
|
|
736
|
-
send(frame) {
|
|
737
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
738
|
-
return;
|
|
739
|
-
this.ws.send(JSON.stringify(frame));
|
|
740
|
-
}
|
|
741
|
-
wsUrl() {
|
|
742
|
-
const url = new URL(this.options.url);
|
|
743
|
-
const token = this.options.token ?? relayTokenFromEnv();
|
|
744
|
-
if (token && !url.searchParams.has("token"))
|
|
745
|
-
url.searchParams.set("token", token);
|
|
746
|
-
return url.toString();
|
|
747
|
-
}
|
|
748
|
-
registrationMeta() {
|
|
749
|
-
return {
|
|
750
|
-
...this.options.meta ?? {},
|
|
751
|
-
...this.options.providerCapabilities ? { providerCapabilities: this.options.providerCapabilities } : {},
|
|
752
|
-
...this.options.context ? { context: this.options.context } : {}
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
heartbeatMeta() {
|
|
756
|
-
const dynamic = this.options.heartbeatMeta?.();
|
|
757
|
-
const meta = {
|
|
758
|
-
...dynamic ?? {},
|
|
759
|
-
...this.options.providerCapabilities ? { providerCapabilities: this.options.providerCapabilities } : {},
|
|
760
|
-
...this.options.context ? { context: this.options.context } : {}
|
|
761
|
-
};
|
|
762
|
-
return Object.keys(meta).length ? meta : undefined;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
// sdk/src/provider-base.ts
|
|
766
|
-
function unrefTimer2(timer) {
|
|
767
|
-
timer?.unref?.();
|
|
768
|
-
}
|
|
769
6
|
|
|
770
|
-
|
|
771
|
-
options;
|
|
772
|
-
client;
|
|
773
|
-
running = false;
|
|
774
|
-
pending = new Map;
|
|
775
|
-
drain;
|
|
776
|
-
constructor(options) {
|
|
777
|
-
this.options = options;
|
|
778
|
-
this.client = new RelayBusClient({
|
|
779
|
-
url: options.relayUrl,
|
|
780
|
-
role: "provider",
|
|
781
|
-
componentId: `${options.adapter.providerName}:${options.agentId}`,
|
|
782
|
-
agentId: options.agentId,
|
|
783
|
-
instanceId: options.instanceId,
|
|
784
|
-
token: options.token,
|
|
785
|
-
capabilities: options.adapter.capabilities,
|
|
786
|
-
tags: options.tags ?? [options.adapter.providerName],
|
|
787
|
-
machine: options.machine,
|
|
788
|
-
meta: { provider: options.adapter.providerName, ...options.meta ?? {} }
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
async run() {
|
|
792
|
-
if (this.running)
|
|
793
|
-
return;
|
|
794
|
-
this.running = true;
|
|
795
|
-
this.client.on("message.new", (message) => this.queueMessage(message));
|
|
796
|
-
await this.client.connect();
|
|
797
|
-
this.startStatusLoop();
|
|
798
|
-
}
|
|
799
|
-
async stop() {
|
|
800
|
-
this.running = false;
|
|
801
|
-
if (this.drain)
|
|
802
|
-
clearInterval(this.drain);
|
|
803
|
-
this.drain = undefined;
|
|
804
|
-
await this.client.close();
|
|
805
|
-
}
|
|
806
|
-
queueMessage(message) {
|
|
807
|
-
if (message.to !== this.options.agentId && message.to !== "broadcast" && !message.to.startsWith("tag:") && !message.to.startsWith("cap:") && !message.to.startsWith("label:") && !message.to.startsWith("team:")) {
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
this.pending.set(message.id, message);
|
|
811
|
-
this.drainMessages();
|
|
812
|
-
}
|
|
813
|
-
async drainMessages() {
|
|
814
|
-
if (!this.running || this.pending.size === 0)
|
|
815
|
-
return;
|
|
816
|
-
const messages2 = [...this.pending.values()].sort((a, b) => a.id - b.id);
|
|
817
|
-
this.pending.clear();
|
|
818
|
-
await this.options.adapter.deliverMessage(this.options.agentId, messages2);
|
|
819
|
-
}
|
|
820
|
-
startStatusLoop() {
|
|
821
|
-
this.drain = setInterval(async () => {
|
|
822
|
-
if (!this.running)
|
|
823
|
-
return;
|
|
824
|
-
try {
|
|
825
|
-
const status = await this.options.adapter.getAgentStatus(this.options.agentId);
|
|
826
|
-
this.client.status({ agentStatus: status, ready: true });
|
|
827
|
-
} catch {
|
|
828
|
-
this.client.status({ agentStatus: "online", ready: false });
|
|
829
|
-
}
|
|
830
|
-
}, 30000);
|
|
831
|
-
unrefTimer2(this.drain);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
// sdk/src/claim-tracker.ts
|
|
835
|
-
class WorkClaimTracker {
|
|
836
|
-
providerActive = false;
|
|
837
|
-
claims = new Map;
|
|
838
|
-
setProviderActive(active) {
|
|
839
|
-
this.providerActive = active;
|
|
840
|
-
}
|
|
841
|
-
start(kind, id, expiresAt) {
|
|
842
|
-
this.claims.set(`${kind}:${id}`, { kind, id, startedAt: Date.now(), expiresAt });
|
|
843
|
-
}
|
|
844
|
-
finish(kind, id) {
|
|
845
|
-
this.claims.delete(`${kind}:${id}`);
|
|
846
|
-
}
|
|
847
|
-
expire(now = Date.now()) {
|
|
848
|
-
for (const [key, claim] of this.claims) {
|
|
849
|
-
if (claim.expiresAt !== undefined && claim.expiresAt <= now)
|
|
850
|
-
this.claims.delete(key);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
status() {
|
|
854
|
-
return this.providerActive || this.claims.size > 0 ? "busy" : "idle";
|
|
855
|
-
}
|
|
856
|
-
active() {
|
|
857
|
-
return [...this.claims.values()];
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
// sdk/src/process-utils.ts
|
|
861
|
-
import { execFile } from "child_process";
|
|
862
|
-
import { promisify } from "util";
|
|
863
|
-
var execFileAsync = promisify(execFile);
|
|
864
|
-
// sdk/src/teams-config.ts
|
|
865
|
-
var TEAM_IDLE_THRESHOLD_MINUTES_MAX = 24 * 60;
|
|
866
|
-
// sdk/src/admission/semaphore.ts
|
|
867
|
-
class Semaphore {
|
|
868
|
-
#limit;
|
|
869
|
-
#inFlight = 0;
|
|
870
|
-
#waiters = [];
|
|
871
|
-
constructor(limit) {
|
|
872
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
873
|
-
throw new RangeError(`Semaphore limit must be an integer >= 1, got ${limit}`);
|
|
874
|
-
}
|
|
875
|
-
this.#limit = limit;
|
|
876
|
-
}
|
|
877
|
-
get limit() {
|
|
878
|
-
return this.#limit;
|
|
879
|
-
}
|
|
880
|
-
get inFlight() {
|
|
881
|
-
return this.#inFlight;
|
|
882
|
-
}
|
|
883
|
-
get waiting() {
|
|
884
|
-
return this.#waiters.length;
|
|
885
|
-
}
|
|
886
|
-
get available() {
|
|
887
|
-
return this.#limit - this.#inFlight;
|
|
888
|
-
}
|
|
889
|
-
tryAcquire() {
|
|
890
|
-
if (this.#inFlight < this.#limit) {
|
|
891
|
-
this.#inFlight++;
|
|
892
|
-
return true;
|
|
893
|
-
}
|
|
894
|
-
return false;
|
|
895
|
-
}
|
|
896
|
-
acquire() {
|
|
897
|
-
if (this.tryAcquire())
|
|
898
|
-
return Promise.resolve();
|
|
899
|
-
return new Promise((resolve) => {
|
|
900
|
-
this.#waiters.push(resolve);
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
release() {
|
|
904
|
-
const next = this.#waiters.shift();
|
|
905
|
-
if (next) {
|
|
906
|
-
next();
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
if (this.#inFlight > 0)
|
|
910
|
-
this.#inFlight--;
|
|
911
|
-
}
|
|
912
|
-
async run(fn) {
|
|
913
|
-
await this.acquire();
|
|
914
|
-
try {
|
|
915
|
-
return await fn();
|
|
916
|
-
} finally {
|
|
917
|
-
this.release();
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
// runner/src/config.ts
|
|
7
|
+
// runner/src/message-body-config.ts
|
|
922
8
|
function messageBodyMaxCharsFromEnv() {
|
|
923
9
|
return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
|
|
924
10
|
}
|
|
@@ -1085,6 +171,38 @@ function providerMessageBodyMaxChars() {
|
|
|
1085
171
|
}
|
|
1086
172
|
return DEFAULT_PROVIDER_MESSAGE_BODY_MAX_CHARS;
|
|
1087
173
|
}
|
|
174
|
+
function previewDeliveredBody(rawBody, messageId, maxChars = providerMessageBodyMaxChars()) {
|
|
175
|
+
if (rawBody.length <= maxChars)
|
|
176
|
+
return { body: rawBody, truncated: false, guidance: [] };
|
|
177
|
+
const fetchLines = [
|
|
178
|
+
`Read full: agent-relay get-message ${messageId}`,
|
|
179
|
+
`Body only: agent-relay get-message ${messageId} --body`
|
|
180
|
+
];
|
|
181
|
+
const marker = (elided2) => `[\u2026 ${elided2} chars elided from the middle \u2014 full report: relay_get_message #${messageId} \u2026]`;
|
|
182
|
+
const budget = maxChars - marker(rawBody.length).length - 4;
|
|
183
|
+
if (budget < 200) {
|
|
184
|
+
const tail2 = rawBody.slice(rawBody.length - maxChars).replace(/^\S*\s+/, "").trimStart();
|
|
185
|
+
return {
|
|
186
|
+
body: tail2,
|
|
187
|
+
truncated: true,
|
|
188
|
+
guidance: [`[truncated: showing last ${tail2.length} of ${rawBody.length} chars \u2014 head trimmed, tail/verdict preserved]`, ...fetchLines]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const headChars = Math.floor(budget * 0.35);
|
|
192
|
+
const tailChars = budget - headChars;
|
|
193
|
+
const head = rawBody.slice(0, headChars).replace(/\s+\S*$/, "").trimEnd();
|
|
194
|
+
const tail = rawBody.slice(rawBody.length - tailChars).replace(/^\S*\s+/, "").trimStart();
|
|
195
|
+
const elided = rawBody.length - head.length - tail.length;
|
|
196
|
+
return {
|
|
197
|
+
body: `${head}
|
|
198
|
+
|
|
199
|
+
${marker(elided)}
|
|
200
|
+
|
|
201
|
+
${tail}`,
|
|
202
|
+
truncated: true,
|
|
203
|
+
guidance: [`[truncated: showing first ${head.length} + last ${tail.length} of ${rawBody.length} chars \u2014 tail/verdict preserved]`, ...fetchLines]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
1088
206
|
function attachmentRefs(message) {
|
|
1089
207
|
const payloadRefs = message.payload?.attachments;
|
|
1090
208
|
const topLevelRefs = message.attachments;
|
|
@@ -1111,9 +229,9 @@ var NOTIFICATION_NUDGE = "\u21AA Notification \u2014 no reply needed.";
|
|
|
1111
229
|
function isNotificationMessage(message) {
|
|
1112
230
|
return isPersistedRelayMessage(message) && message.replyExpected === false;
|
|
1113
231
|
}
|
|
1114
|
-
function dedupePersistedMessages(
|
|
232
|
+
function dedupePersistedMessages(messages) {
|
|
1115
233
|
const seen = new Set;
|
|
1116
|
-
return
|
|
234
|
+
return messages.filter((message) => {
|
|
1117
235
|
if (!isPersistedRelayMessage(message))
|
|
1118
236
|
return true;
|
|
1119
237
|
if (seen.has(message.id))
|
|
@@ -1199,8 +317,8 @@ function providerSenderLabel(message) {
|
|
|
1199
317
|
function shouldShowReplyReminder(deliveryCount) {
|
|
1200
318
|
return deliveryCount <= 1 || deliveryCount % REMINDER_EVERY_DELIVERIES === 0;
|
|
1201
319
|
}
|
|
1202
|
-
function latestReplyableMessage(
|
|
1203
|
-
return
|
|
320
|
+
function latestReplyableMessage(messages) {
|
|
321
|
+
return messages.filter((message) => isPersistedRelayMessage2(message) && !isMemoryInjection(message) && !isReactionNotification(message) && !isSpawnObligationMessage(message) && message.replyExpected !== false).at(-1);
|
|
1204
322
|
}
|
|
1205
323
|
function formatMessage(message, relaySurface) {
|
|
1206
324
|
const subject = message.subject ? `Subject: ${message.subject}
|
|
@@ -1208,20 +326,8 @@ function formatMessage(message, relaySurface) {
|
|
|
1208
326
|
const isMemoryContext = isMemoryInjection(message);
|
|
1209
327
|
const canFetchMessage = isPersistedRelayMessage2(message);
|
|
1210
328
|
const rawBody = relaySurface ? message.body : stripRelayScaffolding(message.body);
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
const preview = shouldPreview ? {
|
|
1214
|
-
body: rawBody.slice(0, maxChars),
|
|
1215
|
-
truncated: true
|
|
1216
|
-
} : {
|
|
1217
|
-
body: rawBody,
|
|
1218
|
-
truncated: false
|
|
1219
|
-
};
|
|
1220
|
-
const truncationGuidance = preview.truncated ? [
|
|
1221
|
-
`[truncated: showing first ${maxChars} of ${rawBody.length} chars]`,
|
|
1222
|
-
`Read full: agent-relay get-message ${message.id}`,
|
|
1223
|
-
`Body only: agent-relay get-message ${message.id} --body`
|
|
1224
|
-
].join(`
|
|
329
|
+
const preview = canFetchMessage ? previewDeliveredBody(rawBody, message.id, providerMessageBodyMaxChars()) : { body: rawBody, truncated: false, guidance: [] };
|
|
330
|
+
const truncationGuidance = preview.truncated ? preview.guidance.join(`
|
|
1225
331
|
`) : undefined;
|
|
1226
332
|
if (isMemoryContext) {
|
|
1227
333
|
return [
|
|
@@ -1250,9 +356,9 @@ function replyReminder(message, readOnly) {
|
|
|
1250
356
|
].join(`
|
|
1251
357
|
`);
|
|
1252
358
|
}
|
|
1253
|
-
function claudeProviderMessageText(
|
|
359
|
+
function claudeProviderMessageText(messages, options) {
|
|
1254
360
|
const relaySurface = options.relaySurface !== false;
|
|
1255
|
-
const uniqueMessages = dedupePersistedMessages(
|
|
361
|
+
const uniqueMessages = dedupePersistedMessages(messages);
|
|
1256
362
|
const sections = uniqueMessages.map((message) => formatMessage(message, relaySurface));
|
|
1257
363
|
const replyable = latestReplyableMessage(uniqueMessages);
|
|
1258
364
|
if (relaySurface && replyable && shouldShowReplyReminder(options.deliveryCount)) {
|
|
@@ -1276,7 +382,7 @@ ws.onmessage = (event) => {
|
|
|
1276
382
|
const payload = parsePayload(String(event.data));
|
|
1277
383
|
if (!payload || payload.type !== "message.deliver" || !Array.isArray(payload.messages))
|
|
1278
384
|
return;
|
|
1279
|
-
const
|
|
385
|
+
const messages = payload.messages;
|
|
1280
386
|
deliveryCount += 1;
|
|
1281
387
|
if (firstDelivery) {
|
|
1282
388
|
console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? claudeReadOnlyRelayContext() : claudeRelayContext());
|
|
@@ -1288,14 +394,14 @@ ws.onmessage = (event) => {
|
|
|
1288
394
|
console.log("");
|
|
1289
395
|
firstDelivery = false;
|
|
1290
396
|
}
|
|
1291
|
-
console.log(claudeProviderMessageText(
|
|
397
|
+
console.log(claudeProviderMessageText(messages, {
|
|
1292
398
|
deliveryCount,
|
|
1293
399
|
readOnly: process.env.AGENT_RELAY_APPROVAL === "read-only"
|
|
1294
400
|
}));
|
|
1295
401
|
ws.send(JSON.stringify({
|
|
1296
402
|
type: "message.delivered",
|
|
1297
403
|
deliveryId: typeof payload.deliveryId === "string" ? payload.deliveryId : "",
|
|
1298
|
-
messageIds:
|
|
404
|
+
messageIds: messages.map((message) => message.id)
|
|
1299
405
|
}));
|
|
1300
406
|
};
|
|
1301
407
|
ws.onerror = () => process.exit(1);
|
package/src/adapter.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AgentProfile, LivenessSignal, Message, ProviderState } from "agent-relay-sdk";
|
|
2
|
-
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { isRecord } from "agent-relay-sdk/types/guards";
|
|
3
3
|
import type { LivenessInputs } from "./liveness";
|
|
4
4
|
import type { SessionEvent } from "./session-insights";
|
|
5
|
-
import { messageBodyMaxCharsFromEnv } from "./config";
|
|
5
|
+
import { messageBodyMaxCharsFromEnv } from "./message-body-config";
|
|
6
6
|
import { relayReplyReminderText } from "./relay-instructions";
|
|
7
7
|
import { renderRunnerTemplate } from "./template-resolver";
|
|
8
8
|
|
|
@@ -250,6 +250,53 @@ export function providerMessageBodyMaxChars(): number {
|
|
|
250
250
|
return DEFAULT_PROVIDER_MESSAGE_BODY_MAX_CHARS;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
export interface DeliveredBodyPreview {
|
|
254
|
+
body: string;
|
|
255
|
+
truncated: boolean;
|
|
256
|
+
/** Retrieval-guidance lines (empty when not truncated). Call sites may append provider extras. */
|
|
257
|
+
guidance: string[];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// #1058 — one home for the delivered-body cap. Head-only truncation silently ate the TAIL, which is
|
|
261
|
+
// exactly where a report-up final puts its verdict / SHIP recommendation. Preserve BOTH ends: keep a
|
|
262
|
+
// head slice for opening context and a tail slice for the conclusion, eliding the middle and pointing
|
|
263
|
+
// at the untrimmed body. The tail is favored (it holds the verdict). Slices snap to whitespace so we
|
|
264
|
+
// don't deliver a half-word at either seam. Grep before adding another truncation site — this is it.
|
|
265
|
+
export function previewDeliveredBody(
|
|
266
|
+
rawBody: string,
|
|
267
|
+
messageId: number,
|
|
268
|
+
maxChars: number = providerMessageBodyMaxChars(),
|
|
269
|
+
): DeliveredBodyPreview {
|
|
270
|
+
if (rawBody.length <= maxChars) return { body: rawBody, truncated: false, guidance: [] };
|
|
271
|
+
const fetchLines = [
|
|
272
|
+
`Read full: agent-relay get-message ${messageId}`,
|
|
273
|
+
`Body only: agent-relay get-message ${messageId} --body`,
|
|
274
|
+
];
|
|
275
|
+
const marker = (elided: number) => `[… ${elided} chars elided from the middle — full report: relay_get_message #${messageId} …]`;
|
|
276
|
+
// Reserve room for the marker and its two blank-line joins; upper-bound the marker length by using
|
|
277
|
+
// the (always-larger) full body length so the reserved budget can never underflow the real marker.
|
|
278
|
+
const budget = maxChars - marker(rawBody.length).length - 4;
|
|
279
|
+
if (budget < 200) {
|
|
280
|
+
// Cap too small to keep a useful head — front-truncate, preserving the tail/verdict whole.
|
|
281
|
+
const tail = rawBody.slice(rawBody.length - maxChars).replace(/^\S*\s+/, "").trimStart();
|
|
282
|
+
return {
|
|
283
|
+
body: tail,
|
|
284
|
+
truncated: true,
|
|
285
|
+
guidance: [`[truncated: showing last ${tail.length} of ${rawBody.length} chars — head trimmed, tail/verdict preserved]`, ...fetchLines],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const headChars = Math.floor(budget * 0.35);
|
|
289
|
+
const tailChars = budget - headChars;
|
|
290
|
+
const head = rawBody.slice(0, headChars).replace(/\s+\S*$/, "").trimEnd();
|
|
291
|
+
const tail = rawBody.slice(rawBody.length - tailChars).replace(/^\S*\s+/, "").trimStart();
|
|
292
|
+
const elided = rawBody.length - head.length - tail.length;
|
|
293
|
+
return {
|
|
294
|
+
body: `${head}\n\n${marker(elided)}\n\n${tail}`,
|
|
295
|
+
truncated: true,
|
|
296
|
+
guidance: [`[truncated: showing first ${head.length} + last ${tail.length} of ${rawBody.length} chars — tail/verdict preserved]`, ...fetchLines],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
253
300
|
function attachmentRefs(message: Message): Record<string, unknown>[] {
|
|
254
301
|
const payloadRefs = message.payload?.attachments;
|
|
255
302
|
const topLevelRefs = (message as Message & { attachments?: unknown }).attachments;
|
|
@@ -375,23 +422,11 @@ export function providerMessageText(messages: Message[]): string {
|
|
|
375
422
|
const subject = message.subject ? `Subject: ${message.subject}\n` : "";
|
|
376
423
|
const isMemoryContext = isMemoryInjection(message);
|
|
377
424
|
const canReferenceMessage = isPersistedRelayMessage(message) && !isMemoryContext && !isReactionNotification(message);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
body: message.body.slice(0, maxChars),
|
|
382
|
-
truncated: true,
|
|
383
|
-
}
|
|
384
|
-
: {
|
|
385
|
-
body: message.body,
|
|
386
|
-
truncated: false,
|
|
387
|
-
};
|
|
425
|
+
const preview = canReferenceMessage
|
|
426
|
+
? previewDeliveredBody(message.body, message.id, maxChars)
|
|
427
|
+
: { body: message.body, truncated: false, guidance: [] };
|
|
388
428
|
const truncationGuidance = preview.truncated
|
|
389
|
-
? [
|
|
390
|
-
`[truncated: showing first ${maxChars} of ${message.body.length} chars]`,
|
|
391
|
-
`Read full: agent-relay get-message ${message.id}`,
|
|
392
|
-
`Body only: agent-relay get-message ${message.id} --body`,
|
|
393
|
-
`Long reply CLI fallback: agent-relay /reply ${message.id} --body-file response.md`,
|
|
394
|
-
].join("\n")
|
|
429
|
+
? [...preview.guidance, `Long reply CLI fallback: agent-relay /reply ${message.id} --body-file response.md`].join("\n")
|
|
395
430
|
: undefined;
|
|
396
431
|
if (isMemoryContext) {
|
|
397
432
|
return [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Message } from "agent-relay-sdk";
|
|
2
|
-
import { isRecord } from "agent-relay-sdk";
|
|
3
|
-
import { dedupePersistedMessages, isNotificationMessage, NOTIFICATION_NUDGE, providerAttachmentText, providerMessageBodyMaxChars } from "../adapter";
|
|
2
|
+
import { isRecord } from "agent-relay-sdk/types/guards";
|
|
3
|
+
import { dedupePersistedMessages, isNotificationMessage, NOTIFICATION_NUDGE, previewDeliveredBody, providerAttachmentText, providerMessageBodyMaxChars } from "../adapter";
|
|
4
4
|
import { relayReplyReminderText } from "../relay-instructions";
|
|
5
5
|
|
|
6
6
|
const REMINDER_EVERY_DELIVERIES = 5;
|
|
@@ -74,24 +74,10 @@ function formatMessage(message: Message, relaySurface: boolean): string {
|
|
|
74
74
|
const isMemoryContext = isMemoryInjection(message);
|
|
75
75
|
const canFetchMessage = isPersistedRelayMessage(message);
|
|
76
76
|
const rawBody = relaySurface ? message.body : stripRelayScaffolding(message.body);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
body: rawBody.slice(0, maxChars),
|
|
82
|
-
truncated: true,
|
|
83
|
-
}
|
|
84
|
-
: {
|
|
85
|
-
body: rawBody,
|
|
86
|
-
truncated: false,
|
|
87
|
-
};
|
|
88
|
-
const truncationGuidance = preview.truncated
|
|
89
|
-
? [
|
|
90
|
-
`[truncated: showing first ${maxChars} of ${rawBody.length} chars]`,
|
|
91
|
-
`Read full: agent-relay get-message ${message.id}`,
|
|
92
|
-
`Body only: agent-relay get-message ${message.id} --body`,
|
|
93
|
-
].join("\n")
|
|
94
|
-
: undefined;
|
|
77
|
+
const preview = canFetchMessage
|
|
78
|
+
? previewDeliveredBody(rawBody, message.id, providerMessageBodyMaxChars())
|
|
79
|
+
: { body: rawBody, truncated: false, guidance: [] };
|
|
80
|
+
const truncationGuidance = preview.truncated ? preview.guidance.join("\n") : undefined;
|
|
95
81
|
|
|
96
82
|
if (isMemoryContext) {
|
|
97
83
|
return [
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "
|
|
|
6
6
|
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
8
|
import { getManifest } from "agent-relay-providers";
|
|
9
|
+
export { messageBodyMaxCharsFromEnv } from "./message-body-config";
|
|
9
10
|
|
|
10
11
|
interface ProviderConfig {
|
|
11
12
|
command: string;
|
|
@@ -241,10 +242,6 @@ export function providerHomeRootFromEnv(): string {
|
|
|
241
242
|
return process.env.AGENT_RELAY_PROVIDER_HOME_ROOT || join(homedir(), ".agent-relay", "provider-homes");
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
export function messageBodyMaxCharsFromEnv(): string | undefined {
|
|
245
|
-
return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
245
|
export function loadProviderConfig(provider: string, home = agentRelayHome()): LoadedProviderConfig {
|
|
249
246
|
const path = join(providersDir(home), `${provider}.json`);
|
|
250
247
|
const raw = readJson(path);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { messageBodyMaxCharsFromEnv } from "./message-body-config/config";
|