summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/api/src/agent-manager.ts2
-rw-r--r--packages/core/src/tools/send-to-tab.ts32
-rw-r--r--packages/core/tests/tools/send-to-tab.test.ts16
3 files changed, 42 insertions, 8 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index 36a26f8..4264884 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -84,7 +84,7 @@ const TOOL_DESCRIPTIONS: Record<string, string> = {
youtube_transcribe:
"Fetch the transcript/subtitles for a YouTube video. Set background=true to start in the background and get a job_id for later retrieval.",
send_to_tab:
- "Send a message to another tab (agent) by its short ID, as shown in the tab bar. Fire-and-forget: it queues/wakes the target and returns immediately without waiting for a reply. Use read_tab later to read the target's response.",
+ "Send a message to another tab (agent) by its short ID, as shown in the tab bar. Fire-and-forget: it queues/wakes the target and returns immediately without waiting for a reply. Do NOT sleep, poll, or run commands to wait — a reply arrives on its own in a later turn (or use read_tab in a future turn); if you are only waiting, end your turn.",
read_tab:
"Read another tab (agent)'s most recent completed response by its short ID. Returns a non-blocking snapshot; if the target is still running you get its previous completed turn. Use after send_to_tab to collect a reply.",
};
diff --git a/packages/core/src/tools/send-to-tab.ts b/packages/core/src/tools/send-to-tab.ts
index eb86b7e..84e5f25 100644
--- a/packages/core/src/tools/send-to-tab.ts
+++ b/packages/core/src/tools/send-to-tab.ts
@@ -64,9 +64,14 @@ export function createSendToTabTool(callbacks: SendToTabCallbacks): ToolDefiniti
" - If the target tab is idle, your message WAKES it and starts a new turn.",
"",
"This is fire-and-forget: it returns immediately and does NOT wait for a reply.",
- "Use the 'read_tab' tool with the same ID later to read the target's latest response.",
+ "Do NOT sleep, poll, or run shell commands to wait for a reply — that wastes turns and",
+ "money. If the target replies it arrives on its own as a new message in a later turn; you",
+ "can also call 'read_tab' with the same ID in a FUTURE turn to check. If you have other",
+ "work to do, keep going; if you are ONLY waiting for the reply, end your turn now.",
"",
- "Your tab ID is auto-added to the top of the message so the recipient can reply to you.",
+ "Your tab ID is auto-added to the top of the message so the recipient knows who to reply",
+ "to. The recipient must use this same 'send_to_tab' tool (addressed to your ID) to answer;",
+ "a plain text response reaches only their own user, not you.",
"IDs are git-style prefixes: pass any length that uniquely identifies the target (min 4 chars).",
"If the ID is ambiguous you'll be asked to add a character.",
].join("\n"),
@@ -117,8 +122,18 @@ export function createSendToTabTool(callbacks: SendToTabCallbacks): ToolDefiniti
}
// Stamp provenance so the recipient (and the watching user) can see
- // which tab the message came from and reply back via its handle.
- const delivered = `[message from tab ${callbacks.self.handle}]\n\n${message}`;
+ // which tab the message came from and how to reply. The header makes
+ // clear this is a PEER AGENT, not the recipient's own user, and the
+ // footer states the reply contract: a reply (only if warranted) must
+ // go back through `send_to_tab`, since a plain text answer reaches
+ // only the recipient's own user — not this sender.
+ const delivered = [
+ `[message from tab ${callbacks.self.handle} — this is another agent, NOT your user]`,
+ "",
+ message,
+ "",
+ `[To reply to tab ${callbacks.self.handle}, use the send_to_tab tool with tab_id "${callbacks.self.handle}". Only reply if this message asks you to, or your user tells you to — it may just be context or instructions. A plain text response goes to your own user, not to this agent.]`,
+ ].join("\n");
try {
const result = await callbacks.deliver(target.id, delivered);
@@ -138,7 +153,14 @@ export function createSendToTabTool(callbacks: SendToTabCallbacks): ToolDefiniti
result.status === "queued"
? "queued (target is busy; it will be picked up next turn)"
: "delivered (target was idle; a new turn has started)";
- return `Message ${verb}. Target tab: ${target.handle} (${target.title}). Use read_tab with "${target.handle}" to read its reply later.`;
+ return [
+ `Message ${verb}. Target tab: ${target.handle} (${target.title}).`,
+ "",
+ "Do NOT sleep, poll, or run commands to wait for a reply. If the target replies it",
+ `arrives on its own as a new message later; you can also call read_tab with "${target.handle}"`,
+ "in a FUTURE turn to check. Keep working if you have other tasks; if you are ONLY",
+ "waiting for this reply, end your turn now.",
+ ].join("\n");
} catch (err) {
return `Error delivering message: ${err instanceof Error ? err.message : String(err)}`;
}
diff --git a/packages/core/tests/tools/send-to-tab.test.ts b/packages/core/tests/tools/send-to-tab.test.ts
index 4450fc5..68f8fa0 100644
--- a/packages/core/tests/tools/send-to-tab.test.ts
+++ b/packages/core/tests/tools/send-to-tab.test.ts
@@ -24,6 +24,9 @@ describe("createSendToTabTool — schema & description", () => {
expect(tool.name).toBe("send_to_tab");
expect(tool.description).toContain("fire-and-forget");
expect(tool.description.toLowerCase()).toContain("queued");
+ // Description must steer the model away from busy-waiting for a reply.
+ expect(tool.description.toLowerCase()).toContain("do not sleep");
+ expect(tool.description.toLowerCase()).toContain("end your turn");
});
});
@@ -35,11 +38,20 @@ describe("createSendToTabTool — execute()", () => {
expect(deliver).toHaveBeenCalledTimes(1);
const [targetId, delivered] = deliver.mock.calls[0] ?? [];
expect(targetId).toBe("target-id");
- // Provenance prefix names the sending tab's handle.
- expect(delivered).toContain("[message from tab self]");
+ // Provenance header names the sending tab's handle and marks it as a
+ // peer agent (not the recipient's own user).
+ expect(delivered).toContain("[message from tab self");
+ expect(delivered).toContain("another agent");
expect(delivered).toContain("hello there");
+ // Reply contract: the recipient must answer via send_to_tab back to the
+ // sender's handle, not as a plain text reply to its own user.
+ expect(delivered).toContain('send_to_tab tool with tab_id "self"');
+ expect(delivered.toLowerCase()).toContain("only reply if");
expect(out).toContain("idle");
expect(out).toContain("targ");
+ // Sender is steered away from busy-waiting and told to end its turn.
+ expect(out.toLowerCase()).toContain("do not sleep");
+ expect(out.toLowerCase()).toContain("end your turn");
});
it("reports the queued status when the target is busy", async () => {