@canonmsg/agent-sdk 13.2.1 → 13.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import {
8
8
  wasmMlsCustody,
9
9
  windowsAclProblems,
10
10
  writeDurable
11
- } from "./chunk-POBMFS2K.js";
11
+ } from "./chunk-BQGXOAIW.js";
12
12
  import {
13
13
  BoundedFileReadError,
14
14
  CanonApiError,
@@ -30,17 +30,20 @@ import {
30
30
  renderCanonHostInboundContent,
31
31
  resolveCanonRuntimeConnection,
32
32
  splitTextWithinBytes,
33
+ utf8ByteLength,
33
34
  verifyCanonRuntimeConnection
34
- } from "./chunk-HTLKCUQG.js";
35
+ } from "./chunk-SS22I6HM.js";
35
36
  import {
37
+ EndpointError,
36
38
  NodeMlsDevCustodyError,
37
39
  createNodeMlsDevCustody,
38
40
  createPortableNodeMlsDevCustody
39
- } from "./chunk-BPSFHEAA.js";
41
+ } from "./chunk-HFT75BNB.js";
40
42
  import {
41
43
  parseMlsDevClientConfiguration
42
- } from "./chunk-2NVLVPSG.js";
44
+ } from "./chunk-NIORQKUI.js";
43
45
  import {
46
+ AGENT_HOME_ELSEWHERE,
44
47
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
45
48
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
46
49
  CONNECTOR_RELAY_INSTALLATION_PREFIX,
@@ -54,10 +57,13 @@ import {
54
57
  MLS_DEV_TEXT_PROTOCOL_V3,
55
58
  NO_REPLY_ACK_NOTE,
56
59
  PARTICIPATION_HISTORY_FETCH_LIMIT,
60
+ VERB_LIMITS,
57
61
  __name,
62
+ agentHomeElsewhereMessage,
58
63
  buildParticipationHistorySnapshot,
59
64
  connectorRelayResponseBytes,
60
65
  evaluateParticipationPolicy,
66
+ isCanonClientVersion,
61
67
  isConnectorRelayCredential,
62
68
  isConnectorRelayInstallation,
63
69
  isMlsDevTextProtocol,
@@ -70,7 +76,7 @@ import {
70
76
  parseMlsDevGroupRoster,
71
77
  resolveAllowedUploadMimeType,
72
78
  resolveConversationPolicyScope
73
- } from "./chunk-VIS5WXNH.js";
79
+ } from "./chunk-CUPSYLBV.js";
74
80
 
75
81
  // src/runner/types.ts
76
82
  var DriverSessionError = class extends Error {
@@ -1636,6 +1642,282 @@ import { createHash as createHash4, createHmac } from "node:crypto";
1636
1642
  import { constants as constants3 } from "node:fs";
1637
1643
  import { open as open2, realpath as realpath2 } from "node:fs/promises";
1638
1644
  import { isAbsolute, resolve } from "node:path";
1645
+
1646
+ // ../agent-tools/src/communication-tool.ts
1647
+ var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
1648
+ var NON_EMPTY_ID = { type: "string", minLength: 1, maxLength: 160 };
1649
+ var ACTION_FIELDS = {
1650
+ list_conversations: { required: [], optional: ["limit"] },
1651
+ discover_agents: { required: [], optional: ["query", "limit", "cursor"] },
1652
+ message_existing: { required: ["conversationId", "text"], optional: ["messageId"] },
1653
+ start_conversation: { required: ["principalId", "text"], optional: ["selection", "messageId"] },
1654
+ create_conversation: { required: ["memberIds"], optional: ["name"] },
1655
+ manage_participants: { required: ["conversationId", "userId", "operation"], optional: [] },
1656
+ leave_conversation: { required: ["conversationId"], optional: [] },
1657
+ forward_message: { required: ["sourceConversationId", "targetConversationId", "messageId"], optional: ["text"] },
1658
+ share_contact: { required: ["conversationId", "contactUserId"], optional: ["text", "messageId"] }
1659
+ };
1660
+ var ALIASES = {
1661
+ start_direct: "start_conversation",
1662
+ create_group: "create_conversation",
1663
+ manage_group_members: "manage_participants"
1664
+ };
1665
+ var CANON_COMMUNICATE_SHAPE = Object.fromEntries(Object.entries(ACTION_FIELDS).map(([action, { required, optional }]) => [action, [...required, ...optional]]));
1666
+ var FIELD_SCHEMAS = {
1667
+ limit: { type: "integer", minimum: 1, maximum: 100 },
1668
+ query: { type: "string", maxLength: MAX_DISCOVER_AGENTS_QUERY_LENGTH },
1669
+ cursor: { type: "string", minLength: 1 },
1670
+ conversationId: NON_EMPTY_ID,
1671
+ text: { type: "string" },
1672
+ messageId: NON_EMPTY_ID,
1673
+ principalId: NON_EMPTY_ID,
1674
+ selection: {
1675
+ type: "object",
1676
+ required: ["mode"],
1677
+ additionalProperties: false,
1678
+ properties: { mode: { type: "string", enum: ["new", "latest_or_new", "specific"] }, conversationId: NON_EMPTY_ID }
1679
+ },
1680
+ memberIds: { type: "array", minItems: 1, maxItems: VERB_LIMITS.groupMembers - 1, uniqueItems: true, items: NON_EMPTY_ID },
1681
+ name: { type: "string", minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
1682
+ userId: NON_EMPTY_ID,
1683
+ operation: { type: "string", enum: ["add", "remove"] },
1684
+ sourceConversationId: NON_EMPTY_ID,
1685
+ targetConversationId: NON_EMPTY_ID,
1686
+ contactUserId: NON_EMPTY_ID
1687
+ };
1688
+ var SEND_ID = "a stable id for this message; reuse it when retrying";
1689
+ var FIELD_NOTES = {
1690
+ limit: { list_conversations: "how many conversations, 1 to 100", discover_agents: `how many agents, 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS} (default ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS})` },
1691
+ query: { discover_agents: `a name or description prefix of at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters; leave it out to list the directory` },
1692
+ cursor: { discover_agents: "the cursor a previous discover_agents call with the same query returned" },
1693
+ conversationId: {
1694
+ message_existing: "the conversation to post in",
1695
+ share_contact: "the conversation to share the contact in",
1696
+ manage_participants: "the conversation",
1697
+ leave_conversation: "the conversation to leave"
1698
+ },
1699
+ text: {
1700
+ message_existing: "the message",
1701
+ start_conversation: `the opening message, never split; at most ${MLS_DEV_TEXT_MAX_BYTES} UTF-8 bytes when encrypted. Use message_existing for longer follow-ups`,
1702
+ forward_message: "a caption",
1703
+ share_contact: "a caption"
1704
+ },
1705
+ messageId: { message_existing: SEND_ID, start_conversation: SEND_ID, share_contact: SEND_ID, forward_message: "the message to forward" },
1706
+ principalId: { start_conversation: "the person or agent to talk to" },
1707
+ selection: { start_conversation: "which direct conversation: mode new (the default), latest_or_new, or specific with its conversationId" },
1708
+ memberIds: { create_conversation: "the people and agents to add besides you" },
1709
+ name: { create_conversation: "a title" },
1710
+ userId: { manage_participants: "the person or agent to add or remove" },
1711
+ operation: { manage_participants: "add or remove" },
1712
+ sourceConversationId: { forward_message: "the conversation the message is in" },
1713
+ targetConversationId: { forward_message: "the conversation to forward it to" },
1714
+ contactUserId: { share_contact: "whose contact card to share" }
1715
+ };
1716
+ function communicateInputSchema(shape, textMaxLength = VERB_LIMITS.messageTextBytes) {
1717
+ const actions = Object.keys(shape);
1718
+ const properties = {
1719
+ action: { type: "string", enum: actions, description: "What to do. Each other field says which actions take it." }
1720
+ };
1721
+ for (const [field, schema] of Object.entries(FIELD_SCHEMAS)) {
1722
+ const uses = actions.filter((action) => shape[action].includes(field) || ACTION_FIELDS[action].required.includes(field));
1723
+ if (!uses.length) continue;
1724
+ const notes = uses.map((action) => `${action} (${ACTION_FIELDS[action].required.includes(field) ? "required" : "optional"}): ${FIELD_NOTES[field][action]}`);
1725
+ properties[field] = { ...schema, ...field === "text" && textMaxLength !== null ? { maxLength: textMaxLength } : {}, description: `${notes.join("; ")}.` };
1726
+ }
1727
+ return { type: "object", required: ["action"], additionalProperties: false, properties };
1728
+ }
1729
+ __name(communicateInputSchema, "communicateInputSchema");
1730
+ function isRecord3(value) {
1731
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1732
+ }
1733
+ __name(isRecord3, "isRecord");
1734
+ function readRequiredString(value) {
1735
+ return typeof value === "string" && value.trim() ? value : null;
1736
+ }
1737
+ __name(readRequiredString, "readRequiredString");
1738
+ var listed = /* @__PURE__ */ __name((names) => names.length < 2 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`, "listed");
1739
+ function parseCommunicateToolInput(value, shape = CANON_COMMUNICATE_SHAPE) {
1740
+ if (!isRecord3(value)) throw new Error("communicate arguments must be an object");
1741
+ const action = typeof value.action === "string" ? ALIASES[value.action] ?? value.action : "";
1742
+ if (!Object.hasOwn(shape, action)) {
1743
+ throw new Error(`communicate.action is invalid: use ${listed(Object.keys(shape))}`);
1744
+ }
1745
+ const fields = ACTION_FIELDS[action];
1746
+ const missing = fields.required.filter((field) => value[field] === void 0 || value[field] === null || typeof value[field] === "string" && !value[field].trim());
1747
+ if (missing.length) throw new Error(`${String(value.action)} needs ${listed(missing)}`);
1748
+ const taken = ["action", ...fields.required, ...fields.optional];
1749
+ const extra = Object.keys(value).filter((field) => !taken.includes(field));
1750
+ if (extra.length) throw new Error(`${String(value.action)} does not take ${listed(extra)}`);
1751
+ if (value.action === "list_conversations") {
1752
+ if (value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100)) throw new Error("list_conversations requires an optional limit from 1 to 100");
1753
+ return { action: "list_conversations", ...typeof value.limit === "number" ? { limit: value.limit } : {} };
1754
+ }
1755
+ if (value.action === "leave_conversation") {
1756
+ const conversationId = readRequiredString(value.conversationId);
1757
+ if (!conversationId) throw new Error("leave_conversation requires conversationId");
1758
+ return { action: "leave_conversation", conversationId };
1759
+ }
1760
+ if (value.action === "discover_agents") {
1761
+ if (value.query !== void 0 && typeof value.query !== "string") {
1762
+ throw new Error("communicate.query must be a string");
1763
+ }
1764
+ if (typeof value.query === "string" && value.query.length > MAX_DISCOVER_AGENTS_QUERY_LENGTH) {
1765
+ throw new Error(
1766
+ `communicate.query must be at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters`
1767
+ );
1768
+ }
1769
+ if (value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS)) {
1770
+ throw new Error(
1771
+ `communicate.limit must be an integer from 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS}`
1772
+ );
1773
+ }
1774
+ if (value.cursor !== void 0 && (typeof value.cursor !== "string" || value.cursor.length === 0)) {
1775
+ throw new Error("communicate.cursor must be a non-empty string");
1776
+ }
1777
+ return {
1778
+ action: "discover_agents",
1779
+ ...typeof value.query === "string" ? { query: value.query } : {},
1780
+ limit: typeof value.limit === "number" ? value.limit : CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
1781
+ ...typeof value.cursor === "string" ? { cursor: value.cursor } : {}
1782
+ };
1783
+ }
1784
+ if (value.action === "create_group" || value.action === "create_conversation") {
1785
+ const name = value.name === void 0 ? void 0 : readRequiredString(value.name);
1786
+ if (name === null) throw new Error("communicate.name must be a non-empty string when supplied");
1787
+ if (!Array.isArray(value.memberIds) || value.memberIds.length === 0 || !value.memberIds.every((entry) => readRequiredString(entry) !== null)) {
1788
+ throw new Error("communicate.memberIds must contain at least one principal id");
1789
+ }
1790
+ return {
1791
+ action: value.action,
1792
+ ...name ? { name } : {},
1793
+ memberIds: [...new Set(value.memberIds)]
1794
+ };
1795
+ }
1796
+ if (value.action === "forward_message") {
1797
+ const sourceConversationId = readRequiredString(value.sourceConversationId);
1798
+ const targetConversationId = readRequiredString(value.targetConversationId);
1799
+ const messageId2 = readRequiredString(value.messageId);
1800
+ if (!sourceConversationId) throw new Error("communicate.sourceConversationId is required");
1801
+ if (!targetConversationId) throw new Error("communicate.targetConversationId is required");
1802
+ if (!messageId2) throw new Error("communicate.messageId is required");
1803
+ if (value.text !== void 0 && typeof value.text !== "string") {
1804
+ throw new Error("communicate.text must be a string");
1805
+ }
1806
+ return {
1807
+ action: "forward_message",
1808
+ sourceConversationId,
1809
+ targetConversationId,
1810
+ messageId: messageId2,
1811
+ ...value.text !== void 0 ? { text: value.text } : {}
1812
+ };
1813
+ }
1814
+ if (value.action === "share_contact") {
1815
+ const conversationId = readRequiredString(value.conversationId);
1816
+ const contactUserId = readRequiredString(value.contactUserId);
1817
+ const messageId2 = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
1818
+ if (!conversationId) throw new Error("communicate.conversationId is required");
1819
+ if (!contactUserId) throw new Error("communicate.contactUserId is required");
1820
+ if (value.text !== void 0 && typeof value.text !== "string") {
1821
+ throw new Error("communicate.text must be a string");
1822
+ }
1823
+ if (value.messageId !== void 0 && !messageId2) {
1824
+ throw new Error("communicate.messageId must be a non-empty string");
1825
+ }
1826
+ return {
1827
+ action: "share_contact",
1828
+ conversationId,
1829
+ contactUserId,
1830
+ ...value.text !== void 0 ? { text: value.text } : {},
1831
+ ...messageId2 ? { messageId: messageId2 } : {}
1832
+ };
1833
+ }
1834
+ if (value.action === "manage_group_members" || value.action === "manage_participants") {
1835
+ const conversationId = readRequiredString(value.conversationId);
1836
+ const userId = readRequiredString(value.userId);
1837
+ if (!conversationId) throw new Error("communicate.conversationId is required");
1838
+ if (!userId) throw new Error("communicate.userId is required");
1839
+ if (value.operation !== "add" && value.operation !== "remove") {
1840
+ throw new Error("communicate.operation must be add or remove");
1841
+ }
1842
+ return {
1843
+ action: value.action,
1844
+ conversationId,
1845
+ userId,
1846
+ operation: value.operation
1847
+ };
1848
+ }
1849
+ const text3 = readRequiredString(value.text);
1850
+ const messageId = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
1851
+ if (!text3) throw new Error("communicate.text is required");
1852
+ if (value.messageId !== void 0 && !messageId) {
1853
+ throw new Error("communicate.messageId must be a non-empty string");
1854
+ }
1855
+ if (value.action === "message_existing") {
1856
+ const conversationId = readRequiredString(value.conversationId);
1857
+ if (!conversationId) throw new Error("communicate.conversationId is required");
1858
+ return {
1859
+ action: "message_existing",
1860
+ conversationId,
1861
+ text: text3,
1862
+ ...messageId ? { messageId } : {}
1863
+ };
1864
+ }
1865
+ if (value.action === "start_direct" || value.action === "start_conversation") {
1866
+ const principalId = readRequiredString(value.principalId);
1867
+ if (!principalId) throw new Error("communicate.principalId is required");
1868
+ if (utf8ByteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
1869
+ throw new Error(`${value.action} opening text must be at most ${MLS_DEV_TEXT_MAX_BYTES} UTF-8 bytes; use message_existing for longer follow-ups`);
1870
+ }
1871
+ const selection = value.selection === void 0 ? value.action === "start_conversation" ? { mode: "new" } : void 0 : parseDirectConversationSelection(value.selection);
1872
+ return {
1873
+ action: value.action,
1874
+ principalId,
1875
+ text: text3,
1876
+ ...selection ? { selection } : {},
1877
+ ...messageId ? { messageId } : {}
1878
+ };
1879
+ }
1880
+ throw new Error("communicate.action is invalid");
1881
+ }
1882
+ __name(parseCommunicateToolInput, "parseCommunicateToolInput");
1883
+ function renderResult(result) {
1884
+ let id;
1885
+ if (result.status === "messaged" || result.status === "forwarded" || result.status === "shared") {
1886
+ id = result.messageId;
1887
+ } else if (result.status === "added" || result.status === "removed") {
1888
+ id = result.userId;
1889
+ } else if (result.status === "created") {
1890
+ id = result.conversationId;
1891
+ } else if (result.status === "requested" || result.status === "pending") {
1892
+ id = result.requestId;
1893
+ }
1894
+ return {
1895
+ content: [{
1896
+ type: "text",
1897
+ text: `communicate: ${result.status}${id ? ` (${id})` : ""}
1898
+ ${JSON.stringify(result, null, 2)}`
1899
+ }]
1900
+ };
1901
+ }
1902
+ __name(renderResult, "renderResult");
1903
+ async function executeCanonCommunicateTool(client, args, shape = CANON_COMMUNICATE_SHAPE) {
1904
+ let input;
1905
+ try {
1906
+ input = parseCommunicateToolInput(args, shape);
1907
+ } catch (error) {
1908
+ return {
1909
+ content: [{
1910
+ type: "text",
1911
+ text: `Invalid communicate arguments: ${error.message}`
1912
+ }],
1913
+ isError: true
1914
+ };
1915
+ }
1916
+ return renderResult(await client.communicate(input));
1917
+ }
1918
+ __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
1919
+
1920
+ // src/runner/mls.ts
1639
1921
  var fail = /* @__PURE__ */ __name(() => {
1640
1922
  throw new Error("MLS_AUTHORITY_REQUIRED");
1641
1923
  }, "fail");
@@ -1712,30 +1994,22 @@ function mentionsAgent(text3, names) {
1712
1994
  });
1713
1995
  }
1714
1996
  __name(mentionsAgent, "mentionsAgent");
1997
+ var ENCRYPTED_COMMUNICATE_SHAPE = {
1998
+ list_conversations: ["limit"],
1999
+ discover_agents: ["limit"],
2000
+ message_existing: ["conversationId", "text"]
2001
+ };
1715
2002
  var ENCRYPTED_COMMUNICATE = {
1716
2003
  name: "communicate",
1717
2004
  description: "List Canon conversations or agents, or send text to the current encrypted conversation. Other Canon outputs are not yet supported.",
1718
- inputSchema: { type: "object", oneOf: [
1719
- {
1720
- type: "object",
1721
- required: ["action"],
1722
- additionalProperties: false,
1723
- properties: { action: { const: "list_conversations" }, limit: { type: "integer", minimum: 1, maximum: 100 } }
1724
- },
1725
- {
1726
- type: "object",
1727
- required: ["action"],
1728
- additionalProperties: false,
1729
- properties: { action: { const: "discover_agents" }, limit: { type: "integer", minimum: 1, maximum: 10 } }
1730
- },
1731
- {
1732
- type: "object",
1733
- required: ["action", "conversationId", "text"],
1734
- additionalProperties: false,
1735
- properties: { action: { const: "message_existing" }, conversationId: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 } }
1736
- }
1737
- ] }
2005
+ inputSchema: communicateInputSchema(ENCRYPTED_COMMUNICATE_SHAPE, null)
1738
2006
  };
2007
+ var errandShape = /* @__PURE__ */ __name((contact) => ({ ...ENCRYPTED_COMMUNICATE_SHAPE, ...contact ? { start_conversation: ["principalId", "text"] } : {} }), "errandShape");
2008
+ var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
2009
+ name: "communicate",
2010
+ description: "List Canon conversations or agents, or send text to your errand's recipient or to the conversation it came from" + (contact ? ". Start the conversation with the recipient first, with start_conversation." : "."),
2011
+ inputSchema: communicateInputSchema(errandShape(contact), null)
2012
+ }), "errandCommunicate");
1739
2013
  var MAX_CONFIGURATION_BYTES = 64 * 1024;
1740
2014
  function invalid() {
1741
2015
  throw new Error("MLS_DEV_CONFIGURATION_INVALID");
@@ -2412,232 +2686,16 @@ function relayRunnerHome(home, installationId) {
2412
2686
  }
2413
2687
  __name(relayRunnerHome, "relayRunnerHome");
2414
2688
 
2689
+ // src/version.ts
2690
+ var AGENT_SDK_VERSION = true ? "13.3.1" : void 0;
2691
+ var RUNNER_CLIENT = isCanonClientVersion(AGENT_SDK_VERSION) ? Object.freeze({ kind: "runner", version: AGENT_SDK_VERSION }) : void 0;
2692
+
2415
2693
  // src/runner/runner.ts
2416
2694
  import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
2417
2695
  import { readFileSync as readFileSync7, rmSync as rmSync2 } from "node:fs";
2418
2696
  import { realpath as realpath4, stat as stat2 } from "node:fs/promises";
2419
2697
  import { basename as basename4, isAbsolute as isAbsolute3, join as join8, sep } from "node:path";
2420
2698
 
2421
- // ../agent-tools/src/communication-tool.ts
2422
- var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
2423
- function isRecord3(value) {
2424
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
2425
- }
2426
- __name(isRecord3, "isRecord");
2427
- function readRequiredString(value) {
2428
- return typeof value === "string" && value.trim() ? value : null;
2429
- }
2430
- __name(readRequiredString, "readRequiredString");
2431
- function hasOnlyKeys(record, allowed) {
2432
- return Object.keys(record).every((key) => allowed.has(key));
2433
- }
2434
- __name(hasOnlyKeys, "hasOnlyKeys");
2435
- function parseCommunicateToolInput(value) {
2436
- if (!isRecord3(value)) throw new Error("communicate arguments must be an object");
2437
- if (value.action === "list_conversations") {
2438
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "limit"])) || value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100)) throw new Error("list_conversations requires an optional limit from 1 to 100");
2439
- return { action: "list_conversations", ...typeof value.limit === "number" ? { limit: value.limit } : {} };
2440
- }
2441
- if (value.action === "leave_conversation") {
2442
- const conversationId = readRequiredString(value.conversationId);
2443
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "conversationId"])) || !conversationId) throw new Error("leave_conversation requires conversationId");
2444
- return { action: "leave_conversation", conversationId };
2445
- }
2446
- if (value.action === "discover_agents") {
2447
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "query", "limit", "cursor"]))) {
2448
- throw new Error("discover_agents contains unsupported fields");
2449
- }
2450
- if (value.query !== void 0 && typeof value.query !== "string") {
2451
- throw new Error("communicate.query must be a string");
2452
- }
2453
- if (typeof value.query === "string" && value.query.length > MAX_DISCOVER_AGENTS_QUERY_LENGTH) {
2454
- throw new Error(
2455
- `communicate.query must be at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters`
2456
- );
2457
- }
2458
- if (value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS)) {
2459
- throw new Error(
2460
- `communicate.limit must be an integer from 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS}`
2461
- );
2462
- }
2463
- if (value.cursor !== void 0 && (typeof value.cursor !== "string" || value.cursor.length === 0)) {
2464
- throw new Error("communicate.cursor must be a non-empty string");
2465
- }
2466
- return {
2467
- action: "discover_agents",
2468
- ...typeof value.query === "string" ? { query: value.query } : {},
2469
- limit: typeof value.limit === "number" ? value.limit : CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
2470
- ...typeof value.cursor === "string" ? { cursor: value.cursor } : {}
2471
- };
2472
- }
2473
- if (value.action === "create_group" || value.action === "create_conversation") {
2474
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "name", "memberIds"]))) {
2475
- throw new Error("create_group contains unsupported fields");
2476
- }
2477
- const name = value.name === void 0 ? void 0 : readRequiredString(value.name);
2478
- if (name === null) throw new Error("communicate.name must be a non-empty string when supplied");
2479
- if (!Array.isArray(value.memberIds) || value.memberIds.length === 0 || !value.memberIds.every((entry) => readRequiredString(entry) !== null)) {
2480
- throw new Error("communicate.memberIds must contain at least one principal id");
2481
- }
2482
- return {
2483
- action: value.action,
2484
- ...name ? { name } : {},
2485
- memberIds: [...new Set(value.memberIds)]
2486
- };
2487
- }
2488
- if (value.action === "forward_message") {
2489
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set([
2490
- "action",
2491
- "sourceConversationId",
2492
- "targetConversationId",
2493
- "messageId",
2494
- "text"
2495
- ]))) {
2496
- throw new Error("forward_message contains unsupported fields");
2497
- }
2498
- const sourceConversationId = readRequiredString(value.sourceConversationId);
2499
- const targetConversationId = readRequiredString(value.targetConversationId);
2500
- const messageId2 = readRequiredString(value.messageId);
2501
- if (!sourceConversationId) throw new Error("communicate.sourceConversationId is required");
2502
- if (!targetConversationId) throw new Error("communicate.targetConversationId is required");
2503
- if (!messageId2) throw new Error("communicate.messageId is required");
2504
- if (value.text !== void 0 && typeof value.text !== "string") {
2505
- throw new Error("communicate.text must be a string");
2506
- }
2507
- return {
2508
- action: "forward_message",
2509
- sourceConversationId,
2510
- targetConversationId,
2511
- messageId: messageId2,
2512
- ...value.text !== void 0 ? { text: value.text } : {}
2513
- };
2514
- }
2515
- if (value.action === "share_contact") {
2516
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set([
2517
- "action",
2518
- "conversationId",
2519
- "contactUserId",
2520
- "text",
2521
- "messageId"
2522
- ]))) {
2523
- throw new Error("share_contact contains unsupported fields");
2524
- }
2525
- const conversationId = readRequiredString(value.conversationId);
2526
- const contactUserId = readRequiredString(value.contactUserId);
2527
- const messageId2 = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
2528
- if (!conversationId) throw new Error("communicate.conversationId is required");
2529
- if (!contactUserId) throw new Error("communicate.contactUserId is required");
2530
- if (value.text !== void 0 && typeof value.text !== "string") {
2531
- throw new Error("communicate.text must be a string");
2532
- }
2533
- if (value.messageId !== void 0 && !messageId2) {
2534
- throw new Error("communicate.messageId must be a non-empty string");
2535
- }
2536
- return {
2537
- action: "share_contact",
2538
- conversationId,
2539
- contactUserId,
2540
- ...value.text !== void 0 ? { text: value.text } : {},
2541
- ...messageId2 ? { messageId: messageId2 } : {}
2542
- };
2543
- }
2544
- if (value.action === "manage_group_members" || value.action === "manage_participants") {
2545
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set([
2546
- "action",
2547
- "conversationId",
2548
- "userId",
2549
- "operation"
2550
- ]))) {
2551
- throw new Error("manage_group_members contains unsupported fields");
2552
- }
2553
- const conversationId = readRequiredString(value.conversationId);
2554
- const userId = readRequiredString(value.userId);
2555
- if (!conversationId) throw new Error("communicate.conversationId is required");
2556
- if (!userId) throw new Error("communicate.userId is required");
2557
- if (value.operation !== "add" && value.operation !== "remove") {
2558
- throw new Error("communicate.operation must be add or remove");
2559
- }
2560
- return {
2561
- action: value.action,
2562
- conversationId,
2563
- userId,
2564
- operation: value.operation
2565
- };
2566
- }
2567
- const text3 = readRequiredString(value.text);
2568
- const messageId = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
2569
- if (!text3) throw new Error("communicate.text is required");
2570
- if (value.messageId !== void 0 && !messageId) {
2571
- throw new Error("communicate.messageId must be a non-empty string");
2572
- }
2573
- if (value.action === "message_existing") {
2574
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "conversationId", "text", "messageId"]))) {
2575
- throw new Error("message_existing contains unsupported fields");
2576
- }
2577
- const conversationId = readRequiredString(value.conversationId);
2578
- if (!conversationId) throw new Error("communicate.conversationId is required");
2579
- return {
2580
- action: "message_existing",
2581
- conversationId,
2582
- text: text3,
2583
- ...messageId ? { messageId } : {}
2584
- };
2585
- }
2586
- if (value.action === "start_direct" || value.action === "start_conversation") {
2587
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "principalId", "text", "selection", "messageId"]))) {
2588
- throw new Error("start_direct contains unsupported fields");
2589
- }
2590
- const principalId = readRequiredString(value.principalId);
2591
- if (!principalId) throw new Error("communicate.principalId is required");
2592
- const selection = value.selection === void 0 ? value.action === "start_conversation" ? { mode: "new" } : void 0 : parseDirectConversationSelection(value.selection);
2593
- return {
2594
- action: value.action,
2595
- principalId,
2596
- text: text3,
2597
- ...selection ? { selection } : {},
2598
- ...messageId ? { messageId } : {}
2599
- };
2600
- }
2601
- throw new Error("communicate.action is invalid");
2602
- }
2603
- __name(parseCommunicateToolInput, "parseCommunicateToolInput");
2604
- function renderResult(result) {
2605
- let id;
2606
- if (result.status === "messaged" || result.status === "forwarded" || result.status === "shared") {
2607
- id = result.messageId;
2608
- } else if (result.status === "added" || result.status === "removed") {
2609
- id = result.userId;
2610
- } else if (result.status === "created") {
2611
- id = result.conversationId;
2612
- } else if (result.status === "requested" || result.status === "pending") {
2613
- id = result.requestId;
2614
- }
2615
- return {
2616
- content: [{
2617
- type: "text",
2618
- text: `communicate: ${result.status}${id ? ` (${id})` : ""}
2619
- ${JSON.stringify(result, null, 2)}`
2620
- }]
2621
- };
2622
- }
2623
- __name(renderResult, "renderResult");
2624
- async function executeCanonCommunicateTool(client, args) {
2625
- let input;
2626
- try {
2627
- input = parseCommunicateToolInput(args);
2628
- } catch (error) {
2629
- return {
2630
- content: [{
2631
- type: "text",
2632
- text: `Invalid communicate arguments: ${error.message}`
2633
- }],
2634
- isError: true
2635
- };
2636
- }
2637
- return renderResult(await client.communicate(input));
2638
- }
2639
- __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
2640
-
2641
2699
  // src/runner/finals.ts
2642
2700
  import { createHash as createHash7 } from "node:crypto";
2643
2701
  import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmdirSync, unlinkSync as unlinkSync2 } from "node:fs";
@@ -3326,6 +3384,7 @@ function answerText(answer) {
3326
3384
  return answer.text?.trim() ? answer.text : "They answered with an empty message.";
3327
3385
  }
3328
3386
  __name(answerText, "answerText");
3387
+ var AGENT_KEY_REFUSED = "AGENT_KEY_REFUSED";
3329
3388
  var sha2564 = /* @__PURE__ */ __name((value) => createHash9("sha256").update(value).digest("hex"), "sha256");
3330
3389
  var toolText = /* @__PURE__ */ __name((text3, isError = false) => ({ content: [{ type: "text", text: text3 }], ...isError ? { isError } : {} }), "toolText");
3331
3390
  var turnEnded2 = /* @__PURE__ */ __name(() => new DOMException("This turn has ended", "AbortError"), "turnEnded");
@@ -3371,7 +3430,21 @@ var STYLE_RULES = {
3371
3430
  observer: "Stay quiet unless a reply is necessary, and end your turn with no_reply."
3372
3431
  };
3373
3432
  var factText = /* @__PURE__ */ __name((value, limit = MAX_FACT_NAME) => speaker(value).slice(0, limit).trimEnd(), "factText");
3374
- function factsPrompt({ name, topic, owner, members, rules }, kind) {
3433
+ var mentionKey = /* @__PURE__ */ __name((value) => value.normalize("NFC").trim().replace(/^@+/, "").replace(/\s+/g, " ").toLowerCase(), "mentionKey");
3434
+ var mentionsToo = /* @__PURE__ */ __name((name, other) => other.length > 0 && name.startsWith(other) && !/^[\p{L}\p{N}\p{M}_]/u.test(name.slice(other.length)), "mentionsToo");
3435
+ function mentionOf(peer, agents) {
3436
+ const name = peer.name?.trim();
3437
+ const key = name ? mentionKey(name) : "";
3438
+ if (name && key && !name.startsWith("@") && factText(name) === name && !agents.some((other) => other !== peer && !other.isYou && [other.name, other.id].some((word) => word !== void 0 && mentionsToo(key, mentionKey(word))))) return name;
3439
+ return ENDPOINT_ID_PATTERN.test(peer.id) ? peer.id : void 0;
3440
+ }
3441
+ __name(mentionOf, "mentionOf");
3442
+ function factsPrompt({ name, topic, owner, members, rules }, kind, self) {
3443
+ const agents = members.filter((member) => member.type === "ai_agent");
3444
+ const reach = rules && !rules.allowAgentToAgent ? [] : members.slice(0, MAX_FACT_MEMBERS).filter((member) => member.type === "ai_agent" && !member.isYou).flatMap((peer) => {
3445
+ const mention = mentionOf(peer, agents), seen = factText(peer.name ?? peer.id);
3446
+ return !mention ? [] : [mention === seen ? `@${mention}` : `@${mention} (the id of ${seen})`];
3447
+ });
3375
3448
  const shown = members.slice(0, MAX_FACT_MEMBERS).map((member) => member.isYou ? "you" : factText(member.name ?? member.id) + (member.isOwner ? OWNER_MARK : member.type === "ai_agent" ? " [agent]" : ""));
3376
3449
  const lines = !rules ? [] : [
3377
3450
  STYLE_RULES[rules.style],
@@ -3382,10 +3455,12 @@ function factsPrompt({ name, topic, owner, members, rules }, kind) {
3382
3455
  ].filter((line) => line !== void 0);
3383
3456
  return [
3384
3457
  FACTS_OPEN,
3458
+ ...self ? [`Your name: ${factText(self)}`] : [],
3385
3459
  `Your owner: ${factText(owner.name ?? owner.id)}`,
3386
3460
  ...name ? [`Conversation: ${factText(name)}`] : [],
3387
3461
  ...topic ? [`Topic: ${factText(topic, MAX_FACT_TOPIC)}`] : [],
3388
3462
  `Members: ${shown.join(", ")}${members.length > shown.length ? `, and ${members.length - shown.length} more` : ""}`,
3463
+ ...reach.length ? [`${reach.length > 1 ? "Other agents here answer only a message that mentions them" : "Another agent here answers only a message that mentions it"}: ${reach.join(", ")}.`] : [],
3389
3464
  ...lines.length ? [
3390
3465
  "This conversation's rules, which your owner or any of its admins can set. They say how you take part here, not what you may do:",
3391
3466
  ...lines.map((line) => `- ${line}`)
@@ -3394,16 +3469,6 @@ function factsPrompt({ name, topic, owner, members, rules }, kind) {
3394
3469
  ];
3395
3470
  }
3396
3471
  __name(factsPrompt, "factsPrompt");
3397
- var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
3398
- name: "communicate",
3399
- description: "List Canon conversations or agents, or send text to your errand's recipient or to the conversation it came from" + (contact ? ". Start the conversation with the recipient first, with start_conversation." : "."),
3400
- inputSchema: { type: "object", oneOf: [...ENCRYPTED_COMMUNICATE.inputSchema.oneOf, ...contact ? [{
3401
- type: "object",
3402
- required: ["action", "principalId", "text"],
3403
- additionalProperties: false,
3404
- properties: { action: { const: "start_conversation" }, principalId: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 } }
3405
- }] : []] }
3406
- }), "errandCommunicate");
3407
3472
  var promptLine = /* @__PURE__ */ __name((message2, name, media, mark = "") => [
3408
3473
  `${speaker(name)}${mark}: ${defused(renderCanonHostInboundContent(message2, media?.files))}`,
3409
3474
  ...(media?.failed ?? []).map((label) => `[Attachment "${defused(inert(label))}" could not be downloaded]`)
@@ -3507,7 +3572,10 @@ async function createRunner(options) {
3507
3572
  await assertPrivateHome(home);
3508
3573
  const mls = options.mls === false ? void 0 : options.mls ?? (options.endpoint ? void 0 : defaultMlsDevice(home, options.environmentId));
3509
3574
  const log = options.log ?? ((line) => console.error(`[canon-runner] ${line}`));
3510
- const warn = /* @__PURE__ */ __name((what) => (error) => log(`${what}: ${error instanceof Error ? error.message : String(error)}`), "warn");
3575
+ const warn = /* @__PURE__ */ __name((what) => (error) => {
3576
+ if (mayEnd(error)) void recheck();
3577
+ log(`${what}: ${error instanceof Error ? error.message : String(error)}`);
3578
+ }, "warn");
3511
3579
  const abortGraceMs = options.abortGraceMs ?? 1e4;
3512
3580
  const noProgressMs = options.noProgressMs ?? 30 * 6e4;
3513
3581
  const askExpiryMs = options.askExpiryMs ?? 60 * 6e4;
@@ -3553,6 +3621,7 @@ async function createRunner(options) {
3553
3621
  }, "keyed");
3554
3622
  let outboundClosed = false;
3555
3623
  let ownershipVersion;
3624
+ let installationId;
3556
3625
  let displayName;
3557
3626
  let ownerOf;
3558
3627
  const refused = /* @__PURE__ */ new Set();
@@ -3686,11 +3755,52 @@ async function createRunner(options) {
3686
3755
  log(why === "plaintext" ? `Refusing room ${roomId}: it is not encrypted, and this runner takes part only in encrypted rooms. Its messages are not delivered to this agent.` : `Refusing encrypted room ${roomId}: ${why === "no-device" ? "this runner has no MLS device (run it with --mls-dev-config)" : why === "remote" ? `the ${driver.name} driver runs off this host, which an encrypted room allows only in relay mode` : "its encryption protocol is not supported"}. Its messages are not delivered to this agent.`);
3687
3756
  }
3688
3757
  __name(refuse, "refuse");
3758
+ let homeSince = 0;
3759
+ let refusedBy;
3760
+ async function readAgent() {
3761
+ const refuse2 = /* @__PURE__ */ __name((error) => {
3762
+ refusedBy = error;
3763
+ options.onFatal?.(error);
3764
+ return error;
3765
+ }, "refuse");
3766
+ let me;
3767
+ try {
3768
+ me = await client.getAgentMe();
3769
+ } catch (error) {
3770
+ if (options.relay || !(error instanceof CanonApiError && error.status === 401)) throw error;
3771
+ throw refuse2(Object.assign(
3772
+ new Error(`Canon no longer accepts ${displayName ?? "this agent"}'s key: a new key replaced it, or the agent was deleted.`),
3773
+ { code: AGENT_KEY_REFUSED, cause: error }
3774
+ ));
3775
+ }
3776
+ if (!options.relay && me.home && (me.home.kind === "connector" || me.home.installationId !== void 0 && me.home.installationId !== installationId)) {
3777
+ throw refuse2(Object.assign(new Error(agentHomeElsewhereMessage(me.displayName, me.home)), { code: AGENT_HOME_ELSEWHERE }));
3778
+ }
3779
+ refusedBy = void 0;
3780
+ return me;
3781
+ }
3782
+ __name(readAgent, "readAgent");
3783
+ function mayEnd(error) {
3784
+ return (error instanceof EndpointError || error instanceof CanonApiError) && (error.code === AGENT_HOME_ELSEWHERE || error.status === 401 && error.code !== "CONNECTOR_ACCESS_TOKEN_EXPIRED");
3785
+ }
3786
+ __name(mayEnd, "mayEnd");
3787
+ let rechecking;
3788
+ function recheck() {
3789
+ if (options.relay || !client) return Promise.resolve();
3790
+ rechecking ??= readAgent().then(() => void 0, (error) => {
3791
+ if (error !== refusedBy) log(`home check: ${error instanceof Error ? error.message : String(error)}`);
3792
+ }).finally(() => {
3793
+ rechecking = void 0;
3794
+ });
3795
+ return within(rechecking, 1e4).then(() => void 0);
3796
+ }
3797
+ __name(recheck, "recheck");
3689
3798
  async function authorize() {
3690
3799
  await endpoint.mlsDevDevice.ensureReady();
3691
- const me = await client.getAgentMe();
3800
+ const me = await readAgent();
3692
3801
  requireMlsSupport({ environmentId: options.environmentId });
3693
3802
  if (me.agentId !== agentId || me.ownershipVersion !== ownershipVersion) throw new Error("MLS_AUTHORITY_REQUIRED");
3803
+ homeSince = !options.relay && me.home?.kind === "runner" ? me.home.since ?? 0 : 0;
3694
3804
  outboundClosed = me.outboundPolicy === "closed";
3695
3805
  if (outboundClosed && options.relay) throw new Error("MLS_AUTHORITY_REQUIRED");
3696
3806
  displayName = me.displayName;
@@ -3733,6 +3843,50 @@ ${error instanceof Error ? error.message : String(error)}`;
3733
3843
  turn.timer.unref?.();
3734
3844
  }
3735
3845
  __name(progress, "progress");
3846
+ const waitingRooms = /* @__PURE__ */ new Map(), noting = /* @__PURE__ */ new Map();
3847
+ let ownerAsk;
3848
+ function noteAdmission(roomId, known) {
3849
+ const next = (noting.get(roomId) ?? Promise.resolve()).then(() => lookAtAdmission(roomId, known));
3850
+ noting.set(roomId, next.catch(() => void 0));
3851
+ return next;
3852
+ }
3853
+ __name(noteAdmission, "noteAdmission");
3854
+ async function lookAtAdmission(roomId, known) {
3855
+ if (!mlsOn || stopping) return;
3856
+ const inGroup = /* @__PURE__ */ __name(async () => {
3857
+ const view = await endpoint.mlsDevGroups.read(roomId);
3858
+ return !!view && view.status !== "removed";
3859
+ }, "inGroup");
3860
+ const read = /* @__PURE__ */ __name(async () => await endpoint.getConversation(roomId).catch(() => null) ?? void 0, "read");
3861
+ let joined = await inGroup(), room2 = known;
3862
+ if (!joined && known && !waitingRooms.has(roomId)) {
3863
+ room2 = await read() ?? room2;
3864
+ joined = await inGroup();
3865
+ }
3866
+ if (joined) {
3867
+ if (waitingRooms.has(roomId)) log(`Joined ${waitingRooms.get(roomId)}.`);
3868
+ waitingRooms.delete(roomId);
3869
+ return;
3870
+ }
3871
+ if (waitingRooms.has(roomId) || (await endpoint.mlsDevGroups.readAdmissionHold(roomId)).outside !== true) return;
3872
+ room2 ??= await read();
3873
+ const members = Array.isArray(room2?.memberIds) ? room2.memberIds : [];
3874
+ if (!members.includes(agentId)) return;
3875
+ const label = typeof room2?.name === "string" && room2.name.trim() ? `"${room2.name.trim()}"` : `room ${roomId}`;
3876
+ waitingRooms.set(roomId, label);
3877
+ ownerAsk ??= client.getAgentMe().then((me) => ({ id: me.ownerId, ...me.ownerName ? { name: me.ownerName } : {} }));
3878
+ const owner2 = await ownerAsk.catch(() => void 0);
3879
+ const ask2 = owner2 && members.includes(owner2.id) ? `Ask ${owner2.name ?? "its owner"} to open Canon.` : "Ask one of its members to open Canon.";
3880
+ log(`Waiting to join ${label}: only a device already in it can add this agent's device. ${ask2}`);
3881
+ }
3882
+ __name(lookAtAdmission, "lookAtAdmission");
3883
+ async function noteAdmissions() {
3884
+ const { conversations = [] } = await endpoint.query("list_conversations", { limit: 100 });
3885
+ for (const room2 of conversations) {
3886
+ if (typeof room2.id === "string" && ["mls", "mlsRequired", "mlsProtocol"].some((key) => Object.hasOwn(room2, key))) await noteAdmission(room2.id, room2);
3887
+ }
3888
+ }
3889
+ __name(noteAdmissions, "noteAdmissions");
3736
3890
  async function offer(event) {
3737
3891
  offering += 1;
3738
3892
  touch();
@@ -3783,6 +3937,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3783
3937
  await authorize();
3784
3938
  const binding = await groups.readVerifiedMessageBinding({ conversationId, messageId: payload.message.id });
3785
3939
  if (!binding) throw new Error("MLS_INPUT_NOT_VERIFIED");
3940
+ if (binding.acceptedAtMs < homeSince) return void await handle.settle("mls-before-home");
3786
3941
  const { messages } = await endpoint.query("get_messages", { conversationId, messageIds: [binding.messageId] });
3787
3942
  const [message2] = messages;
3788
3943
  if (messages.length !== 1 || message2.id !== binding.messageId || message2.senderId !== binding.senderId || message2.senderType !== binding.senderPrincipal.type || !Number.isSafeInteger(message2.mlsFeedOrdinal) || typeof message2.text !== "string" && !(message2.text === null && (message2.attachments?.length ?? 0) === 1)) throw new Error("MLS_INPUT_NOT_VERIFIED");
@@ -4166,6 +4321,8 @@ ${decision.reasonCode}`;
4166
4321
  function bindTools(current, turn) {
4167
4322
  const questions = /* @__PURE__ */ new Map();
4168
4323
  let asked = 0;
4324
+ const errandContact = Boolean(turn.errand && "contact" in turn.errand.target);
4325
+ const shape = turn.errand ? errandShape(errandContact) : ENCRYPTED_COMMUNICATE_SHAPE;
4169
4326
  const handlers = {
4170
4327
  no_reply: /* @__PURE__ */ __name(async ({ reason }) => {
4171
4328
  turn.silenced = typeof reason === "string" && reason.trim() ? { reason } : {};
@@ -4186,7 +4343,7 @@ ${decision.reasonCode}`;
4186
4343
  turn.outbox.push(file.path);
4187
4344
  return { result: toolText(`${basename4(file.path)} will be sent with your reply.`) };
4188
4345
  }, "send_file"),
