From fc9ffd74f951df91cf37250bc268a4a934a1215e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 22 May 2026 11:09:05 -0600 Subject: [PATCH 1/2] Add explicit self-send option to send tool --- packages/agents-runtime/src/tools/send.ts | 53 +++++++++++++++---- .../agents-runtime/test/send-tool.test.ts | 45 ++++++++++++++++ packages/agents/src/agents/horton.ts | 4 +- packages/agents/src/agents/worker.ts | 2 +- 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/packages/agents-runtime/src/tools/send.ts b/packages/agents-runtime/src/tools/send.ts index e7c6b43bca..2a58b5961e 100644 --- a/packages/agents-runtime/src/tools/send.ts +++ b/packages/agents-runtime/src/tools/send.ts @@ -7,6 +7,11 @@ type SendFn = ( opts?: { type?: string; afterMs?: number } ) => Promise +export interface CreateSendToolOptions { + /** Optional URL of the current entity, used when the tool is called with `self: true`. */ + selfEntityUrl?: string +} + function asToolResult(value: unknown) { return { content: [ @@ -20,15 +25,25 @@ function asToolResult(value: unknown) { } } -export function createSendTool(send: SendFn): AgentTool { +export function createSendTool( + send: SendFn, + opts: CreateSendToolOptions = {} +): AgentTool { return { name: `send`, label: `Send Message`, - description: `Send a message to an Electric Agent/entity by entity URL. Use afterMs to schedule delayed delivery.`, + description: `Send a message to an Electric Agent/entity. Set self: true to send to yourself; use this with afterMs to schedule future work for yourself. Otherwise provide entityUrl.`, parameters: Type.Object({ - entityUrl: Type.String({ - description: `Target entity URL to send the message to.`, - }), + entityUrl: Type.Optional( + Type.String({ + description: `Target entity URL to send the message to. Omit when self is true.`, + }) + ), + self: Type.Optional( + Type.Boolean({ + description: `Send to this agent/entity. Use self: true with afterMs when scheduling future work for yourself.`, + }) + ), payload: Type.Any({ description: `Message payload to deliver to the target entity.`, }), @@ -42,8 +57,9 @@ export function createSendTool(send: SendFn): AgentTool { ), }), execute: async (_toolCallId, params) => { - const { entityUrl, payload, type, afterMs } = params as { - entityUrl: string + const { entityUrl, self, payload, type, afterMs } = params as { + entityUrl?: string + self?: boolean payload: unknown type?: string afterMs?: number @@ -57,17 +73,34 @@ export function createSendTool(send: SendFn): AgentTool { throw new Error(`afterMs must be a non-negative finite number`) } - const result = await send(entityUrl, payload, { type, afterMs }) - return asToolResult({ sent: true, entityUrl, type, afterMs, result }) + if (self && !opts.selfEntityUrl) { + throw new Error(`self is not available in this context`) + } + if (!self && !entityUrl) { + throw new Error(`provide entityUrl or set self: true`) + } + + const targetUrl = self ? opts.selfEntityUrl! : entityUrl! + const result = await send(targetUrl, payload, { type, afterMs }) + return asToolResult({ + sent: true, + entityUrl, + self, + targetUrl, + type, + afterMs, + result, + }) } catch (err) { const message = err instanceof Error ? err.message : String(err) return asToolResult({ sent: false, error: true, entityUrl, + self, type, afterMs, - message: `Failed to send to ${entityUrl}: ${message}`, + message: `Failed to send to ${self ? `self` : (entityUrl ?? `target`)}: ${message}`, }) } }, diff --git a/packages/agents-runtime/test/send-tool.test.ts b/packages/agents-runtime/test/send-tool.test.ts index bba51e2264..871be92e58 100644 --- a/packages/agents-runtime/test/send-tool.test.ts +++ b/packages/agents-runtime/test/send-tool.test.ts @@ -30,6 +30,7 @@ describe(`send tool`, () => { { sent: true, entityUrl: `http://localhost:4437/entities/agent-1`, + targetUrl: `http://localhost:4437/entities/agent-1`, type: `note`, afterMs: 250, result: { @@ -45,6 +46,50 @@ describe(`send tool`, () => { }) }) + it(`sends to the current entity when self is true`, async () => { + const send = vi.fn(async () => ({ + sent: true as const, + targetUrl: `/horton/current`, + })) + const tool = createSendTool(send, { selfEntityUrl: `/horton/current` }) + + const result = await tool.execute?.(`call-1`, { + self: true, + payload: { task: `check_ci` }, + type: `scheduled_check`, + afterMs: 300_000, + }) + + expect(send).toHaveBeenCalledWith( + `/horton/current`, + { task: `check_ci` }, + { type: `scheduled_check`, afterMs: 300_000 } + ) + expect(result).toMatchObject({ + details: {}, + content: [ + { + type: `text`, + text: JSON.stringify( + { + sent: true, + self: true, + targetUrl: `/horton/current`, + type: `scheduled_check`, + afterMs: 300_000, + result: { + sent: true, + targetUrl: `/horton/current`, + }, + }, + null, + 2 + ), + }, + ], + }) + }) + it(`returns structured error results when send fails`, async () => { const send = vi.fn(async () => { throw new Error(`server returned 500`) diff --git a/packages/agents/src/agents/horton.ts b/packages/agents/src/agents/horton.ts index fa45417914..1edc2c0693 100644 --- a/packages/agents/src/agents/horton.ts +++ b/packages/agents/src/agents/horton.ts @@ -237,7 +237,7 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem - web_search: search the web - fetch_url: fetch and convert a URL to markdown - spawn_worker: dispatch a subagent for an isolated task -- send: send a message to an Electric Agent/entity by entity URL +- send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs. ${docsTools}${skillsTools} # Working with files @@ -300,7 +300,7 @@ export function createHortonTools( ] : [fetchUrlTool]), createSpawnWorkerTool(ctx, opts.modelConfig), - createSendTool(ctx.send), + createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }), ...(opts.docsSearchTool ? [opts.docsSearchTool] : []), ] } diff --git a/packages/agents/src/agents/worker.ts b/packages/agents/src/agents/worker.ts index 614b3a55ec..a44d1c3b78 100644 --- a/packages/agents/src/agents/worker.ts +++ b/packages/agents/src/agents/worker.ts @@ -143,7 +143,7 @@ function buildToolsForWorker( out.push(createSpawnWorkerTool(ctx)) break case `send`: - out.push(createSendTool(ctx.send)) + out.push(createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl })) break } } From 1b642bc2f9867b199ffd3d699a0a05b8334ba124 Mon Sep 17 00:00:00 2001 From: Ilia Borovitinov Date: Mon, 25 May 2026 18:27:25 +0300 Subject: [PATCH 2/2] changeset --- .changeset/gentle-shirts-smash.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/gentle-shirts-smash.md diff --git a/.changeset/gentle-shirts-smash.md b/.changeset/gentle-shirts-smash.md new file mode 100644 index 0000000000..526385e708 --- /dev/null +++ b/.changeset/gentle-shirts-smash.md @@ -0,0 +1,6 @@ +--- +'@electric-ax/agents-runtime': patch +'@electric-ax/agents': patch +--- + +feat: add self-send option to `send`