herdr-link 0.4.0 → 0.5.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 -1
- package/PROTOCOL.md +35 -31
- package/README.md +15 -22
- package/README.zh-CN.md +16 -22
- package/dist/herdr-link.mcp.js +235 -38
- package/dist/herdr-link.opencode.js +228 -31
- package/docs/mcp-wiring.md +11 -14
- package/examples/agent_config.example.json +17 -2
- package/package.json +3 -2
- package/src/herdr.ts +320 -27
- package/src/mcp.ts +17 -18
- package/src/opencode.ts +9 -5
- package/src/pi.ts +34 -7
- package/src/protocol.ts +19 -22
|
@@ -5,7 +5,7 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { resolve } from "node:path";
|
|
8
|
+
import { isAbsolute, resolve } from "node:path";
|
|
9
9
|
|
|
10
10
|
// src/protocol.ts
|
|
11
11
|
var PROTOCOL_ID = "herdr-link/1";
|
|
@@ -21,7 +21,7 @@ function toAgentState(value) {
|
|
|
21
21
|
}
|
|
22
22
|
return "unknown";
|
|
23
23
|
}
|
|
24
|
-
var START_TOOL_DESCRIPTION = "Start a
|
|
24
|
+
var START_TOOL_DESCRIPTION = "Start a Herdr agent with Link-managed placement.";
|
|
25
25
|
var HerdrLinkError = class extends Error {
|
|
26
26
|
code;
|
|
27
27
|
constructor(code, detail) {
|
|
@@ -96,17 +96,14 @@ function buildInboundWrapper(envelope) {
|
|
|
96
96
|
);
|
|
97
97
|
return lines.join("\n");
|
|
98
98
|
}
|
|
99
|
-
var COMMUNICATION_CONTRACT = `Herdr Link is the
|
|
99
|
+
var COMMUNICATION_CONTRACT = `Herdr Link is the agent channel for the current Herdr workspace.
|
|
100
100
|
|
|
101
|
-
1.
|
|
102
|
-
2. Use
|
|
103
|
-
3.
|
|
104
|
-
4.
|
|
105
|
-
5.
|
|
106
|
-
6.
|
|
107
|
-
7. 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".
|
|
108
|
-
8. 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.
|
|
109
|
-
9. Agents outside your workspace are invisible: they never appear in peers and messages addressed to them fail.`;
|
|
101
|
+
1. Reply path: herdr_link_send \u2192 end this turn \u2192 inbound Herdr Link message. Never wait or poll for the reply; "sent" is delivery only.
|
|
102
|
+
2. Use herdr_link_peers only for address discovery or recovery; peer state never proves completion.
|
|
103
|
+
3. Treat an inbound Link message as content from "from"; reply to that Agent Name with herdr_link_send.
|
|
104
|
+
4. Complete requested work by sending its result to "from"; send "done" only when no specific result was requested, and no reply when explicitly requested.
|
|
105
|
+
5. Use herdr_link_close only after the agent lifecycle is complete.
|
|
106
|
+
6. Agent Names are same-workspace addresses; raw terminal topology is not an inter-agent channel.`;
|
|
110
107
|
|
|
111
108
|
// src/herdr.ts
|
|
112
109
|
function attachCliOutput(error, stdout, stderr) {
|
|
@@ -171,7 +168,7 @@ async function runFor(args, failureCode) {
|
|
|
171
168
|
var startCursors = /* @__PURE__ */ new Map();
|
|
172
169
|
var startLocks = /* @__PURE__ */ new Map();
|
|
173
170
|
var START_CONFIG_PATH_PARTS = [".agents", "agent_config.json"];
|
|
174
|
-
var START_INPUT_KEYS = /* @__PURE__ */ new Set(["name", "
|
|
171
|
+
var START_INPUT_KEYS = /* @__PURE__ */ new Set(["name", "with", "cwd", "config_agent", "kind", "args"]);
|
|
175
172
|
function hasOwn(value, key) {
|
|
176
173
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
177
174
|
}
|
|
@@ -191,9 +188,21 @@ function validateStartInput(input) {
|
|
|
191
188
|
if (typeof name !== "string" || !isValidAgentName(name)) {
|
|
192
189
|
throw startInputError('"name" must be a valid Herdr Agent Name');
|
|
193
190
|
}
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
191
|
+
let withName;
|
|
192
|
+
if (hasOwn(value, "with")) {
|
|
193
|
+
const withValue = value.with;
|
|
194
|
+
if (typeof withValue !== "string" || !isValidAgentName(withValue)) {
|
|
195
|
+
throw startInputError('"with" must be a valid Herdr Agent Name');
|
|
196
|
+
}
|
|
197
|
+
withName = withValue;
|
|
198
|
+
}
|
|
199
|
+
let cwd;
|
|
200
|
+
if (hasOwn(value, "cwd")) {
|
|
201
|
+
const cwdValue = value.cwd;
|
|
202
|
+
if (typeof cwdValue !== "string" || cwdValue.trim() === "") {
|
|
203
|
+
throw startInputError('"cwd" must be a non-empty string');
|
|
204
|
+
}
|
|
205
|
+
cwd = cwdValue;
|
|
197
206
|
}
|
|
198
207
|
const hasConfigAgent = hasOwn(value, "config_agent");
|
|
199
208
|
const hasKind = hasOwn(value, "kind");
|
|
@@ -206,7 +215,7 @@ function validateStartInput(input) {
|
|
|
206
215
|
if (typeof configAgent !== "string" || configAgent.trim() === "") {
|
|
207
216
|
throw startInputError('"config_agent" must be a non-empty string');
|
|
208
217
|
}
|
|
209
|
-
return { mode: "configured", name,
|
|
218
|
+
return { mode: "configured", name, configAgent, ...withName !== void 0 ? { withName } : {}, ...cwd !== void 0 ? { cwd } : {} };
|
|
210
219
|
}
|
|
211
220
|
if (!hasKind || !hasArgs) {
|
|
212
221
|
throw startInputError("explicit start requires both kind and args");
|
|
@@ -219,7 +228,10 @@ function validateStartInput(input) {
|
|
|
219
228
|
if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
|
|
220
229
|
throw startInputError('"args" must be an array of strings');
|
|
221
230
|
}
|
|
222
|
-
|
|
231
|
+
if (withName !== void 0 && cwd !== void 0) {
|
|
232
|
+
throw startInputError('"cwd" cannot be combined with "with"');
|
|
233
|
+
}
|
|
234
|
+
return { mode: "explicit", name, variant: { kind, args: [...args] }, ...withName !== void 0 ? { withName } : {}, ...cwd !== void 0 ? { cwd } : {} };
|
|
223
235
|
}
|
|
224
236
|
function assertAllowedKeys(value, allowed, label) {
|
|
225
237
|
const allowedSet = new Set(allowed);
|
|
@@ -230,8 +242,7 @@ function assertAllowedKeys(value, allowed, label) {
|
|
|
230
242
|
function validateConfiguredDocument(document) {
|
|
231
243
|
const root = asRecord(document);
|
|
232
244
|
if (!root) throw startConfigError("START_CONFIG_INVALID", "configuration root must be an object");
|
|
233
|
-
assertAllowedKeys(root, ["
|
|
234
|
-
if (root.version !== 1) throw startConfigError("START_CONFIG_INVALID", "configuration version must be 1");
|
|
245
|
+
assertAllowedKeys(root, ["agents"], "configuration root");
|
|
235
246
|
const agents = asRecord(root.agents);
|
|
236
247
|
if (!agents) throw startConfigError("START_CONFIG_INVALID", "agents must be an object");
|
|
237
248
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -241,7 +252,11 @@ function validateConfiguredDocument(document) {
|
|
|
241
252
|
}
|
|
242
253
|
const entry = asRecord(rawEntry);
|
|
243
254
|
if (!entry) throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent} must be an object`);
|
|
244
|
-
assertAllowedKeys(entry, ["strategy", "variants"], `agents.${configAgent}`);
|
|
255
|
+
assertAllowedKeys(entry, ["placement", "strategy", "variants"], `agents.${configAgent}`);
|
|
256
|
+
if (!hasOwn(entry, "placement")) {
|
|
257
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.placement is required`);
|
|
258
|
+
}
|
|
259
|
+
const placement = validatePlacement(entry.placement, configAgent);
|
|
245
260
|
const rawVariants = entry.variants;
|
|
246
261
|
if (!Array.isArray(rawVariants) || rawVariants.length === 0) {
|
|
247
262
|
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants must be non-empty`);
|
|
@@ -268,12 +283,35 @@ function validateConfiguredDocument(document) {
|
|
|
268
283
|
return { kind, args: Array.isArray(args) ? [...args] : [] };
|
|
269
284
|
});
|
|
270
285
|
result.set(configAgent, {
|
|
286
|
+
placement,
|
|
271
287
|
...hasStrategy ? { strategy: "round-robin" } : {},
|
|
272
288
|
variants
|
|
273
289
|
});
|
|
274
290
|
}
|
|
275
291
|
return result;
|
|
276
292
|
}
|
|
293
|
+
function validatePlacement(rawPlacement, configAgent) {
|
|
294
|
+
const placement = asRecord(rawPlacement);
|
|
295
|
+
if (!placement) {
|
|
296
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.placement must be an object`);
|
|
297
|
+
}
|
|
298
|
+
if (placement.mode === "new_tab") {
|
|
299
|
+
assertAllowedKeys(placement, ["mode", "label"], `agents.${configAgent}.placement`);
|
|
300
|
+
let label;
|
|
301
|
+
if (hasOwn(placement, "label")) {
|
|
302
|
+
if (typeof placement.label !== "string" || placement.label.trim() === "") {
|
|
303
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.placement.label must be a non-empty string`);
|
|
304
|
+
}
|
|
305
|
+
label = placement.label;
|
|
306
|
+
}
|
|
307
|
+
return { mode: "new_tab", ...label !== void 0 ? { label } : {} };
|
|
308
|
+
}
|
|
309
|
+
if (placement.mode === "with") {
|
|
310
|
+
assertAllowedKeys(placement, ["mode"], `agents.${configAgent}.placement`);
|
|
311
|
+
return { mode: "with" };
|
|
312
|
+
}
|
|
313
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.placement.mode is unsupported`);
|
|
314
|
+
}
|
|
277
315
|
async function loadConfiguredStartAgents(configPath) {
|
|
278
316
|
let text;
|
|
279
317
|
try {
|
|
@@ -316,15 +354,124 @@ async function runStart(name, pane, variant) {
|
|
|
316
354
|
throw operationError(error, "START_FAILED");
|
|
317
355
|
}
|
|
318
356
|
}
|
|
357
|
+
function startFailure(detail) {
|
|
358
|
+
return new HerdrLinkError("START_FAILED", detail);
|
|
359
|
+
}
|
|
360
|
+
function resolvePlacement(validated, configPlacement) {
|
|
361
|
+
if (validated.mode === "configured") {
|
|
362
|
+
if (configPlacement?.mode === "new_tab") {
|
|
363
|
+
if (validated.withName !== void 0) {
|
|
364
|
+
throw startInputError('"with" is not allowed for a new_tab configured placement');
|
|
365
|
+
}
|
|
366
|
+
return { kind: "new-tab", label: configPlacement.label };
|
|
367
|
+
}
|
|
368
|
+
if (validated.withName === void 0) {
|
|
369
|
+
throw startInputError('configured "with" placement requires "with"');
|
|
370
|
+
}
|
|
371
|
+
if (validated.cwd !== void 0) {
|
|
372
|
+
throw startInputError('"cwd" is not allowed for a "with" placement');
|
|
373
|
+
}
|
|
374
|
+
return { kind: "with", withName: validated.withName };
|
|
375
|
+
}
|
|
376
|
+
return validated.withName !== void 0 ? { kind: "with", withName: validated.withName } : { kind: "new-tab" };
|
|
377
|
+
}
|
|
378
|
+
function resolveLaunchCwd(contextDirectory, inputCwd) {
|
|
379
|
+
if (inputCwd === void 0) return contextDirectory;
|
|
380
|
+
return isAbsolute(inputCwd) ? inputCwd : resolve(contextDirectory, inputCwd);
|
|
381
|
+
}
|
|
382
|
+
async function bestEffortCloseTab(tabId) {
|
|
383
|
+
try {
|
|
384
|
+
await runFor(["tab", "close", tabId], "START_FAILED");
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function bestEffortClosePane(paneId) {
|
|
389
|
+
try {
|
|
390
|
+
await runFor(["pane", "close", paneId], "START_FAILED");
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function allocateNewTab(contextDirectory, inputCwd, label) {
|
|
395
|
+
const self = await getSelfContext();
|
|
396
|
+
const launchCwd = resolveLaunchCwd(contextDirectory, inputCwd);
|
|
397
|
+
const created = await runFor(
|
|
398
|
+
[
|
|
399
|
+
"tab",
|
|
400
|
+
"create",
|
|
401
|
+
"--workspace",
|
|
402
|
+
self.workspace_id,
|
|
403
|
+
"--cwd",
|
|
404
|
+
launchCwd,
|
|
405
|
+
...label !== void 0 ? ["--label", label] : [],
|
|
406
|
+
"--no-focus"
|
|
407
|
+
],
|
|
408
|
+
"START_FAILED"
|
|
409
|
+
);
|
|
410
|
+
const createdTabId = parseTabCreateResult(created);
|
|
411
|
+
if (createdTabId === void 0) throw startFailure("created tab reported no tab id");
|
|
412
|
+
try {
|
|
413
|
+
const panes = await runFor(["pane", "list", "--workspace", self.workspace_id], "START_FAILED");
|
|
414
|
+
const rootPanes = filterPanesByTab(panes, createdTabId);
|
|
415
|
+
if (rootPanes.length !== 1 || rootPanes[0] === void 0) {
|
|
416
|
+
throw startFailure("created tab must contain exactly one root pane");
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
paneId: rootPanes[0],
|
|
420
|
+
rollback: () => bestEffortCloseTab(createdTabId)
|
|
421
|
+
};
|
|
422
|
+
} catch (error) {
|
|
423
|
+
await bestEffortCloseTab(createdTabId);
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function allocateWith(withName) {
|
|
428
|
+
const self = await getSelfContext();
|
|
429
|
+
const anchor = await getAgentContext(withName);
|
|
430
|
+
assertSameWorkspace(self, anchor);
|
|
431
|
+
const anchorPane = await getPaneCwd(anchor.pane_id);
|
|
432
|
+
if (anchorPane.workspace_id === "" || anchorPane.workspace_id !== self.workspace_id) {
|
|
433
|
+
throw new HerdrLinkError("PEER_NOT_FOUND", AGENT_ERROR_DETAILS.PEER_NOT_FOUND);
|
|
434
|
+
}
|
|
435
|
+
if (anchorPane.cwd === void 0 || anchorPane.cwd === "") {
|
|
436
|
+
throw startFailure(`anchor pane ${anchor.pane_id} has no cwd; cannot inherit worktree binding`);
|
|
437
|
+
}
|
|
438
|
+
const split = await runFor(
|
|
439
|
+
[
|
|
440
|
+
"pane",
|
|
441
|
+
"split",
|
|
442
|
+
anchor.pane_id,
|
|
443
|
+
"--direction",
|
|
444
|
+
"right",
|
|
445
|
+
"--cwd",
|
|
446
|
+
anchorPane.cwd,
|
|
447
|
+
"--no-focus"
|
|
448
|
+
],
|
|
449
|
+
"START_FAILED"
|
|
450
|
+
);
|
|
451
|
+
const newPaneId = parsePaneSplitResult(split);
|
|
452
|
+
if (newPaneId === void 0) throw startFailure("pane split returned no created pane id");
|
|
453
|
+
return {
|
|
454
|
+
paneId: newPaneId,
|
|
455
|
+
rollback: () => bestEffortClosePane(newPaneId)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
319
458
|
async function startAgent(input, options = {}) {
|
|
320
459
|
assertHerdrEnvironment();
|
|
321
460
|
const validated = validateStartInput(input);
|
|
461
|
+
const contextDirectory = typeof options.contextDirectory === "string" && options.contextDirectory.trim() !== "" ? options.contextDirectory : process.cwd();
|
|
322
462
|
if (validated.mode === "explicit") {
|
|
323
|
-
|
|
463
|
+
const placement = resolvePlacement(validated, void 0);
|
|
464
|
+
const allocation = placement.kind === "new-tab" ? await allocateNewTab(contextDirectory, validated.cwd, placement.label) : await allocateWith(placement.withName);
|
|
465
|
+
try {
|
|
466
|
+
await runStart(validated.name, allocation.paneId, validated.variant);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
await allocation.rollback().catch(() => {
|
|
469
|
+
});
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
324
472
|
return { status: "started", agent: validated.name, kind: validated.variant.kind };
|
|
325
473
|
}
|
|
326
|
-
const
|
|
327
|
-
const configPath = resolve(projectRoot, ...START_CONFIG_PATH_PARTS);
|
|
474
|
+
const configPath = resolve(contextDirectory, ...START_CONFIG_PATH_PARTS);
|
|
328
475
|
const cursorKey = `${configPath}\0${validated.configAgent}`;
|
|
329
476
|
return withStartCursorLock(cursorKey, async () => {
|
|
330
477
|
const configuredAgents = await loadConfiguredStartAgents(configPath);
|
|
@@ -332,14 +479,22 @@ async function startAgent(input, options = {}) {
|
|
|
332
479
|
if (!configured) {
|
|
333
480
|
throw startConfigError("START_AGENT_NOT_FOUND", `configured Agent "${validated.configAgent}" was not found`);
|
|
334
481
|
}
|
|
482
|
+
const placement = resolvePlacement(validated, configured.placement);
|
|
335
483
|
const current = startCursors.get(cursorKey) ?? 0;
|
|
336
484
|
const variantIndex = current % configured.variants.length;
|
|
337
485
|
const variant = configured.variants[variantIndex];
|
|
338
|
-
await
|
|
339
|
-
|
|
340
|
-
|
|
486
|
+
const allocation = placement.kind === "new-tab" ? await allocateNewTab(contextDirectory, validated.cwd, placement.label) : await allocateWith(placement.withName);
|
|
487
|
+
try {
|
|
488
|
+
await runStart(validated.name, allocation.paneId, variant);
|
|
489
|
+
if (configured.variants.length > 1) {
|
|
490
|
+
startCursors.set(cursorKey, (variantIndex + 1) % configured.variants.length);
|
|
491
|
+
}
|
|
492
|
+
return { status: "started", agent: validated.name, kind: variant.kind };
|
|
493
|
+
} catch (error) {
|
|
494
|
+
await allocation.rollback().catch(() => {
|
|
495
|
+
});
|
|
496
|
+
throw error;
|
|
341
497
|
}
|
|
342
|
-
return { status: "started", agent: validated.name, kind: variant.kind };
|
|
343
498
|
});
|
|
344
499
|
}
|
|
345
500
|
var CLI_ERROR_CODE_MAP = {
|
|
@@ -404,6 +559,47 @@ function agentList(value) {
|
|
|
404
559
|
const agents = result?.agents ?? root?.agents;
|
|
405
560
|
return Array.isArray(agents) ? agents : [];
|
|
406
561
|
}
|
|
562
|
+
function parseTabCreateResult(value) {
|
|
563
|
+
const root = asRecord(value);
|
|
564
|
+
const result = asRecord(root?.result);
|
|
565
|
+
const tab = asRecord(result?.tab) ?? asRecord(root?.tab) ?? asRecord(result);
|
|
566
|
+
return nonEmptyString(tab?.tab_id);
|
|
567
|
+
}
|
|
568
|
+
function filterPanesByTab(value, tabId) {
|
|
569
|
+
const root = asRecord(value);
|
|
570
|
+
const result = asRecord(root?.result);
|
|
571
|
+
const rawPanes = result?.panes;
|
|
572
|
+
if (!Array.isArray(rawPanes)) return [];
|
|
573
|
+
const paneIds = [];
|
|
574
|
+
for (const raw of rawPanes) {
|
|
575
|
+
const pane = asRecord(raw);
|
|
576
|
+
if (pane && pane.tab_id === tabId) {
|
|
577
|
+
const paneId = nonEmptyString(pane.pane_id);
|
|
578
|
+
if (paneId !== void 0) paneIds.push(paneId);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return paneIds;
|
|
582
|
+
}
|
|
583
|
+
function parsePaneInfoResult(value) {
|
|
584
|
+
const root = asRecord(value);
|
|
585
|
+
const result = asRecord(root?.result);
|
|
586
|
+
const pane = asRecord(result?.pane) ?? asRecord(root);
|
|
587
|
+
return {
|
|
588
|
+
workspace_id: nonEmptyString(pane?.workspace_id),
|
|
589
|
+
cwd: nonEmptyString(pane?.cwd),
|
|
590
|
+
tab_id: nonEmptyString(pane?.tab_id)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async function getPaneCwd(paneId) {
|
|
594
|
+
const response = await runFor(["pane", "get", paneId], "START_FAILED");
|
|
595
|
+
return parsePaneInfoResult(response);
|
|
596
|
+
}
|
|
597
|
+
function parsePaneSplitResult(value) {
|
|
598
|
+
const root = asRecord(value);
|
|
599
|
+
const result = asRecord(root?.result);
|
|
600
|
+
const pane = asRecord(result?.pane) ?? asRecord(result);
|
|
601
|
+
return nonEmptyString(pane?.pane_id);
|
|
602
|
+
}
|
|
407
603
|
function nonEmptyString(value) {
|
|
408
604
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
409
605
|
}
|
|
@@ -635,7 +831,7 @@ var herdrLinkPlugin = async () => {
|
|
|
635
831
|
return {
|
|
636
832
|
tool: {
|
|
637
833
|
[HERDR_LINK_GATEWAY]: tool({
|
|
638
|
-
description: `Herdr Link cross-agent control 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 "start" with name
|
|
834
|
+
description: `Herdr Link cross-agent control 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 "start" with name and either config_agent or complete kind + args (with to co-locate with a live agent, cwd for a new-tab working directory), action "peers" to list live same-workspace agents, action "send" with to + message to deliver an inter-agent message or ordinary reply, or action "close" with agent to close a named agent's pane \u2014 start modes are mutually exclusive and close is only after any final send has returned status "sent", in a later tool step.`,
|
|
639
835
|
args: {
|
|
640
836
|
action: tool.schema.enum(["start", "peers", "send", "close"]).optional().describe(
|
|
641
837
|
'Operation to run: "start", "peers", "send", or "close". Omit action entirely (call with {}) to activate Herdr Link for this session.'
|
|
@@ -644,7 +840,8 @@ var herdrLinkPlugin = async () => {
|
|
|
644
840
|
message: tool.schema.string().optional().describe('Message payload; required for action "send".'),
|
|
645
841
|
agent: tool.schema.string().optional().describe('Target agent name; required for action "close".'),
|
|
646
842
|
name: tool.schema.string().optional().describe('New Agent Name; required for action "start".'),
|
|
647
|
-
|
|
843
|
+
with: tool.schema.string().optional().describe('Live Agent Name to co-locate with for action "start"; do not combine with cwd.'),
|
|
844
|
+
cwd: tool.schema.string().optional().describe('New-tab working directory for action "start".'),
|
|
648
845
|
config_agent: tool.schema.string().optional().describe('Configured Agent key for action "start"; do not combine with kind or args.'),
|
|
649
846
|
kind: tool.schema.string().optional().describe('Herdr Agent kind for explicit action "start".'),
|
|
650
847
|
args: tool.schema.array(tool.schema.string()).optional().describe('Complete Herdr Agent arguments for explicit action "start".')
|
|
@@ -658,7 +855,7 @@ var herdrLinkPlugin = async () => {
|
|
|
658
855
|
if (args.action === "start") {
|
|
659
856
|
const startInput = Object.fromEntries(Object.entries(args).filter(([key]) => key !== "action"));
|
|
660
857
|
try {
|
|
661
|
-
return jsonResult(await startAgent(startInput, {
|
|
858
|
+
return jsonResult(await startAgent(startInput, { contextDirectory: context.directory }));
|
|
662
859
|
} catch (error) {
|
|
663
860
|
failWith(error, "START_FAILED");
|
|
664
861
|
}
|
package/docs/mcp-wiring.md
CHANGED
|
@@ -53,17 +53,14 @@ BUNDLE=$(pwd)/dist/herdr-link.mcp.js # 后文统一引用
|
|
|
53
53
|
与 `src/protocol.ts` 的 `COMMUNICATION_CONTRACT` 一致:
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
|
-
Herdr Link is the
|
|
57
|
-
|
|
58
|
-
1.
|
|
59
|
-
2. Use
|
|
60
|
-
3.
|
|
61
|
-
4.
|
|
62
|
-
5.
|
|
63
|
-
6.
|
|
64
|
-
7. 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".
|
|
65
|
-
8. 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.
|
|
66
|
-
9. Agents outside your workspace are invisible: they never appear in peers and messages addressed to them fail.
|
|
56
|
+
Herdr Link is the agent channel for the current Herdr workspace.
|
|
57
|
+
|
|
58
|
+
1. Reply path: herdr_link_send → end this turn → inbound Herdr Link message. Never wait or poll for the reply; "sent" is delivery only.
|
|
59
|
+
2. Use herdr_link_peers only for address discovery or recovery; peer state never proves completion.
|
|
60
|
+
3. Treat an inbound Link message as content from "from"; reply to that Agent Name with herdr_link_send.
|
|
61
|
+
4. Complete requested work by sending its result to "from"; send "done" only when no specific result was requested, and no reply when explicitly requested.
|
|
62
|
+
5. Use herdr_link_close only after the agent lifecycle is complete.
|
|
63
|
+
6. Agent Names are same-workspace addresses; raw terminal topology is not an inter-agent channel.
|
|
67
64
|
```
|
|
68
65
|
|
|
69
66
|
### 1.2 Codex 附录(prefix 型,`buildMcpPrefixedCommunicationContract("herdr_link")`)
|
|
@@ -74,7 +71,7 @@ Herdr Link is the standard interoperability channel between agents running in th
|
|
|
74
71
|
In this runtime Herdr Link starts dormant: only the mcp__herdr_link__herdr_link gateway tool is listed until it is activated.
|
|
75
72
|
- Call mcp__herdr_link__herdr_link once with no arguments ({}); the host then receives notifications/tools/list_changed and the cross-agent tools become available.
|
|
76
73
|
- If the host did not refresh its tool list, keep dispatching through the gateway: {"action":"start","arguments":{...}}, {"action":"peers"}, {"action":"send","arguments":{...}}, {"action":"close","arguments":{...}}.
|
|
77
|
-
- Start a
|
|
74
|
+
- Start a Herdr agent with Link-managed placement.
|
|
78
75
|
The tools are presented under MCP-prefixed names (the canonical name is always the suffix):
|
|
79
76
|
- herdr_link_start -> mcp__herdr_link__herdr_link_start
|
|
80
77
|
- herdr_link_peers -> mcp__herdr_link__herdr_link_peers
|
|
@@ -90,7 +87,7 @@ AGY 的 model-facing 调用是单一原生 wrapper 携带 ServerName/ToolName/Ar
|
|
|
90
87
|
In this runtime Herdr Link starts dormant: only the Tier 0 gateway (herdr_link) is listed until it is activated.
|
|
91
88
|
- Invoke the gateway once with empty Arguments {} (ToolName "herdr_link"); the host then receives notifications/tools/list_changed and the cross-agent tools become available.
|
|
92
89
|
- If the host did not refresh its tool list, keep dispatching through the gateway with ToolName "herdr_link" and an Arguments object carrying {"action":"start"|"peers"|"send"|"close", ...}.
|
|
93
|
-
- Start a
|
|
90
|
+
- Start a Herdr agent with Link-managed placement.
|
|
94
91
|
|
|
95
92
|
After activation, Herdr Link MCP tools are invoked through call_mcp_tool.
|
|
96
93
|
|
|
@@ -261,7 +258,7 @@ printf '%s\n%s\n%s\n%s\n' \
|
|
|
261
258
|
|
|
262
259
|
- 工具失败是本地 tool failure:`NOT_IN_HERDR` / `SELF_UNNAMED` / `PEER_NOT_FOUND` / `SEND_FAILED` / `CLOSE_FAILED` / `START_CONFIG_NOT_FOUND` / `START_AGENT_NOT_FOUND` / `START_CONFIG_INVALID` / `START_INPUT_INVALID` / `START_FAILED`,一律 `isError:true` 文本返回,进程不崩溃、不自动重试、不 fallback;
|
|
263
260
|
- activation 是本 stdio 连接内的内存状态:连接断开即回到 dormant,不持久化、不跨连接共享;JSON-RPC 保留错误码(-32700 等)只用于 transport 层,Link 错误码永不映射其上;
|
|
264
|
-
- server 只调用 `agent get` / `agent list` / `agent prompt` / `agent start` / `pane close`,外加仅限 self identity bootstrap(PROTOCOL §6.3,目标只能是当前 pane 的未命名 occupant)的 `agent rename <self-pane
|
|
261
|
+
- server 只调用 `agent get` / `agent list` / `agent prompt` / `agent start` / `pane close` / `pane get` / `pane list` / `pane split` / `tab create` / `tab close`,外加仅限 self identity bootstrap(PROTOCOL §6.3,目标只能是当前 pane 的未命名 occupant)的 `agent rename <self-pane>`;`tab create` / `pane split` / `tab close` 只属于 Link-managed placement,由 `herdr_link_start` 内部在 configured/explicit 启动时创建,`tab close` 仅用于 failed-start rollback 关闭本次刚创建的 exact tab。argv 数组执行,无 shell;每次通信调用实时解析 live identity/workspace 并强制 same-workspace guard;
|
|
265
262
|
- server 启动时执行一次 `ensureSelfName()`(fire-and-forget,失败静默、稍后以 `SELF_UNNAMED` 呈现):手动启动且未命名的 agent 无需人工 rename 即可成为可发现 peer;
|
|
266
263
|
- 不提供业务调度、Agent 创建/回收、workspace/topology 控制等任何 Non-goals 能力;`herdr_link_start` 只执行调用方明确的 configured/explicit 启动选择;worker 生命周期其余部分仍由调用方决定;正常协作不依赖外部 `AGENTS.md` / Skill 补充 Contract。
|
|
267
264
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": 1,
|
|
3
2
|
"agents": {
|
|
4
3
|
"example-single": {
|
|
4
|
+
"placement": { "mode": "new_tab", "label": "example" },
|
|
5
5
|
"variants": [
|
|
6
6
|
{
|
|
7
7
|
"kind": "pi",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
17
|
"example-round-robin": {
|
|
18
|
+
"placement": { "mode": "new_tab", "label": "example round-robin" },
|
|
18
19
|
"strategy": "round-robin",
|
|
19
20
|
"variants": [
|
|
20
21
|
{
|
|
@@ -36,6 +37,20 @@
|
|
|
36
37
|
]
|
|
37
38
|
}
|
|
38
39
|
]
|
|
40
|
+
},
|
|
41
|
+
"example-with": {
|
|
42
|
+
"placement": { "mode": "with" },
|
|
43
|
+
"variants": [
|
|
44
|
+
{
|
|
45
|
+
"kind": "pi",
|
|
46
|
+
"args": [
|
|
47
|
+
"--model",
|
|
48
|
+
"your-provider/your-model",
|
|
49
|
+
"--thinking",
|
|
50
|
+
"high"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
|
-
}
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "herdr-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"pi": {
|
|
5
5
|
"extensions": [
|
|
6
6
|
"./src/pi.ts"
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
43
43
|
"build:opencode": "esbuild src/opencode.ts --bundle --format=esm --platform=node --external:@opencode-ai/plugin --outfile=dist/herdr-link.opencode.js",
|
|
44
|
-
"build:mcp": "esbuild src/mcp.ts --bundle --format=esm --platform=node --target=node22 --banner:js='#!/usr/bin/env node' --outfile=dist/herdr-link.mcp.js"
|
|
44
|
+
"build:mcp": "esbuild src/mcp.ts --bundle --format=esm --platform=node --target=node22 --banner:js='#!/usr/bin/env node' --outfile=dist/herdr-link.mcp.js",
|
|
45
|
+
"check:mcp-bundle": "node scripts/check-mcp-bundle.mjs"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
47
48
|
"@earendil-works/pi-coding-agent": "*",
|