4189
- communicate: /* @__PURE__ */ __name(async (args) => ({ result: await communicate(current, turn, args) }), "communicate"),
4346
+ communicate: /* @__PURE__ */ __name(async (args) => ({ result: await communicate(current, turn, args, shape) }), "communicate"),
4190
4347
  // RUN-25 through a tool: the same `ask`, so the same responder, card, expiry, waiting_input and paused deadline;
4191
4348
  // the turn's end or an abort withdraws it, and the call then fails.
4192
4349
  ask_question: /* @__PURE__ */ __name(async (args) => {
@@ -4213,7 +4370,7 @@ ${decision.reasonCode}`;
4213
4370
  // §12.6: an errand turn reaches its recipient and its origin, and asks only in an origin its report goes to.
4214
4371
  definitions: errand ? [
4215
4372
  TOOLS.no_reply,
4216
- errandCommunicate("contact" in errand.target),
4373
+ errandCommunicate(errandContact),
4217
4374
  ...errand.reports && cards && offersQuestions ? [TOOLS.ask_question] : []
4218
4375
  ] : [
4219
4376
  TOOLS.no_reply,
@@ -4250,12 +4407,12 @@ ${decision.reasonCode}`;
4250
4407
  }, "call") } };
4251
4408
  }
4252
4409
  __name(bindTools, "bindTools");
