herdr-link 0.2.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/PROTOCOL.md +244 -0
- package/README.md +159 -0
- package/README.zh-CN.md +159 -0
- package/dist/herdr-link.mcp.js +796 -0
- package/dist/herdr-link.opencode.js +535 -0
- package/docs/mcp-wiring.md +275 -0
- package/package.json +65 -0
- package/scripts/mcp-probe.mjs +41 -0
- package/src/herdr.ts +535 -0
- package/src/mcp.ts +582 -0
- package/src/opencode.ts +180 -0
- package/src/pi.ts +170 -0
- package/src/protocol.ts +284 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// src/opencode.ts
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
|
|
4
|
+
// src/herdr.ts
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
// src/protocol.ts
|
|
9
|
+
var PROTOCOL_ID = "herdr-link/1";
|
|
10
|
+
var AGENT_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
11
|
+
var MESSAGE_ID_RE = /^hl_[a-z0-9]+_[a-z0-9]+$/;
|
|
12
|
+
var HERDR_LINK_GATEWAY = "herdr_link";
|
|
13
|
+
var AGENT_STATES = ["idle", "working", "blocked", "done", "unknown"];
|
|
14
|
+
function toAgentState(value) {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
if (AGENT_STATES.includes(normalized)) {
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
var HerdrLinkError = class extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(code, detail) {
|
|
26
|
+
super(detail ? `${code}: ${detail}` : code);
|
|
27
|
+
this.name = "HerdrLinkError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var AGENT_ERROR_DETAILS = {
|
|
32
|
+
NOT_IN_HERDR: "Herdr environment is unavailable",
|
|
33
|
+
SELF_UNNAMED: "Herdr Link could not establish a stable Agent Name",
|
|
34
|
+
PEER_NOT_FOUND: "target agent is not a live peer",
|
|
35
|
+
SEND_FAILED: "Herdr did not accept message delivery",
|
|
36
|
+
CLOSE_FAILED: "Herdr pane close failed"
|
|
37
|
+
};
|
|
38
|
+
function formatAgentFacingError(error, fallbackCode) {
|
|
39
|
+
const code = error instanceof HerdrLinkError ? error.code : fallbackCode;
|
|
40
|
+
return `${code}: ${AGENT_ERROR_DETAILS[code]}`;
|
|
41
|
+
}
|
|
42
|
+
function createMessageId() {
|
|
43
|
+
const ts = Date.now().toString(36);
|
|
44
|
+
const rand = Math.random().toString(36).slice(2, 10) || "0";
|
|
45
|
+
return `hl_${ts}_${rand}`;
|
|
46
|
+
}
|
|
47
|
+
function isValidAgentName(name) {
|
|
48
|
+
return AGENT_NAME_RE.test(name);
|
|
49
|
+
}
|
|
50
|
+
function isValidMessageId(id) {
|
|
51
|
+
return MESSAGE_ID_RE.test(id);
|
|
52
|
+
}
|
|
53
|
+
function buildEnvelope(input) {
|
|
54
|
+
if (!isValidAgentName(input.from)) {
|
|
55
|
+
throw new HerdrLinkError(
|
|
56
|
+
"SELF_UNNAMED",
|
|
57
|
+
`self agent name "${input.from}" is not a valid Herdr agent name`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!input.to || !isValidAgentName(input.to)) {
|
|
61
|
+
throw new HerdrLinkError(
|
|
62
|
+
"PEER_NOT_FOUND",
|
|
63
|
+
`target agent name "${input.to}" is not a valid Herdr agent name`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (typeof input.message !== "string" || input.message.trim() === "") {
|
|
67
|
+
throw new HerdrLinkError("SEND_FAILED", "message must be a non-empty string");
|
|
68
|
+
}
|
|
69
|
+
if (input.reply_to !== void 0 && !isValidMessageId(input.reply_to)) {
|
|
70
|
+
throw new HerdrLinkError(
|
|
71
|
+
"SEND_FAILED",
|
|
72
|
+
"reply_to must be a valid herdr-link/1 message id when present"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const envelope = {
|
|
76
|
+
protocol: PROTOCOL_ID,
|
|
77
|
+
id: createMessageId(),
|
|
78
|
+
from: input.from,
|
|
79
|
+
to: input.to,
|
|
80
|
+
message: input.message
|
|
81
|
+
};
|
|
82
|
+
if (input.reply_to !== void 0) {
|
|
83
|
+
envelope.reply_to = input.reply_to;
|
|
84
|
+
}
|
|
85
|
+
return envelope;
|
|
86
|
+
}
|
|
87
|
+
var INBOUND_WRAPPER_MARKER = `[${PROTOCOL_ID}]`;
|
|
88
|
+
function buildInboundWrapper(envelope) {
|
|
89
|
+
const lines = [
|
|
90
|
+
`${INBOUND_WRAPPER_MARKER} inter-agent message delivered through the ${HERDR_LINK_GATEWAY} gateway.`,
|
|
91
|
+
`From: ${envelope.from}`,
|
|
92
|
+
`Message id: ${envelope.id}`
|
|
93
|
+
];
|
|
94
|
+
if (envelope.reply_to !== void 0) {
|
|
95
|
+
lines.push(`Reply to: ${envelope.reply_to}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push(
|
|
98
|
+
"",
|
|
99
|
+
"The JSON object below is the complete herdr-link/1 envelope; the text around it is delivery metadata and is not part of the message.",
|
|
100
|
+
`Treat the envelope's "message" field as content sent by the agent named in "from".`,
|
|
101
|
+
"If a reply is needed, activate the Herdr Link gateway when dormant, then use the active Herdr Link send capability to send to envelope.from with reply_to set to envelope.id.",
|
|
102
|
+
"",
|
|
103
|
+
JSON.stringify(envelope)
|
|
104
|
+
);
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
var COMMUNICATION_CONTRACT = `Herdr Link is the standard interoperability channel between agents running in the same Herdr workspace.
|
|
108
|
+
|
|
109
|
+
1. Use herdr_link_peers to discover agent addresses; it lists only live agents in your own workspace, each with an advisory activity state.
|
|
110
|
+
2. Use herdr_link_send to send messages to another agent.
|
|
111
|
+
3. A message with protocol "herdr-link/1" is an inter-agent message.
|
|
112
|
+
4. Treat its "message" field as content sent by the agent named in "from".
|
|
113
|
+
5. When replying, send to the received "from" agent and set reply_to to the received "id".
|
|
114
|
+
6. Use herdr_link_close only when you have already decided that a named agent's pane should be closed. If a final message is needed, call close in a later tool step after herdr_link_send returns "sent".
|
|
115
|
+
7. Never use a raw pane id, UI focus, terminal input, or the Herdr CLI as an inter-agent channel; agent names are the only addresses.
|
|
116
|
+
8. Agents outside your workspace are invisible: they never appear in peers and messages addressed to them fail.`;
|
|
117
|
+
|
|
118
|
+
// src/herdr.ts
|
|
119
|
+
function attachCliOutput(error, stdout, stderr) {
|
|
120
|
+
Object.assign(error, { stdout, stderr });
|
|
121
|
+
}
|
|
122
|
+
var defaultHerdrRunner = (file, args) => new Promise((resolve, reject) => {
|
|
123
|
+
execFile(file, args, { encoding: "utf8", shell: false }, (error, stdout, stderr) => {
|
|
124
|
+
if (error) {
|
|
125
|
+
attachCliOutput(error, String(stdout), String(stderr));
|
|
126
|
+
reject(error);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
var herdrRunner = defaultHerdrRunner;
|
|
133
|
+
function assertHerdrEnvironment() {
|
|
134
|
+
if (process.env.HERDR_ENV !== "1") {
|
|
135
|
+
throw new HerdrLinkError("NOT_IN_HERDR", "HERDR_ENV must be 1");
|
|
136
|
+
}
|
|
137
|
+
if (!process.env.HERDR_BIN_PATH) {
|
|
138
|
+
throw new HerdrLinkError("NOT_IN_HERDR", "HERDR_BIN_PATH is missing");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function describeError(error) {
|
|
142
|
+
if (error instanceof Error) return error.message;
|
|
143
|
+
if (typeof error === "string") return error;
|
|
144
|
+
try {
|
|
145
|
+
return JSON.stringify(error);
|
|
146
|
+
} catch {
|
|
147
|
+
return String(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function errorDetail(error) {
|
|
151
|
+
if (error instanceof HerdrLinkError) {
|
|
152
|
+
const prefix = `${error.code}: `;
|
|
153
|
+
return error.message.startsWith(prefix) ? error.message.slice(prefix.length) : error.message;
|
|
154
|
+
}
|
|
155
|
+
return describeError(error);
|
|
156
|
+
}
|
|
157
|
+
function operationError(error, code) {
|
|
158
|
+
return new HerdrLinkError(code, errorDetail(error));
|
|
159
|
+
}
|
|
160
|
+
var HerdrCliError = class extends Error {
|
|
161
|
+
cliCode;
|
|
162
|
+
constructor(cliCode, detail) {
|
|
163
|
+
super(detail);
|
|
164
|
+
this.name = "HerdrCliError";
|
|
165
|
+
this.cliCode = cliCode;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
async function runFor(args, failureCode) {
|
|
169
|
+
assertHerdrEnvironment();
|
|
170
|
+
try {
|
|
171
|
+
return await runHerdr(args);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof HerdrCliError) throw operationError(error, failureCode);
|
|
174
|
+
if (error instanceof HerdrLinkError) throw error;
|
|
175
|
+
throw operationError(error, failureCode);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
var CLI_ERROR_CODE_MAP = {
|
|
179
|
+
agent_not_found: "PEER_NOT_FOUND",
|
|
180
|
+
not_in_herdr: "NOT_IN_HERDR"
|
|
181
|
+
};
|
|
182
|
+
function classifyCliError(error) {
|
|
183
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
184
|
+
const commandError = error;
|
|
185
|
+
for (const output of [commandError.stdout, commandError.stderr]) {
|
|
186
|
+
if (typeof output !== "string") continue;
|
|
187
|
+
let payload;
|
|
188
|
+
try {
|
|
189
|
+
payload = JSON.parse(output);
|
|
190
|
+
} catch {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const errorPayload = asRecord(asRecord(payload)?.error);
|
|
194
|
+
if (!errorPayload) continue;
|
|
195
|
+
const cliCode = errorPayload.code;
|
|
196
|
+
if (typeof cliCode !== "string" || cliCode.length === 0) continue;
|
|
197
|
+
const cliMessage = errorPayload.message;
|
|
198
|
+
const detail = typeof cliMessage === "string" && cliMessage.length > 0 ? `${cliCode}: ${cliMessage}` : cliCode;
|
|
199
|
+
const mappedCode = CLI_ERROR_CODE_MAP[cliCode];
|
|
200
|
+
return mappedCode ? new HerdrLinkError(mappedCode, detail) : new HerdrCliError(cliCode, detail);
|
|
201
|
+
}
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
async function runHerdr(args) {
|
|
205
|
+
assertHerdrEnvironment();
|
|
206
|
+
const binary = process.env.HERDR_BIN_PATH;
|
|
207
|
+
try {
|
|
208
|
+
const output = await herdrRunner(binary, args);
|
|
209
|
+
const parsed = JSON.parse(output.stdout);
|
|
210
|
+
const cliError = classifyCliError(output);
|
|
211
|
+
if (cliError) throw cliError;
|
|
212
|
+
return parsed;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof HerdrLinkError || error instanceof HerdrCliError) throw error;
|
|
215
|
+
const cliError = classifyCliError(error);
|
|
216
|
+
if (cliError) throw cliError;
|
|
217
|
+
throw new HerdrLinkError("NOT_IN_HERDR", `Herdr command or JSON response failed: ${describeError(error)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function asRecord(value) {
|
|
221
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function agentRecord(value) {
|
|
225
|
+
const root = asRecord(value);
|
|
226
|
+
if (!root) return void 0;
|
|
227
|
+
const result = asRecord(root.result);
|
|
228
|
+
const nestedAgent = asRecord(result?.agent) ?? asRecord(root.agent);
|
|
229
|
+
if (nestedAgent) return nestedAgent;
|
|
230
|
+
if (typeof result?.name === "string" || typeof result?.pane_id === "string") return result;
|
|
231
|
+
if (typeof root.name === "string" || typeof root.pane_id === "string") return root;
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
function agentList(value) {
|
|
235
|
+
const root = asRecord(value);
|
|
236
|
+
const result = asRecord(root?.result);
|
|
237
|
+
const agents = result?.agents ?? root?.agents;
|
|
238
|
+
return Array.isArray(agents) ? agents : [];
|
|
239
|
+
}
|
|
240
|
+
function nonEmptyString(value) {
|
|
241
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
242
|
+
}
|
|
243
|
+
function validAgentNameValue(value) {
|
|
244
|
+
const name = nonEmptyString(value);
|
|
245
|
+
return name !== void 0 && isValidAgentName(name) ? name : void 0;
|
|
246
|
+
}
|
|
247
|
+
function readLiveRecord(value) {
|
|
248
|
+
const agent = agentRecord(value);
|
|
249
|
+
return {
|
|
250
|
+
name: validAgentNameValue(agent?.name),
|
|
251
|
+
workspace_id: nonEmptyString(agent?.workspace_id),
|
|
252
|
+
pane_id: nonEmptyString(agent?.pane_id),
|
|
253
|
+
live: typeof agent?.live === "boolean" ? agent.live : void 0
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function readStatus(value) {
|
|
257
|
+
const agent = agentRecord(value);
|
|
258
|
+
return toAgentState(agent?.agent_status ?? agent?.status);
|
|
259
|
+
}
|
|
260
|
+
function isExcludedEntry(value) {
|
|
261
|
+
return agentRecord(value)?.live === false;
|
|
262
|
+
}
|
|
263
|
+
var GENERATED_NAME_PREFIX = "hl-";
|
|
264
|
+
var MAX_GENERATED_NAME_ATTEMPTS = 3;
|
|
265
|
+
var SELF_BOOTSTRAP_FAILED_DETAIL = "Herdr Link could not establish a stable Agent Name";
|
|
266
|
+
function selfUnnamed(detail) {
|
|
267
|
+
return new HerdrLinkError("SELF_UNNAMED", detail ?? SELF_BOOTSTRAP_FAILED_DETAIL);
|
|
268
|
+
}
|
|
269
|
+
function generateAgentName() {
|
|
270
|
+
return `${GENERATED_NAME_PREFIX}${randomBytes(4).toString("hex")}`;
|
|
271
|
+
}
|
|
272
|
+
function stableName(record) {
|
|
273
|
+
return record.live === false ? void 0 : record.name;
|
|
274
|
+
}
|
|
275
|
+
var SELF_PROBE_ATTEMPTS = 3;
|
|
276
|
+
var SELF_PROBE_DELAY_MS = 100;
|
|
277
|
+
var sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
278
|
+
async function fetchSelfRecord(pane) {
|
|
279
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
280
|
+
try {
|
|
281
|
+
return await runFor(["agent", "get", pane], "SELF_UNNAMED");
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const notDetectedYet = error instanceof HerdrLinkError && error.code === "PEER_NOT_FOUND";
|
|
284
|
+
if (notDetectedYet && attempt < SELF_PROBE_ATTEMPTS) {
|
|
285
|
+
await sleepMs(SELF_PROBE_DELAY_MS);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (notDetectedYet) {
|
|
289
|
+
throw selfUnnamed(errorDetail(error));
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var bootstrapInFlight;
|
|
296
|
+
function ensureSelfName() {
|
|
297
|
+
bootstrapInFlight ??= ensureSelfNameFlow().finally(() => {
|
|
298
|
+
bootstrapInFlight = void 0;
|
|
299
|
+
});
|
|
300
|
+
return bootstrapInFlight;
|
|
301
|
+
}
|
|
302
|
+
async function ensureSelfNameFlow() {
|
|
303
|
+
assertHerdrEnvironment();
|
|
304
|
+
const pane = process.env.HERDR_PANE_ID;
|
|
305
|
+
if (!pane) {
|
|
306
|
+
throw selfUnnamed("HERDR_PANE_ID is missing");
|
|
307
|
+
}
|
|
308
|
+
return establishSelfName(pane, readLiveRecord(await fetchSelfRecord(pane)));
|
|
309
|
+
}
|
|
310
|
+
async function establishSelfName(pane, record) {
|
|
311
|
+
const existing = stableName(record);
|
|
312
|
+
if (existing) return existing;
|
|
313
|
+
if (record.live === false) {
|
|
314
|
+
throw selfUnnamed();
|
|
315
|
+
}
|
|
316
|
+
for (let attempt = 0; attempt < MAX_GENERATED_NAME_ATTEMPTS; attempt += 1) {
|
|
317
|
+
try {
|
|
318
|
+
await runHerdr(["agent", "rename", pane, generateAgentName()]);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error instanceof HerdrCliError && error.cliCode === "agent_name_taken") {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (error instanceof HerdrLinkError && error.code === "NOT_IN_HERDR") {
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
throw selfUnnamed();
|
|
327
|
+
}
|
|
328
|
+
const confirmed = stableName(readLiveRecord(await fetchSelfRecord(pane)));
|
|
329
|
+
if (confirmed) return confirmed;
|
|
330
|
+
throw selfUnnamed();
|
|
331
|
+
}
|
|
332
|
+
throw selfUnnamed();
|
|
333
|
+
}
|
|
334
|
+
async function getSelfContext() {
|
|
335
|
+
assertHerdrEnvironment();
|
|
336
|
+
const pane = process.env.HERDR_PANE_ID;
|
|
337
|
+
if (!pane) {
|
|
338
|
+
throw selfUnnamed("HERDR_PANE_ID is missing");
|
|
339
|
+
}
|
|
340
|
+
let response = await fetchSelfRecord(pane);
|
|
341
|
+
let record = readLiveRecord(response);
|
|
342
|
+
if (!stableName(record) && record.live !== false) {
|
|
343
|
+
await ensureSelfName();
|
|
344
|
+
response = await fetchSelfRecord(pane);
|
|
345
|
+
record = readLiveRecord(response);
|
|
346
|
+
}
|
|
347
|
+
const name = stableName(record);
|
|
348
|
+
if (!name) {
|
|
349
|
+
throw selfUnnamed("current Herdr agent has no valid name");
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
name,
|
|
353
|
+
workspace_id: record.workspace_id ?? "",
|
|
354
|
+
pane_id: record.pane_id ?? pane,
|
|
355
|
+
agent_status: readStatus(response)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async function getAgentContext(name) {
|
|
359
|
+
assertHerdrEnvironment();
|
|
360
|
+
if (!isValidAgentName(name)) {
|
|
361
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", `target agent name "${name}" is invalid`);
|
|
362
|
+
}
|
|
363
|
+
const response = await runFor(["agent", "get", name], "PEER_NOT_FOUND");
|
|
364
|
+
const record = readLiveRecord(response);
|
|
365
|
+
if (record.live === false || !record.name || record.name !== name) {
|
|
366
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", `target agent "${name}" has no valid live record`);
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
name: record.name,
|
|
370
|
+
workspace_id: record.workspace_id ?? "",
|
|
371
|
+
pane_id: record.pane_id ?? "",
|
|
372
|
+
agent_status: readStatus(response)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function assertSameWorkspace(self, target) {
|
|
376
|
+
if (self.workspace_id === "" || target.workspace_id === "" || self.workspace_id !== target.workspace_id) {
|
|
377
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", AGENT_ERROR_DETAILS.PEER_NOT_FOUND);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function listPeers() {
|
|
381
|
+
const self = await getSelfContext();
|
|
382
|
+
const response = await runFor(["agent", "list"], "NOT_IN_HERDR");
|
|
383
|
+
const peers = [];
|
|
384
|
+
const seen = /* @__PURE__ */ new Set();
|
|
385
|
+
for (const entry of agentList(response)) {
|
|
386
|
+
const record = readLiveRecord(entry);
|
|
387
|
+
if (!record.name || seen.has(record.name)) continue;
|
|
388
|
+
seen.add(record.name);
|
|
389
|
+
if (record.name === self.name) continue;
|
|
390
|
+
if (self.workspace_id === "" || record.workspace_id !== self.workspace_id) continue;
|
|
391
|
+
if (isExcludedEntry(entry)) continue;
|
|
392
|
+
peers.push({ name: record.name, state: readStatus(entry) });
|
|
393
|
+
}
|
|
394
|
+
return { self: { name: self.name, state: self.agent_status }, peers };
|
|
395
|
+
}
|
|
396
|
+
async function sendMessage(to, message, reply_to) {
|
|
397
|
+
const self = await getSelfContext();
|
|
398
|
+
const target = await getAgentContext(to);
|
|
399
|
+
assertSameWorkspace(self, target);
|
|
400
|
+
const envelope = buildEnvelope({
|
|
401
|
+
from: self.name,
|
|
402
|
+
to: target.name,
|
|
403
|
+
message,
|
|
404
|
+
reply_to
|
|
405
|
+
});
|
|
406
|
+
await runFor(["agent", "prompt", target.name, buildInboundWrapper(envelope)], "SEND_FAILED");
|
|
407
|
+
return { status: "sent", id: envelope.id, to: target.name };
|
|
408
|
+
}
|
|
409
|
+
async function getSelfWorkspaceId() {
|
|
410
|
+
assertHerdrEnvironment();
|
|
411
|
+
const pane = process.env.HERDR_PANE_ID;
|
|
412
|
+
if (!pane) {
|
|
413
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", AGENT_ERROR_DETAILS.PEER_NOT_FOUND);
|
|
414
|
+
}
|
|
415
|
+
const response = await runFor(["agent", "get", pane], "NOT_IN_HERDR");
|
|
416
|
+
const record = readLiveRecord(response);
|
|
417
|
+
if (record.live === false || !record.workspace_id) {
|
|
418
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", AGENT_ERROR_DETAILS.PEER_NOT_FOUND);
|
|
419
|
+
}
|
|
420
|
+
return record.workspace_id;
|
|
421
|
+
}
|
|
422
|
+
async function closeAgentPane(agentName) {
|
|
423
|
+
assertHerdrEnvironment();
|
|
424
|
+
if (!isValidAgentName(agentName)) {
|
|
425
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", `target agent name "${agentName}" is invalid`);
|
|
426
|
+
}
|
|
427
|
+
const selfWorkspaceId = await getSelfWorkspaceId();
|
|
428
|
+
const target = await getAgentContext(agentName);
|
|
429
|
+
if (target.workspace_id === "" || target.workspace_id !== selfWorkspaceId) {
|
|
430
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", AGENT_ERROR_DETAILS.PEER_NOT_FOUND);
|
|
431
|
+
}
|
|
432
|
+
if (!target.pane_id) {
|
|
433
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", `target agent "${agentName}" has no current pane`);
|
|
434
|
+
}
|
|
435
|
+
await runFor(["pane", "close", target.pane_id], "CLOSE_FAILED");
|
|
436
|
+
return { status: "closed", agent: target.name };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/opencode.ts
|
|
440
|
+
var GATEWAY_PRESENTATION_APPENDIX = `In this runtime the active Herdr Link capabilities are dispatched through the single herdr_link gateway.
|
|
441
|
+
- Use herdr_link with action "peers" to list live same-workspace agents.
|
|
442
|
+
- Use herdr_link with action "send", to, message, and reply_to when replying.
|
|
443
|
+
- Use herdr_link with action "close" and an Agent Name only after any final send returns status "sent", in a later tool step.`;
|
|
444
|
+
var GATEWAY_CONTRACT = `${COMMUNICATION_CONTRACT}
|
|
445
|
+
|
|
446
|
+
${GATEWAY_PRESENTATION_APPENDIX}`;
|
|
447
|
+
function isHerdrEnvironment() {
|
|
448
|
+
return process.env.HERDR_ENV === "1" && Boolean(process.env.HERDR_BIN_PATH) && Boolean(process.env.HERDR_PANE_ID);
|
|
449
|
+
}
|
|
450
|
+
function jsonResult(value) {
|
|
451
|
+
return JSON.stringify(value);
|
|
452
|
+
}
|
|
453
|
+
function failWith(error, fallbackCode) {
|
|
454
|
+
throw new Error(formatAgentFacingError(error, fallbackCode), { cause: error });
|
|
455
|
+
}
|
|
456
|
+
function failInvalidAction(action) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`INVALID_ACTION: herdr_link action "${action}" is not supported; use "peers", "send", "close", or omit action (call with {}) to activate.`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
var herdrLinkPlugin = async () => {
|
|
462
|
+
if (!isHerdrEnvironment()) {
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
465
|
+
void ensureSelfName().catch(() => {
|
|
466
|
+
});
|
|
467
|
+
const activatedSessions = /* @__PURE__ */ new Set();
|
|
468
|
+
return {
|
|
469
|
+
tool: {
|
|
470
|
+
[HERDR_LINK_GATEWAY]: tool({
|
|
471
|
+
description: `Herdr Link cross-agent communication gateway (herdr-link/1). Activate only when the user explicitly asks to use Herdr or when handling an inbound Herdr Link message. Call once with no arguments {} to activate Herdr Link for this session; the response lists capabilities. Then pass action "peers" to list live same-workspace agents, "send" with to + message (plus reply_to when replying) to deliver an inter-agent message, or "close" with agent to close a named agent's pane \u2014 only after any final send has returned status "sent", and in a later tool step.`,
|
|
472
|
+
args: {
|
|
473
|
+
action: tool.schema.enum(["peers", "send", "close"]).optional().describe(
|
|
474
|
+
'Operation to run: "peers" | "send" | "close". Omit action entirely (call with {}) to activate Herdr Link for this session.'
|
|
475
|
+
),
|
|
476
|
+
to: tool.schema.string().optional().describe('Target agent name; required for action "send".'),
|
|
477
|
+
message: tool.schema.string().optional().describe('Message payload; required for action "send".'),
|
|
478
|
+
reply_to: tool.schema.string().optional().describe('Message id being replied to; optional, only with action "send".'),
|
|
479
|
+
agent: tool.schema.string().optional().describe('Target agent name; required for action "close".')
|
|
480
|
+
},
|
|
481
|
+
async execute(args, context) {
|
|
482
|
+
if (args.action === void 0) {
|
|
483
|
+
activatedSessions.add(context.sessionID);
|
|
484
|
+
return jsonResult({ status: "active", capabilities: ["peers", "send", "close"] });
|
|
485
|
+
}
|
|
486
|
+
activatedSessions.add(context.sessionID);
|
|
487
|
+
if (args.action === "peers") {
|
|
488
|
+
try {
|
|
489
|
+
return jsonResult(await listPeers());
|
|
490
|
+
} catch (error) {
|
|
491
|
+
failWith(error, "NOT_IN_HERDR");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (args.action === "send") {
|
|
495
|
+
if (typeof args.to !== "string" || args.to === "") {
|
|
496
|
+
failWith(new HerdrLinkError("SEND_FAILED", '"to" must be a non-empty string'), "SEND_FAILED");
|
|
497
|
+
}
|
|
498
|
+
if (typeof args.message !== "string" || args.message === "") {
|
|
499
|
+
failWith(new HerdrLinkError("SEND_FAILED", '"message" must be a non-empty string'), "SEND_FAILED");
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const envelope = await sendMessage(args.to, args.message, args.reply_to);
|
|
503
|
+
return jsonResult({ status: "sent", id: envelope.id, to: envelope.to });
|
|
504
|
+
} catch (error) {
|
|
505
|
+
failWith(error, "SEND_FAILED");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (args.action === "close") {
|
|
509
|
+
if (typeof args.agent !== "string" || args.agent === "") {
|
|
510
|
+
failWith(new HerdrLinkError("CLOSE_FAILED", '"agent" must be a non-empty string'), "CLOSE_FAILED");
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
await closeAgentPane(args.agent);
|
|
514
|
+
return jsonResult({ status: "closed", agent: args.agent });
|
|
515
|
+
} catch (error) {
|
|
516
|
+
failWith(error, "CLOSE_FAILED");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
failInvalidAction(String(args.action));
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
},
|
|
523
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
524
|
+
if (input.sessionID === void 0 || !activatedSessions.has(input.sessionID)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (!output.system.includes(GATEWAY_CONTRACT)) {
|
|
528
|
+
output.system.push(GATEWAY_CONTRACT);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
export {
|
|
534
|
+
herdrLinkPlugin
|
|
535
|
+
};
|