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.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Herdr Link OpenCode Runtime adapter v2 — single-gateway presentation.
3
+ *
4
+ * The model-facing surface is exactly one tiny `herdr_link` dispatcher tool,
5
+ * in both dormant and active states. Calling it with no arguments (`{}`)
6
+ * idempotently activates the channel for the CURRENT session and returns
7
+ * `{ status: "active", capabilities: ["peers", "send", "close"] }`; while a
8
+ * session is active the same gateway executes deterministic actions
9
+ * (`action`: "peers" | "send" | "close") against the core control layer and
10
+ * the compact Communication Contract is injected into that session's system
11
+ * prompt. The three Tier 1 tools are never registered as always-resident
12
+ * surfaces.
13
+ *
14
+ * API basis (public @opencode-ai/plugin 1.18.x only):
15
+ * - `Hooks.tool` registers tools process-wide; there is no public per-session
16
+ * registration or dynamic unload, hence the single-gateway fallback instead
17
+ * of Pi-style `setActiveTools`.
18
+ * - `ToolContext.sessionID` attributes each gateway execution to its session;
19
+ * the per-session ephemeral activation set lives in the plugin closure.
20
+ * - `experimental.chat.system.transform` input carries `sessionID?`; it is
21
+ * OPTIONAL in the public types, so injection fails closed when it is absent
22
+ * (an unattributable system build is never treated as active).
23
+ *
24
+ * Known sessionID limitations (explicit):
25
+ * - `Hooks.tool.definition` exposes only `toolID` (no sessionID), so mutating
26
+ * tool schemas per session cannot be done safely; the gateway schema is
27
+ * static and admits both `{}` (activate) and fully-formed actions.
28
+ * - The activation set is in-memory per plugin instance: restarting the
29
+ * OpenCode server returns every session to dormant until it re-activates.
30
+ */
31
+ import { tool, type Plugin } from "@opencode-ai/plugin";
32
+
33
+ import { closeAgentPane, ensureSelfName, listPeers, sendMessage } from "./herdr.ts";
34
+ import {
35
+ COMMUNICATION_CONTRACT,
36
+ HERDR_LINK_GATEWAY,
37
+ HerdrLinkError,
38
+ formatAgentFacingError,
39
+ type LinkErrorCode,
40
+ } from "./protocol.ts";
41
+
42
+ /** Runtime-specific active presentation; the semantic Contract remains canonical. */
43
+ const GATEWAY_PRESENTATION_APPENDIX = `In this runtime the active Herdr Link capabilities are dispatched through the single herdr_link gateway.
44
+ - Use herdr_link with action "peers" to list live same-workspace agents.
45
+ - Use herdr_link with action "send", to, message, and reply_to when replying.
46
+ - Use herdr_link with action "close" and an Agent Name only after any final send returns status "sent", in a later tool step.`;
47
+
48
+ const GATEWAY_CONTRACT = `${COMMUNICATION_CONTRACT}\n\n${GATEWAY_PRESENTATION_APPENDIX}`;
49
+
50
+ function isHerdrEnvironment(): boolean {
51
+ return (
52
+ process.env.HERDR_ENV === "1" &&
53
+ Boolean(process.env.HERDR_BIN_PATH) &&
54
+ Boolean(process.env.HERDR_PANE_ID)
55
+ );
56
+ }
57
+
58
+ function jsonResult(value: object): string {
59
+ return JSON.stringify(value);
60
+ }
61
+
62
+ /** Throws the stable `${CODE}: ${detail}` Agent-facing message; causes stay internal. */
63
+ function failWith(error: unknown, fallbackCode: LinkErrorCode): never {
64
+ throw new Error(formatAgentFacingError(error, fallbackCode), { cause: error });
65
+ }
66
+
67
+ /** Dispatcher-level usage guard for actions rejected by the schema anyway. */
68
+ function failInvalidAction(action: string): never {
69
+ throw new Error(
70
+ `INVALID_ACTION: herdr_link action "${action}" is not supported; use "peers", "send", "close", or omit action (call with {}) to activate.`,
71
+ );
72
+ }
73
+
74
+ export const herdrLinkPlugin: Plugin = async () => {
75
+ if (!isHerdrEnvironment()) {
76
+ return {};
77
+ }
78
+
79
+ // Self identity bootstrap (PROTOCOL.md §6.3): fire-and-forget so plugin
80
+ // startup never blocks or fails on Herdr IO; gateway actions fall back
81
+ // through getSelfContext(). Failures surface later as SELF_UNNAMED.
82
+ void ensureSelfName().catch(() => {});
83
+
84
+ // Per-runtime-session activation set. Ephemeral by design: in-memory only,
85
+ // scoped to this plugin instance, never persisted, never shared across
86
+ // instances. Losing it (server restart) merely returns sessions to dormant.
87
+ const activatedSessions = new Set<string>();
88
+
89
+ return {
90
+ tool: {
91
+ [HERDR_LINK_GATEWAY]: tool({
92
+ description:
93
+ "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. " +
94
+ 'Call once with no arguments {} to activate Herdr Link for this session; the response lists capabilities. ' +
95
+ '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 — ' +
96
+ 'only after any final send has returned status "sent", and in a later tool step.',
97
+ args: {
98
+ action: tool.schema
99
+ .enum(["peers", "send", "close"])
100
+ .optional()
101
+ .describe(
102
+ 'Operation to run: "peers" | "send" | "close". Omit action entirely (call with {}) to activate Herdr Link for this session.',
103
+ ),
104
+ to: tool.schema
105
+ .string()
106
+ .optional()
107
+ .describe('Target agent name; required for action "send".'),
108
+ message: tool.schema
109
+ .string()
110
+ .optional()
111
+ .describe('Message payload; required for action "send".'),
112
+ reply_to: tool.schema
113
+ .string()
114
+ .optional()
115
+ .describe('Message id being replied to; optional, only with action "send".'),
116
+ agent: tool.schema
117
+ .string()
118
+ .optional()
119
+ .describe('Target agent name; required for action "close".'),
120
+ },
121
+ async execute(args, context) {
122
+ if (args.action === undefined) {
123
+ activatedSessions.add(context.sessionID);
124
+ return jsonResult({ status: "active", capabilities: ["peers", "send", "close"] });
125
+ }
126
+ // Gateway action dispatch is also an explicit activation path for
127
+ // hosts that bypass the empty gateway call or do not refresh schemas.
128
+ activatedSessions.add(context.sessionID);
129
+
130
+ if (args.action === "peers") {
131
+ try {
132
+ return jsonResult(await listPeers());
133
+ } catch (error) {
134
+ failWith(error, "NOT_IN_HERDR");
135
+ }
136
+ }
137
+
138
+ if (args.action === "send") {
139
+ if (typeof args.to !== "string" || args.to === "") {
140
+ failWith(new HerdrLinkError("SEND_FAILED", '"to" must be a non-empty string'), "SEND_FAILED");
141
+ }
142
+ if (typeof args.message !== "string" || args.message === "") {
143
+ failWith(new HerdrLinkError("SEND_FAILED", '"message" must be a non-empty string'), "SEND_FAILED");
144
+ }
145
+ try {
146
+ const envelope = await sendMessage(args.to, args.message, args.reply_to);
147
+ return jsonResult({ status: "sent", id: envelope.id, to: envelope.to });
148
+ } catch (error) {
149
+ failWith(error, "SEND_FAILED");
150
+ }
151
+ }
152
+
153
+ if (args.action === "close") {
154
+ if (typeof args.agent !== "string" || args.agent === "") {
155
+ failWith(new HerdrLinkError("CLOSE_FAILED", '"agent" must be a non-empty string'), "CLOSE_FAILED");
156
+ }
157
+ try {
158
+ await closeAgentPane(args.agent);
159
+ return jsonResult({ status: "closed", agent: args.agent });
160
+ } catch (error) {
161
+ failWith(error, "CLOSE_FAILED");
162
+ }
163
+ }
164
+
165
+ failInvalidAction(String(args.action));
166
+ },
167
+ }),
168
+ },
169
+ "experimental.chat.system.transform": async (input, output) => {
170
+ // Fail closed: an optional/absent sessionID cannot be attributed to an
171
+ // activation, so the Contract is withheld rather than guessed.
172
+ if (input.sessionID === undefined || !activatedSessions.has(input.sessionID)) {
173
+ return;
174
+ }
175
+ if (!output.system.includes(GATEWAY_CONTRACT)) {
176
+ output.system.push(GATEWAY_CONTRACT);
177
+ }
178
+ },
179
+ };
180
+ };
package/src/pi.ts ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Herdr Link Pi Runtime adapter v2 — Tier 0/Tier 1 presentation.
3
+ *
4
+ * Tier 0 (dormant): with the three Herdr environment variables present, the
5
+ * adapter registers everything but keeps the model-facing surface down to the
6
+ * tiny `herdr_link` gateway. The three Tier 1 tools
7
+ * (`herdr_link_peers`/`herdr_link_send`/`herdr_link_close`) stay inactive and
8
+ * no Communication Contract is injected.
9
+ *
10
+ * Tier 1 (active): calling the gateway with `{}` idempotently activates the
11
+ * channel inside the current runtime session via the official dynamic tool
12
+ * API (`pi.setActiveTools`, additive change) and starts injecting the compact
13
+ * Contract through `before_agent_start`.
14
+ *
15
+ * API basis (public Pi Extension API, @earendil-works/pi-coding-agent 0.84.x):
16
+ * - `pi.registerTool()` must cover every tool before it can appear in
17
+ * `pi.setActiveTools()` ("Names passed to pi.setActiveTools() must already
18
+ * be registered"); action methods throw while extensions are still loading,
19
+ * so the initial dormant set is applied on `session_start` — the same
20
+ * pattern as the official Dynamic Tool Loading example in docs/extensions.md.
21
+ * - Activation inside a tool `execute()` may call `pi.setActiveTools()`
22
+ * additively; Pi applies the new set before the next model request.
23
+ * - Lazily loaded tools should omit active-only prompt metadata
24
+ * (`promptSnippet`/`promptGuidelines`) and rely on their `description`;
25
+ * activating such metadata would rebuild the system prompt mid-session.
26
+ */
27
+ import type { ExtensionAPI, ToolExecutionMode } from "@earendil-works/pi-coding-agent";
28
+ import { Type } from "typebox";
29
+
30
+ import { closeAgentPane, ensureSelfName, listPeers, sendMessage } from "./herdr.ts";
31
+ import { COMMUNICATION_CONTRACT, formatAgentFacingError } from "./protocol.ts";
32
+
33
+ const TIER1_TOOL_NAMES = ["herdr_link_peers", "herdr_link_send", "herdr_link_close"] as const;
34
+ const TIER1_TOOL_SET = new Set<string>(TIER1_TOOL_NAMES);
35
+
36
+ const GATEWAY_PARAMETERS = Type.Object({});
37
+ const PEERS_PARAMETERS = Type.Object({});
38
+ const SEND_PARAMETERS = Type.Object({
39
+ to: Type.String(),
40
+ message: Type.String(),
41
+ reply_to: Type.Optional(Type.String()),
42
+ });
43
+ const CLOSE_PARAMETERS = Type.Object({
44
+ agent: Type.String(),
45
+ });
46
+
47
+
48
+ function toolResult(value: object) {
49
+ return {
50
+ content: [{ type: "text" as const, text: JSON.stringify(value) }],
51
+ details: value,
52
+ };
53
+ }
54
+
55
+ function rethrowToolError(error: unknown, fallbackCode: "NOT_IN_HERDR" | "SEND_FAILED" | "CLOSE_FAILED"): never {
56
+ const toolError = new Error(formatAgentFacingError(error, fallbackCode), { cause: error });
57
+ throw toolError;
58
+ }
59
+
60
+ export default function (pi: ExtensionAPI): void {
61
+ if (
62
+ process.env.HERDR_ENV !== "1" ||
63
+ !process.env.HERDR_BIN_PATH ||
64
+ !process.env.HERDR_PANE_ID
65
+ ) {
66
+ return;
67
+ }
68
+
69
+ // Per-runtime-session state only: never persisted, never shared across
70
+ // sessions. Every session start returns the channel to dormant.
71
+ let activated = false;
72
+
73
+ // --- Tier 1 tools: registered up front (Pi requires registration before
74
+ // setActiveTools), kept initially inactive by the session_start hook below.
75
+ // Their descriptions alone carry the canonical affordances; prompt metadata
76
+ // is intentionally omitted (see module doc).
77
+ pi.registerTool({
78
+ name: "herdr_link_peers",
79
+ label: "Herdr Link Peers",
80
+ description:
81
+ "Discover named agents available through the cross-agent communication channel. Returns { self, peers }; addresses are Agent Names.",
82
+ parameters: PEERS_PARAMETERS,
83
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
84
+ try {
85
+ return toolResult(await listPeers());
86
+ } catch (error) {
87
+ rethrowToolError(error, "NOT_IN_HERDR");
88
+ }
89
+ },
90
+ });
91
+
92
+ pi.registerTool({
93
+ name: "herdr_link_send",
94
+ label: "Herdr Link Send",
95
+ description:
96
+ 'Send an inter-agent message (protocol herdr-link/1) to another agent through the cross-agent communication channel. status "sent" means Herdr accepted delivery, not that the peer finished its task. When replying, set reply_to to the received message id.',
97
+ parameters: SEND_PARAMETERS,
98
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
99
+ try {
100
+ const envelope = await sendMessage(params.to, params.message, params.reply_to);
101
+ return toolResult({ status: "sent", id: envelope.id, to: envelope.to });
102
+ } catch (error) {
103
+ rethrowToolError(error, "SEND_FAILED");
104
+ }
105
+ },
106
+ });
107
+
108
+ pi.registerTool({
109
+ name: "herdr_link_close",
110
+ label: "Herdr Link Close",
111
+ // Pi 默认并行执行同一 assistant response 的 sibling tool calls;close 与 send 同批时
112
+ // 必须保证 send 先完成("sent" 语义=Herdr 已接受投递),故 close 声明为 sequential,
113
+ // 使含 close 的批次整体串行。peers/send 保持默认并行。
114
+ executionMode: "sequential" as ToolExecutionMode,
115
+ description:
116
+ 'Close the Herdr pane currently hosting a named agent. Sequential: if a final message is needed, send it first and call close in a later tool step after herdr_link_send returns status "sent".',
117
+ parameters: CLOSE_PARAMETERS,
118
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
119
+ try {
120
+ await closeAgentPane(params.agent);
121
+ return toolResult({ status: "closed", agent: params.agent });
122
+ } catch (error) {
123
+ rethrowToolError(error, "CLOSE_FAILED");
124
+ }
125
+ },
126
+ });
127
+
128
+ // --- Tier 0 gateway: the only model-visible Herdr surface while dormant.
129
+ // It performs activation only; it never executes peers/send/close work.
130
+ pi.registerTool({
131
+ name: "herdr_link",
132
+ label: "Herdr Link",
133
+ description:
134
+ "Activate the Herdr Link channel only when the user explicitly asks to use Herdr or when handling an inbound Herdr Link message. Call once with empty arguments {} before using Herdr Link; this enables herdr_link_peers, herdr_link_send, and herdr_link_close.",
135
+ promptSnippet: "Activate the Herdr Link cross-agent channel (peers/send/close).",
136
+ parameters: GATEWAY_PARAMETERS,
137
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
138
+ activateChannel();
139
+ return toolResult({ status: "active", capabilities: ["peers", "send", "close"] });
140
+ },
141
+ });
142
+
143
+ // Additive-only change (official requirement): keep every currently active
144
+ // tool — built-ins and other extensions' tools included — and enable Tier 1.
145
+ function activateChannel(): void {
146
+ if (activated) return;
147
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), ...TIER1_TOOL_NAMES])]);
148
+ activated = true;
149
+ }
150
+
151
+ pi.on("session_start", () => {
152
+ // Dormant presentation for this runtime session: drop any Tier 1 tools
153
+ // from the active set (registration makes tools active by default) while
154
+ // preserving built-ins, other extensions' tools, and the gateway itself.
155
+ // Resetting `activated` keeps activation scoped to the current session.
156
+ activated = false;
157
+ pi.setActiveTools(pi.getActiveTools().filter((name) => !TIER1_TOOL_SET.has(name)));
158
+
159
+ // Self identity bootstrap (PROTOCOL.md §6.3), once per runtime session:
160
+ // fire-and-forget so session start never blocks or fails on Herdr IO;
161
+ // communication paths fall back through getSelfContext(). Failures
162
+ // surface later as SELF_UNNAMED.
163
+ void ensureSelfName().catch(() => {});
164
+ });
165
+
166
+ pi.on("before_agent_start", (event) => {
167
+ if (!activated) return undefined;
168
+ return { systemPrompt: `${event.systemPrompt}\n\n${COMMUNICATION_CONTRACT}` };
169
+ });
170
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Herdr Link protocol core — pure protocol types and helpers.
3
+ *
4
+ * No Herdr IO in this file. The canonical specification is PROTOCOL.md
5
+ * at the repository root; this module is its machine-readable core.
6
+ */
7
+
8
+ export const PROTOCOL_ID = "herdr-link/1" as const;
9
+
10
+ /** Herdr agent-name rule: `[a-z][a-z0-9_-]{0,31}`, unique among live agents. */
11
+ export const AGENT_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
12
+ /** Message id rule from PROTOCOL.md §2.3: `hl_<timestamp>_<random>`. */
13
+ export const MESSAGE_ID_RE = /^hl_[a-z0-9]+_[a-z0-9]+$/;
14
+
15
+ /* ------------------------------------------------------------------ *
16
+ * Naming tiers (blueprint v2)
17
+ *
18
+ * Tier 0 is the Herdr Link gateway itself — the host registration
19
+ * namespace every runtime presents its tools against (underscore form;
20
+ * docs/mcp-wiring.md). Tier 1 are the canonical tool names exposed
21
+ * through the gateway. Both are stable machine-usable constants;
22
+ * runtime-specific presented names must map deterministically onto them
23
+ * (PROTOCOL.md §4.4).
24
+ * ------------------------------------------------------------------ */
25
+
26
+ /** Tier 0 — gateway name in its underscore host-namespace form. */
27
+ export const HERDR_LINK_GATEWAY = "herdr_link" as const;
28
+
29
+ /** Tier 1 — canonical tool names exposed through the gateway. */
30
+ export const TOOL_PEERS = "herdr_link_peers" as const;
31
+ export const TOOL_SEND = "herdr_link_send" as const;
32
+ export const TOOL_CLOSE = "herdr_link_close" as const;
33
+ export const HERDR_LINK_TOOLS = [TOOL_PEERS, TOOL_SEND, TOOL_CLOSE] as const;
34
+
35
+ /* ------------------------------------------------------------------ *
36
+ * Agent state (blueprint v2)
37
+ * ------------------------------------------------------------------ */
38
+
39
+ /** Live activity states; any unrecognized Herdr status maps to "unknown". */
40
+ export const AGENT_STATES = ["idle", "working", "blocked", "done", "unknown"] as const;
41
+
42
+ export type AgentState = (typeof AGENT_STATES)[number];
43
+
44
+ /** Maps a raw Herdr status value onto the closed AgentState vocabulary. */
45
+ export function toAgentState(value: unknown): AgentState {
46
+ if (typeof value === "string") {
47
+ const normalized = value.trim().toLowerCase();
48
+ if ((AGENT_STATES as readonly string[]).includes(normalized)) {
49
+ return normalized as AgentState;
50
+ }
51
+ }
52
+ return "unknown";
53
+ }
54
+
55
+ /** A named agent together with its live activity state. Never carries topology ids. */
56
+ export interface PeerInfo {
57
+ name: string;
58
+ state: AgentState;
59
+ }
60
+
61
+ /**
62
+ * Instant peer directory (blueprint v2): same-workspace live agents only,
63
+ * self excluded. Generated fresh on every call; never persisted or cached.
64
+ */
65
+ export interface PeerDirectory {
66
+ self: PeerInfo;
67
+ peers: PeerInfo[];
68
+ }
69
+
70
+ /**
71
+ * Live identity of one agent, freshly resolved from Herdr on every call.
72
+ * Ambient environment values (e.g. HERDR_WORKSPACE_ID) are never a
73
+ * substitute for these fields.
74
+ */
75
+ export interface AgentContext {
76
+ /** Valid Herdr agent name. */
77
+ name: string;
78
+ /** Authoritative workspace id from the live record; "" when unreported (comparisons fail closed). */
79
+ workspace_id: string;
80
+ /** Pane currently hosting the agent. */
81
+ pane_id: string;
82
+ /** Live activity state mapped onto AgentState. */
83
+ agent_status: AgentState;
84
+ }
85
+
86
+ /** The cross-agent message envelope (PROTOCOL.md §2). Minimal fields only. */
87
+ export interface HerdrLinkEnvelope {
88
+ protocol: typeof PROTOCOL_ID;
89
+ id: string;
90
+ from: string;
91
+ to: string;
92
+ reply_to?: string;
93
+ message: string;
94
+ }
95
+
96
+ /** V1 error codes (PROTOCOL.md §7). All are local tool failures, never an envelope. */
97
+ export const LINK_ERROR_CODES = [
98
+ "NOT_IN_HERDR",
99
+ "SELF_UNNAMED",
100
+ "PEER_NOT_FOUND",
101
+ "SEND_FAILED",
102
+ "CLOSE_FAILED",
103
+ ] as const;
104
+
105
+ export type LinkErrorCode = (typeof LINK_ERROR_CODES)[number];
106
+
107
+ /** Standard failure for every Herdr Link tool. */
108
+ export class HerdrLinkError extends Error {
109
+ readonly code: LinkErrorCode;
110
+
111
+ constructor(code: LinkErrorCode, detail?: string) {
112
+ super(detail ? `${code}: ${detail}` : code);
113
+ this.name = "HerdrLinkError";
114
+ this.code = code;
115
+ }
116
+ }
117
+
118
+ /** Stable Agent-facing details; raw Herdr diagnostics remain internal to the error object. */
119
+ export const AGENT_ERROR_DETAILS: Record<LinkErrorCode, string> = {
120
+ NOT_IN_HERDR: "Herdr environment is unavailable",
121
+ SELF_UNNAMED: "Herdr Link could not establish a stable Agent Name",
122
+ PEER_NOT_FOUND: "target agent is not a live peer",
123
+ SEND_FAILED: "Herdr did not accept message delivery",
124
+ CLOSE_FAILED: "Herdr pane close failed",
125
+ };
126
+
127
+ /** Formats a Link failure without exposing raw Herdr topology or CLI details. */
128
+ export function formatAgentFacingError(error: unknown, fallbackCode: LinkErrorCode): string {
129
+ const code = error instanceof HerdrLinkError ? error.code : fallbackCode;
130
+ return `${code}: ${AGENT_ERROR_DETAILS[code]}`;
131
+ }
132
+
133
+ /** Creates a unique message id: `hl_` + base36 timestamp + random suffix. */
134
+ export function createMessageId(): string {
135
+ const ts = Date.now().toString(36);
136
+ const rand = Math.random().toString(36).slice(2, 10) || "0";
137
+ return `hl_${ts}_${rand}`;
138
+ }
139
+
140
+ export function isValidAgentName(name: string): boolean {
141
+ return AGENT_NAME_RE.test(name);
142
+ }
143
+
144
+ export function isValidMessageId(id: string): boolean {
145
+ return MESSAGE_ID_RE.test(id);
146
+ }
147
+
148
+ export interface BuildEnvelopeInput {
149
+ from: string;
150
+ to: string;
151
+ message: string;
152
+ reply_to?: string;
153
+ }
154
+
155
+ /**
156
+ * Builds a validated envelope. `from` is supplied by the Adapter from Herdr
157
+ * identity — the model never submits it. `to`/`message` originate from the
158
+ * model and are validated here.
159
+ */
160
+ export function buildEnvelope(input: BuildEnvelopeInput): HerdrLinkEnvelope {
161
+ if (!isValidAgentName(input.from)) {
162
+ throw new HerdrLinkError(
163
+ "SELF_UNNAMED",
164
+ `self agent name "${input.from}" is not a valid Herdr agent name`,
165
+ );
166
+ }
167
+ if (!input.to || !isValidAgentName(input.to)) {
168
+ throw new HerdrLinkError(
169
+ "PEER_NOT_FOUND",
170
+ `target agent name "${input.to}" is not a valid Herdr agent name`,
171
+ );
172
+ }
173
+ if (typeof input.message !== "string" || input.message.trim() === "") {
174
+ throw new HerdrLinkError("SEND_FAILED", "message must be a non-empty string");
175
+ }
176
+ if (input.reply_to !== undefined && !isValidMessageId(input.reply_to)) {
177
+ throw new HerdrLinkError(
178
+ "SEND_FAILED",
179
+ "reply_to must be a valid herdr-link/1 message id when present",
180
+ );
181
+ }
182
+ const envelope: HerdrLinkEnvelope = {
183
+ protocol: PROTOCOL_ID,
184
+ id: createMessageId(),
185
+ from: input.from,
186
+ to: input.to,
187
+ message: input.message,
188
+ };
189
+ if (input.reply_to !== undefined) {
190
+ envelope.reply_to = input.reply_to;
191
+ }
192
+ return envelope;
193
+ }
194
+
195
+ /**
196
+ * Type guard for receiving side (PROTOCOL.md §3 rule 3): an incoming payload
197
+ * is a Herdr Link message iff it carries `protocol: "herdr-link/1"` and a
198
+ * string `message` body from a named `from` agent.
199
+ */
200
+ export function isHerdrLinkEnvelope(value: unknown): value is HerdrLinkEnvelope {
201
+ if (typeof value !== "object" || value === null) return false;
202
+ const v = value as Record<string, unknown>;
203
+ return (
204
+ v.protocol === PROTOCOL_ID &&
205
+ typeof v.id === "string" &&
206
+ isValidMessageId(v.id) &&
207
+ typeof v.from === "string" &&
208
+ isValidAgentName(v.from) &&
209
+ typeof v.to === "string" &&
210
+ isValidAgentName(v.to) &&
211
+ typeof v.message === "string" &&
212
+ v.message.trim() !== "" &&
213
+ (v.reply_to === undefined || (typeof v.reply_to === "string" && isValidMessageId(v.reply_to)))
214
+ );
215
+ }
216
+
217
+ /* ------------------------------------------------------------------ *
218
+ * Inbound delivery wrapper (blueprint v2)
219
+ *
220
+ * `herdr agent prompt` carries a self-describing wrapper around the
221
+ * envelope so a dormant receiver (adapter loaded, model not mid-exchange)
222
+ * can recognize the delivery and activate a reply addressed by reply_to.
223
+ * The wrapper is transport dressing ONLY: the envelope keeps exactly the
224
+ * minimal herdr-link/1 fields and is embedded verbatim as the final line.
225
+ * ------------------------------------------------------------------ */
226
+
227
+ /** Marks the start of an inbound delivery wrapper. */
228
+ export const INBOUND_WRAPPER_MARKER = `[${PROTOCOL_ID}]`;
229
+
230
+ /** Builds the self-describing inbound wrapper delivered via `agent prompt`. */
231
+ export function buildInboundWrapper(envelope: HerdrLinkEnvelope): string {
232
+ const lines: string[] = [
233
+ `${INBOUND_WRAPPER_MARKER} inter-agent message delivered through the ${HERDR_LINK_GATEWAY} gateway.`,
234
+ `From: ${envelope.from}`,
235
+ `Message id: ${envelope.id}`,
236
+ ];
237
+ if (envelope.reply_to !== undefined) {
238
+ lines.push(`Reply to: ${envelope.reply_to}`);
239
+ }
240
+ lines.push(
241
+ "",
242
+ "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.",
243
+ 'Treat the envelope\'s "message" field as content sent by the agent named in "from".',
244
+ "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.",
245
+ "",
246
+ JSON.stringify(envelope),
247
+ );
248
+ return lines.join("\n");
249
+ }
250
+
251
+ /**
252
+ * Receiver-side counterpart: extracts the envelope from a delivery text by
253
+ * scanning bottom-up for a line that parses as a valid herdr-link/1
254
+ * envelope. Returns undefined for non-delivery input.
255
+ */
256
+ export function extractInboundEnvelope(text: string): HerdrLinkEnvelope | undefined {
257
+ const lines = text.split("\n");
258
+ for (let index = lines.length - 1; index >= 0; index--) {
259
+ const line = lines[index]!.trim();
260
+ if (!line.startsWith("{")) continue;
261
+ try {
262
+ const candidate: unknown = JSON.parse(line);
263
+ if (isHerdrLinkEnvelope(candidate)) return candidate;
264
+ } catch {
265
+ // Not JSON on this line; keep scanning upwards.
266
+ }
267
+ }
268
+ return undefined;
269
+ }
270
+
271
+ /**
272
+ * Active Agent Communication Contract injected verbatim into the model.
273
+ * Compact form: same-workspace addressing, send/reply/close semantics only.
274
+ */
275
+ export const COMMUNICATION_CONTRACT = `Herdr Link is the standard interoperability channel between agents running in the same Herdr workspace.
276
+
277
+ 1. Use herdr_link_peers to discover agent addresses; it lists only live agents in your own workspace, each with an advisory activity state.
278
+ 2. Use herdr_link_send to send messages to another agent.
279
+ 3. A message with protocol "herdr-link/1" is an inter-agent message.
280
+ 4. Treat its "message" field as content sent by the agent named in "from".
281
+ 5. When replying, send to the received "from" agent and set reply_to to the received "id".
282
+ 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".
283
+ 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.
284
+ 8. Agents outside your workspace are invisible: they never appear in peers and messages addressed to them fail.`;