4253
- async function communicate(current, turn, args) {
4410
+ async function communicate(current, turn, args, shape) {
4254
4411
  const errand = turn.errand;
4255
4412
  const origins = errand ? errand.origin ? [errand.origin.room] : [] : turn.contexts.flatMap((record) => record.origin ? [record.origin] : []);
4256
4413
  let command;
4257
4414
  try {
4258
- command = parseCommunicateToolInput(args);
4415
+ command = parseCommunicateToolInput(args, shape);
4259
4416
  } catch (error) {
4260
4417
  return toolText(`Invalid communicate arguments: ${error instanceof Error ? error.message : String(error)}`, true);
4261
4418
  }
@@ -4462,7 +4619,9 @@ ${JSON.stringify(result, null, 2)}`);
4462
4619
  async function stopOutput(record) {
4463
4620
  let queued2 = 0;
4464
4621
  for (const [index, { messageId }] of encryptedMessages(record.mls, record.text, record.options, record).entries()) {
4465
- const status = (await endpoint.getOperation(encryptedOperation(record.mls, messageId)))?.status;
4622
+ let operation = await endpoint.getOperation(encryptedOperation(record.mls, messageId));
4623
+ while (operation?.resentAs) operation = await endpoint.getOperation(operation.resentAs);
4624
+ const status = operation?.status;
4466
4625
  if (status && status !== "succeeded" && status !== "rejected") queued2 = index + 1;
4467
4626
  }
4468
4627
  const { items: _items, errand: _errand, ...rest } = record;
@@ -4470,13 +4629,14 @@ ${JSON.stringify(result, null, 2)}`);
4470
4629
  else await endpoint.finishCompletion(record.turnId);
4471
4630
  }
