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.
@@ -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 new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.";
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 standard interoperability channel between agents running in the same Herdr workspace.
99
+ var COMMUNICATION_CONTRACT = `Herdr Link is the agent channel for the current Herdr workspace.
100
100
 
101
- 1. Use herdr_link_peers only for agent-address discovery or explicit recovery. Its activity state is advisory and must not be used to wait for or infer task completion. When further progress depends on a peer reply, end the current turn and continue when that reply arrives as a new inbound herdr-link/1 message.
102
- 2. Use herdr_link_send to send messages to another agent.
103
- 3. A message with protocol "herdr-link/1" is an inter-agent message.
104
- 4. Treat its "message" field as content sent by the agent named in "from".
105
- 5. When replying, use herdr_link_send to the agent named in "from".
106
- 6. When a received inter-agent message requests work, report the final outcome to the agent named in "from" using herdr_link_send. If specific reply content was requested, send that result; otherwise, after successful completion, send exactly "done". If the work cannot be completed, send a concise failure or blocker. If the sender explicitly requested no reply, do not send a completion message.
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", "pane", "config_agent", "kind", "args"]);
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
- const pane = value.pane;
195
- if (typeof pane !== "string" || pane.trim() === "") {
196
- throw startInputError('"pane" must be a non-empty pane id');
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, pane, configAgent };
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
- return { mode: "explicit", name, pane, variant: { kind, args: [...args] } };
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, ["version", "agents"], "configuration 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
- await runStart(validated.name, validated.pane, validated.variant);
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 projectRoot = typeof options.cwd === "string" && options.cwd.trim() !== "" ? options.cwd : process.cwd();
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 runStart(validated.name, validated.pane, variant);
339
- if (configured.variants.length > 1) {
340
- startCursors.set(cursorKey, (variantIndex + 1) % configured.variants.length);
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 + pane and either config_agent or complete kind + args, 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.`,
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
- pane: tool.schema.string().optional().describe('Existing pane id; required for action "start".'),
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, { cwd: context.directory }));
858
+ return jsonResult(await startAgent(startInput, { contextDirectory: context.directory }));
662
859
  } catch (error) {
663
860
  failWith(error, "START_FAILED");
664
861
  }
@@ -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 standard interoperability channel between agents running in the same Herdr workspace.
57
-
58
- 1. Use herdr_link_peers only for agent-address discovery or explicit recovery. Its activity state is advisory and must not be used to wait for or infer task completion. When further progress depends on a peer reply, end the current turn and continue when that reply arrives as a new inbound herdr-link/1 message.
59
- 2. Use herdr_link_send to send messages to another agent.
60
- 3. A message with protocol "herdr-link/1" is an inter-agent message.
61
- 4. Treat its "message" field as content sent by the agent named in "from".
62
- 5. When replying, use herdr_link_send to the agent named in "from".
63
- 6. When a received inter-agent message requests work, report the final outcome to the agent named in "from" using herdr_link_send. If specific reply content was requested, send that result; otherwise, after successful completion, send exactly "done". If the work cannot be completed, send a concise failure or blocker. If the sender explicitly requested no reply, do not send a completion message.
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 new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.
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 new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.
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>`,共六个 CLI 面,argv 数组执行,无 shell;每次通信调用实时解析 live identity/workspace 并强制 same-workspace guard;
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.4.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": "*",