relay-companion 0.1.99 → 0.1.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/mcp.js +113 -14
package/package.json
CHANGED
package/src/mcp.js
CHANGED
|
@@ -14,7 +14,7 @@ export const TOOLS = [
|
|
|
14
14
|
{
|
|
15
15
|
name: "relay_send",
|
|
16
16
|
description:
|
|
17
|
-
"Send an ordinary Relay message or handoff. Use this for the normal product flow when the human says things like 'send Sven a relay asking how he is doing' or 'relay this context to Jordan'. THREADING: Relay groups messages into conversation threads built from reply chains,
|
|
17
|
+
"Send an ordinary Relay message or handoff. Use this for the normal product flow when the human says things like 'send Sven a relay asking how he is doing' or 'relay this context to Jordan'. THREADING — check this before EVERY send: Relay groups messages into conversation threads built from reply chains, and a thread only comes into existence when the sender links the messages, so an unlinked follow-up is lost as a standalone relay forever. Look at BOTH sides of the recent exchange with this recipient: relay_sent_list for what this human already sent them, and relay_inbox_list for what they sent back. You MUST call relay_sent_list rather than rely on memory — your own earlier sends are not in relay_inbox_list, and a send from a previous session is not in your context. If the new message continues an existing exchange, pass inReplyToRelayId set to the LATEST relay id of that exchange (whichever side sent it) so the conversation stays one thread. A one-way series of updates is still one exchange: rounds 2, 3 and 4 of a piece of work, a correction, or a status update on something you already relayed this person all belong on the first message's thread — not in four standalone relays the human has to reassemble. Omit inReplyToRelayId only when this genuinely opens a subject you have not relayed this person about. A thread also has a NAME the human scans in their Relay list: whenever you continue a thread that is not already well named, pass threadTitle to name the conversation (see that field). Granular digital employees appear as managed, workspace-labelled contacts; use relay_contacts_search and pass the exact matching contactId so a user with several workspaces reaches the intended employee. That message is authored by this human and wakes their employee; it is NOT a Relay task or deterministic workflow. Never use task-only tools to contact a Granular employee. For other recipients, an on-Relay message appears in their companion and Relays list; an off-Relay recipient receives an email open link.",
|
|
18
18
|
inputSchema: {
|
|
19
19
|
type: "object",
|
|
20
20
|
properties: {
|
|
@@ -77,12 +77,12 @@ export const TOOLS = [
|
|
|
77
77
|
inReplyToRelayId: {
|
|
78
78
|
type: "string",
|
|
79
79
|
description:
|
|
80
|
-
"The id of the relay this message replies to. Threads are built from reply chains
|
|
80
|
+
"The id of the relay this message replies to or follows up on — the one field that puts this message into an existing conversation instead of starting a new one. Threads are built from reply chains, so pass the NEWEST relay id in that exchange, never an older one, and the whole conversation shares one thread. Find it in relay_sent_list (this human's own earlier sends — the usual source when following up on your own message), relay_inbox_list (what the other person sent), or the relayId returned by an earlier relay_send in this same session. This does not have to be a literal answer to a question: a follow-up, a next round, a revision, or a status update on something you already relayed this person is a continuation and belongs on the same thread. Omit it only to open a genuinely new subject. When answering a question relay, pass that relay's id here — this is what resumes the asking agent's run.",
|
|
81
81
|
},
|
|
82
82
|
threadTitle: {
|
|
83
83
|
type: "string",
|
|
84
84
|
description:
|
|
85
|
-
"Name the CONVERSATION (not this message). A thread comes into existence the moment a relay replies to another, and its name is what both people scan in their Relay list ever after — so whenever you
|
|
85
|
+
"Name the CONVERSATION (not this message). A thread comes into existence the moment a relay replies to another, and its name is what both people scan in their Relay list ever after — so whenever you continue a thread (inReplyToRelayId set) that is not already well named, title it here. threadTitle only names a conversation that actually exists: setting it WITHOUT inReplyToRelayId merely labels a brand-new one-message thread and groups this relay with nothing, so if you meant to continue an exchange, find its newest relay with relay_sent_list and set inReplyToRelayId as well. Same discipline as `title`, one level up: in as few words as it honestly takes (~5-12 words, hard stop 140 characters), say what this exchange IS and what it is for, so someone reading the list weeks later knows whether to open it. Name the ongoing subject, not the latest turn: 'Postmark migration for sendrelays.com — DNS + verification' beats 'Re: DNS records' or 'Postmark'. Never a bare noun ('Update', 'Question'), never a timestamp, never the other person's name alone. Setting it again renames the thread — use that when a conversation genuinely changes subject, and leave it alone when the existing name still fits.",
|
|
86
86
|
},
|
|
87
87
|
type: {
|
|
88
88
|
type: "string",
|
|
@@ -180,13 +180,32 @@ export const TOOLS = [
|
|
|
180
180
|
{
|
|
181
181
|
name: "relay_inbox_list",
|
|
182
182
|
description:
|
|
183
|
-
"List ordinary Relay messages addressed to this human. Each item carries threadId (the conversation thread it belongs to — the root relay's id) and, on replies, inReplyToRelayId; use them to reconstruct the recent exchange with a sender before continuing it via relay_send's inReplyToRelayId. This does not list task coordination relays; use relay_task_status for task state.",
|
|
183
|
+
"List ordinary Relay messages addressed to this human — the INBOUND half of their correspondence only. Each item carries threadId (the conversation thread it belongs to — the root relay's id) and, on replies, inReplyToRelayId; use them to reconstruct the recent exchange with a sender before continuing it via relay_send's inReplyToRelayId. Relays this human SENT are not here: to follow up on one of their own earlier messages, call relay_sent_list. This does not list task coordination relays; use relay_task_status for task state.",
|
|
184
184
|
inputSchema: { type: "object", properties: {} },
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: "relay_sent_list",
|
|
188
|
+
description:
|
|
189
|
+
"List the ordinary relays this human has SENT, newest first — the outbound counterpart to relay_inbox_list. Call this before sending anyone a follow-up, a next round, or an update on something already relayed to them. Each item carries the relayId, threadId and threadTitle that relay_send's inReplyToRelayId needs to keep an ongoing exchange in ONE conversation, and this is the ONLY way to obtain them: a human's own sends never appear in relay_inbox_list, and a send from an earlier session is not in your context. Pass recipient to narrow to one correspondent; the first item back is then the newest relay in that exchange, which is exactly what inReplyToRelayId wants. Message bodies are omitted to keep the list small — read a full exchange with relay_thread_fetch on any item's threadId.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
recipient: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description:
|
|
196
|
+
"Optional case-insensitive substring of the recipient's name, email, or group name, e.g. 'sven' or 'sven@example.com'. Strongly preferred when following up with a specific person.",
|
|
197
|
+
},
|
|
198
|
+
limit: {
|
|
199
|
+
type: "number",
|
|
200
|
+
description: "How many sends to return, newest first. Defaults to 20; capped at 100.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
186
205
|
{
|
|
187
206
|
name: "relay_thread_fetch",
|
|
188
207
|
description:
|
|
189
|
-
"Fetch ONE full Relay conversation thread, oldest first: its threadTitle (the conversation's name — check it before replying; if it is missing or no longer describes the exchange, pass a better one as relay_send's threadTitle) and every relay in the exchange (both directions) with sender, recipients, timestamps, title, body, and read state. Use this whenever a relay you are opening or replying to is part of a thread (its threadId differs from its own id, or an Open-in-chat instruction names a threadId) so the WHOLE exchange is in context before you act — never reconstruct a conversation by fetching relays one at a time. Pass the threadId (the root relay's id, carried on every relay_inbox_list item
|
|
208
|
+
"Fetch ONE full Relay conversation thread, oldest first: its threadTitle (the conversation's name — check it before replying; if it is missing or no longer describes the exchange, pass a better one as relay_send's threadTitle) and every relay in the exchange (both directions) with sender, recipients, timestamps, title, body, and read state. Use this whenever a relay you are opening or replying to is part of a thread (its threadId differs from its own id, or an Open-in-chat instruction names a threadId) so the WHOLE exchange is in context before you act — never reconstruct a conversation by fetching relays one at a time. Pass the threadId (the root relay's id), which is carried on every relay_inbox_list and relay_sent_list item and returned by relay_send.",
|
|
190
209
|
inputSchema: {
|
|
191
210
|
type: "object",
|
|
192
211
|
properties: {
|
|
@@ -559,10 +578,55 @@ export const MESSAGE_ONLY_TOOL_NAMES = new Set([
|
|
|
559
578
|
"relay_group_update",
|
|
560
579
|
"relay_group_delete",
|
|
561
580
|
"relay_inbox_list",
|
|
581
|
+
// The sender-side history an agent needs to thread a follow-up. Without it,
|
|
582
|
+
// ordinary messaging can only ever start new conversations.
|
|
583
|
+
"relay_sent_list",
|
|
562
584
|
"relay_thread_fetch",
|
|
563
585
|
"relay_acknowledge",
|
|
564
586
|
]);
|
|
565
587
|
|
|
588
|
+
const SENT_LIST_DEFAULT_LIMIT = 20;
|
|
589
|
+
const SENT_LIST_MAX_LIMIT = 100;
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The threading-relevant shape of one sent relay. Bodies, previews and
|
|
593
|
+
* attachment descriptors are dropped: a full sent list runs to hundreds of
|
|
594
|
+
* thousands of characters and can blow an agent's context on a lookup whose
|
|
595
|
+
* whole purpose is to find one id.
|
|
596
|
+
*/
|
|
597
|
+
function toSentSummary(item) {
|
|
598
|
+
return {
|
|
599
|
+
relayId: item?.relayId,
|
|
600
|
+
threadId: item?.threadId ?? item?.relayId,
|
|
601
|
+
...(item?.threadTitle ? { threadTitle: item.threadTitle } : {}),
|
|
602
|
+
...(item?.inReplyToRelayId ? { inReplyToRelayId: item.inReplyToRelayId } : {}),
|
|
603
|
+
title: item?.title,
|
|
604
|
+
recipient: {
|
|
605
|
+
name: item?.recipient?.name ?? "",
|
|
606
|
+
...(item?.recipient?.email ? { email: item.recipient.email } : {}),
|
|
607
|
+
},
|
|
608
|
+
...(item?.recipientGroupName ? { recipientGroupName: item.recipientGroupName } : {}),
|
|
609
|
+
createdAt: item?.createdAt,
|
|
610
|
+
...(item?.readAt ? { readAt: item.readAt } : {}),
|
|
611
|
+
state: item?.state,
|
|
612
|
+
hasAttachments: Boolean(item?.hasAttachments),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function matchesRecipient(item, needle) {
|
|
617
|
+
const haystack = [item?.recipient?.name, item?.recipient?.email, item?.recipientGroupName]
|
|
618
|
+
.filter((value) => typeof value === "string")
|
|
619
|
+
.join(" ")
|
|
620
|
+
.toLowerCase();
|
|
621
|
+
return haystack.includes(needle);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function sentListLimit(value) {
|
|
625
|
+
const parsed = Number(value);
|
|
626
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SENT_LIST_DEFAULT_LIMIT;
|
|
627
|
+
return Math.min(Math.floor(parsed), SENT_LIST_MAX_LIMIT);
|
|
628
|
+
}
|
|
629
|
+
|
|
566
630
|
export function toolsForMode(mode = DEFAULT_COMPANION_MODE) {
|
|
567
631
|
const normalized = normalizeCompanionMode(mode);
|
|
568
632
|
return normalized === COMPANION_MODE_MESSAGES_ONLY
|
|
@@ -575,17 +639,30 @@ function text(obj) {
|
|
|
575
639
|
}
|
|
576
640
|
|
|
577
641
|
function relaySendResultForAgent(result) {
|
|
578
|
-
|
|
642
|
+
// Servers that predate threading hints simply omit the field, so an older API
|
|
643
|
+
// keeps working and only loses the nudge.
|
|
644
|
+
const hint = result?.threadingHint;
|
|
645
|
+
const contactNeedsWork = Boolean(result?.contact?.autoCreated);
|
|
646
|
+
if (!hint && !contactNeedsWork) return result;
|
|
647
|
+
|
|
648
|
+
const instructions = [];
|
|
649
|
+
if (hint?.message) instructions.push(hint.message);
|
|
650
|
+
if (contactNeedsWork) {
|
|
651
|
+
instructions.push(
|
|
652
|
+
result.agentInstruction ||
|
|
653
|
+
[
|
|
654
|
+
`Relay auto-added ${result.contact.email} to this human's contact book because there was no saved contact for this recipient.`,
|
|
655
|
+
"You are responsible for correcting the contact's firstName and surname with relay_contact_update if you know them from reliable context.",
|
|
656
|
+
"If you are not sure of the person's first name and surname, ask the human for clarification before editing the contact.",
|
|
657
|
+
].join(" "),
|
|
658
|
+
);
|
|
659
|
+
}
|
|
579
660
|
return {
|
|
580
661
|
...result,
|
|
581
|
-
agentInstruction:
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
"You are responsible for correcting the contact's firstName and surname with relay_contact_update if you know them from reliable context.",
|
|
586
|
-
"If you are not sure of the person's first name and surname, ask the human for clarification before editing the contact.",
|
|
587
|
-
].join(" "),
|
|
588
|
-
nextRecommendedTool: "relay_contact_update",
|
|
662
|
+
agentInstruction: instructions.join(" "),
|
|
663
|
+
// Threading is the more urgent correction: the window to fix it closes as
|
|
664
|
+
// soon as this send settles, while a contact name can be repaired any time.
|
|
665
|
+
nextRecommendedTool: hint ? "relay_send" : "relay_contact_update",
|
|
589
666
|
};
|
|
590
667
|
}
|
|
591
668
|
|
|
@@ -683,6 +760,28 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
|
|
|
683
760
|
);
|
|
684
761
|
case "relay_inbox_list":
|
|
685
762
|
return text(await client.inbox());
|
|
763
|
+
case "relay_sent_list": {
|
|
764
|
+
const response = await client.sent();
|
|
765
|
+
const all = Array.isArray(response?.items) ? response.items : [];
|
|
766
|
+
const needle = typeof args.recipient === "string" ? args.recipient.trim().toLowerCase() : "";
|
|
767
|
+
const matched = needle ? all.filter((item) => matchesRecipient(item, needle)) : all;
|
|
768
|
+
const limit = sentListLimit(args.limit);
|
|
769
|
+
const items = matched.slice(0, limit).map(toSentSummary);
|
|
770
|
+
return text({
|
|
771
|
+
items,
|
|
772
|
+
matched: matched.length,
|
|
773
|
+
// A truncated list is stated outright: silently returning 20 of 60 reads
|
|
774
|
+
// as "this is everything" and would send an agent back to guessing.
|
|
775
|
+
...(matched.length > items.length ? { truncated: true } : {}),
|
|
776
|
+
...(needle ? { recipientFilter: args.recipient } : {}),
|
|
777
|
+
...(items.length
|
|
778
|
+
? {
|
|
779
|
+
agentInstruction:
|
|
780
|
+
"Items are newest first. To continue any of these conversations, pass that item's relayId as relay_send's inReplyToRelayId — for a follow-up to one person that is normally the first item. Bodies are omitted here; relay_thread_fetch on an item's threadId returns the full exchange.",
|
|
781
|
+
}
|
|
782
|
+
: {}),
|
|
783
|
+
});
|
|
784
|
+
}
|
|
686
785
|
case "relay_thread_fetch":
|
|
687
786
|
return text(await client.thread(args.threadId));
|
|
688
787
|
case "relay_inbox_delete":
|