4472
4631
  __name(stopOutput, "stopOutput");
4632
+ const publicationGuard = /* @__PURE__ */ __name((audience) => ({ automaticResend }) => (automaticResend ? authorize() : assertAudience(audience)).then(() => true, (error) => {
4633
+ if (audienceChanged(error)) throw error;
4634
+ return false;
4635
+ }), "publicationGuard");
4473
4636
  async function sendEncrypted(audience, messageId, text3, signal, replyAuthority) {
4474
4637
  const operationId = encryptedOperation(audience, messageId);
4475
4638
  if (!guards.has(operationId)) {
4476
- guards.set(operationId, endpoint.registerDeliveryGuard(operationId, () => assertAudience(audience).then(() => true, (error) => {
4477
- if (audienceChanged(error)) throw error;
4478
- return false;
4479
- })));
4639
+ guards.set(operationId, endpoint.registerDeliveryGuard(operationId, publicationGuard(audience)));
4480
4640
  }
4481
4641
  if (!await endpoint.getOperation(operationId)) await assertAudience(audience);
4482
4642
  signal?.throwIfAborted();
@@ -4516,7 +4676,7 @@ ${JSON.stringify(result, null, 2)}`);
4516
4676
  async function sendEncryptedItems(audience, items, signal) {
4517
4677
  if (!items.length) return;
4518
4678
  const media = await client.getFileMediaPipeline(options.fetch);
4519
- const sendOptions = { signal, beforePublish: /* @__PURE__ */ __name(() => assertAudience(audience).then(() => true), "beforePublish") };
4679
+ const sendOptions = { signal, beforePublish: publicationGuard(audience) };
4520
4680
  for (const item of items) {
4521
4681
  signal?.throwIfAborted();
4522
4682
  try {
@@ -4599,7 +4759,7 @@ ${JSON.stringify(result, null, 2)}`);
4599
4759
  session: turn.session,
4600
4760
  // §12.6: an errand turn's prompt is its errand; the owner's message is only its input's record.
4601
4761
  prompt: turn.errand ? errandPrompt(turn.errand) : [
4602
- ...turn.facts ? [...factsPrompt(turn.facts, kind), ""] : [],
4762
+ ...turn.facts ? [...factsPrompt(turn.facts, kind, displayName), ""] : [],
4603
4763
  ...turn.notes.flatMap((note) => [NOTE_OPEN, defused(note.text), NOTE_CLOSE, ""]),
4604
4764
  ...turn.contexts.flatMap((record) => [...contextPrompt(record), ""]),
4605
4765
  ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.facts || turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
@@ -5073,7 +5233,7 @@ ${JSON.stringify(result, null, 2)}`);
5073
5233
  ...record.origin ? { origin: record.origin } : {}
5074
5234
  }), "errandFacts");
5075
5235
  function errandPrompt({ record, target, origin, reports }) {
5076
- const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation.` : `the encrypted conversation ${target.room}. Send to it with communicate message_existing.`;
5236
+ const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation. If they are an agent, address them with @${target.contact} (their exact agent id): an agent's message reaches another only if it mentions it.` : `the encrypted conversation ${target.room}. Send to it with communicate message_existing. An agent's message reaches another agent there only if it mentions it, as @ and its exact agent id.`;
5077
5237
  const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` : "your final answer is your report to your owner, and it stays on their machine.";
5078
5238
  return [
5079
5239
  `${ERRAND_OPEN} Your owner sends you on an errand, from the machine you run on. It is not a message in any conversation, and no one else sees it.`,
@@ -5283,7 +5443,10 @@ ${JSON.stringify(result, null, 2)}`);
5283
5443
  const known = rooms.get(data.conversationId);
5284
5444
  if (known && Array.isArray(merged.memberIds) && !merged.memberIds.includes(agentId)) loseMembership(known);
5285
5445
  }
5286
- if (mlsOn && (encrypted || Array.isArray(changes?.memberIds))) await checkAudience(rooms.get(data.conversationId));
5446
+ if (mlsOn && (encrypted || Array.isArray(changes?.memberIds))) {
5447
+ await checkAudience(rooms.get(data.conversationId));
5448
+ await noteAdmission(data.conversationId).catch(warn("room admission"));
5449
+ }
5287
5450
  if (encrypted) mlsAsks?.wake(data.conversationId);
5288
5451
  }
5289
5452
  __name(onRoomChanged, "onRoomChanged");
@@ -5393,15 +5556,19 @@ ${JSON.stringify(result, null, 2)}`);
5393
5556
  environmentId: connection.environmentId,
5394
5557
  streamUrl: connection.streamUrl,
5395
5558
  stateDirectory: join8(home, "endpoint"),
5396
- ...options.client ? { client: options.client } : {},
5559
+ ...options.client ?? RUNNER_CLIENT ? { client: options.client ?? RUNNER_CLIENT } : {},
5397
5560
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
5398
5561
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
5562
+ // The runner is the agent's home; Canon's relay is the connector home under its own installation.
5563
+ claimHome: true,
5564
+ ...options.takeOver ? { takeOver: true } : {},
5399
5565
  ...options.installationId ? { installationId: options.installationId } : {},
5400
5566
  ...mls ? { mlsDev: { configuration: mls.configuration, custody: mls.custody } } : {},
5401
5567
  ...options.endpoint ? { endpoint: options.endpoint } : {},
5402
5568
  ...options.fetch ? { fetch: options.fetch } : {}
5403
5569
  });
5404
5570
  const me = await client.getAgentMe();
5571
+ displayName = me.displayName;
5405
5572
  lock = acquireLock(runnerLockName(connection.environmentId, me.agentId), home);
5406
5573
  endpoint = await client.getEndpoint();
5407
5574
  const identity = await endpoint.ready();
@@ -5410,6 +5577,7 @@ ${JSON.stringify(result, null, 2)}`);
5410
5577
  }
5411
5578
  agentId = identity.principalId;
5412
5579
  ownershipVersion = identity.ownershipVersion;
5580
+ installationId = identity.installationId;
5413
5581
  if (options.wake !== void 0) await client.configureWake(options.wake);
5414
5582
  mlsOn = !!(endpoint.mlsDevDevice && endpoint.mlsDevGroups);
5415
5583
  if (!mlsOn && (await endpoint.capabilities().catch(() => null))?.mls?.roomMode === "default") {
@@ -5479,15 +5647,31 @@ ${JSON.stringify(result, null, 2)}`);
5479
5647
  roots = outbound ? (await Promise.all(outbound.roots.map((root) => realpath4(root).catch(() => "")))).filter(Boolean) : [];
5480
5648
  if (driver.capabilities.tools) tools = await startToolServer();
5481
5649
  await recover();
5482
- const publisher = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
5650
+ const runtimeState = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
5651
+ const publisher = {
5652
+ publishAgentRuntime: /* @__PURE__ */ __name((runtime) => refusedBy ? Promise.resolve() : runtimeState.publishAgentRuntime(runtime), "publishAgentRuntime"),
5653
+ clearAgentRuntime: /* @__PURE__ */ __name(() => refusedBy ? Promise.resolve() : runtimeState.clearAgentRuntime(), "clearAgentRuntime")
5654
+ };
5483
5655
  heartbeat = createRuntimeHeartbeat({ publisher, getRuntime: /* @__PURE__ */ __name(() => ({ runtimeDescriptor: descriptor }), "getRuntime"), onError: warn("runtime heartbeat") });
5484
5656
  inbound = endpoint.acceptInbound({ kind: "message.created", offer, onError: warn("inbox"), ...mlsOn ? { intervalMs: 1e3 } : {} });
5485
5657
  inbound.start();
5486
5658
  let reconnect = false;
5659
+ let streamUp = false;
5660
+ let following = Promise.resolve();
5661
+ const follow = /* @__PURE__ */ __name(() => {
5662
+ const check = reconnect;
5663
+ following = following.then(async () => {
5664
+ if (check) await recheck();
5665
+ if (streamUp) heartbeat?.connect();
5666
+ else void heartbeat?.disconnect();
5667
+ }).catch(warn("runtime heartbeat"));
5668
+ }, "follow");
5487
5669
  detach.push(endpoint.observeEvents(onEvent, {
5488
5670
  families: ["messages", "contacts", "runtime_turn"],
5489
5671
  onConnected: /* @__PURE__ */ __name(() => {
5490
- heartbeat?.connect();
5672
+ streamUp = true;
5673
+ follow();
5674
+ if (mlsOn) void noteAdmissions().catch(warn("room admission"));
5491
5675
  if (reconnect) {
5492
5676
  for (const target of rooms.values()) if (target.turn) scheduleControl(target);
5493
5677
  }
@@ -5499,7 +5683,10 @@ ${JSON.stringify(result, null, 2)}`);
5499
5683
  }
5500
5684
  reconnect = true;
5501
5685
  }, "onConnected"),
5502
- onDisconnected: /* @__PURE__ */ __name(() => void heartbeat?.disconnect(), "onDisconnected"),
5686
+ onDisconnected: /* @__PURE__ */ __name(() => {
5687
+ streamUp = false;
5688
+ follow();
5689
+ }, "onDisconnected"),
5503
5690
  onError: warn("event stream")
5504
5691
  }));
5505
5692
  if (errands) {
@@ -5761,6 +5948,7 @@ export {
5761
5948
  readRelayCredential,
5762
5949
  writeRelayCredential,
5763
5950
  relayRunnerHome,
5951
+ RUNNER_CLIENT,
5764
5952
  runtimeDescriptor,
5765
5953
  createRunner,
5766
5954
  parseWakeConfiguration,