Skip to content

Commit b3d4f02

Browse files
feat(agents-runtime): add explicit self-send option to send tool (#4398)
## Summary - add a `self: true` option to the send tool for sending to the current entity - document that agents should use `self: true` with `afterMs` to schedule future work for themselves - wire Horton and Worker send tools with their current entity URL ## Test - `pnpm --dir packages/agents-runtime test --run test/send-tool.test.ts` --------- Co-authored-by: Ilia Borovitinov <icehaunter@gmail.com>
1 parent b39f581 commit b3d4f02

5 files changed

Lines changed: 97 additions & 13 deletions

File tree

‎.changeset/gentle-shirts-smash.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@electric-ax/agents-runtime': patch
3+
'@electric-ax/agents': patch
4+
---
5+
6+
feat: add self-send option to `send`

‎packages/agents-runtime/src/tools/send.ts‎

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ type SendFn = (
77
opts?: { type?: string; afterMs?: number }
88
) => Promise<SendResult>
99

10+
export interface CreateSendToolOptions {
11+
/** Optional URL of the current entity, used when the tool is called with `self: true`. */
12+
selfEntityUrl?: string
13+
}
14+
1015
function asToolResult(value: unknown) {
1116
return {
1217
content: [
@@ -20,15 +25,25 @@ function asToolResult(value: unknown) {
2025
}
2126
}
2227

23-
export function createSendTool(send: SendFn): AgentTool {
28+
export function createSendTool(
29+
send: SendFn,
30+
opts: CreateSendToolOptions = {}
31+
): AgentTool {
2432
return {
2533
name: `send`,
2634
label: `Send Message`,
27-
description: `Send a message to an Electric Agent/entity by entity URL. Use afterMs to schedule delayed delivery.`,
35+
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.`,
2836
parameters: Type.Object({
29-
entityUrl: Type.String({
30-
description: `Target entity URL to send the message to.`,
31-
}),
37+
entityUrl: Type.Optional(
38+
Type.String({
39+
description: `Target entity URL to send the message to. Omit when self is true.`,
40+
})
41+
),
42+
self: Type.Optional(
43+
Type.Boolean({
44+
description: `Send to this agent/entity. Use self: true with afterMs when scheduling future work for yourself.`,
45+
})
46+
),
3247
payload: Type.Any({
3348
description: `Message payload to deliver to the target entity.`,
3449
}),
@@ -42,8 +57,9 @@ export function createSendTool(send: SendFn): AgentTool {
4257
),
4358
}),
4459
execute: async (_toolCallId, params) => {
45-
const { entityUrl, payload, type, afterMs } = params as {
46-
entityUrl: string
60+
const { entityUrl, self, payload, type, afterMs } = params as {
61+
entityUrl?: string
62+
self?: boolean
4763
payload: unknown
4864
type?: string
4965
afterMs?: number
@@ -57,17 +73,34 @@ export function createSendTool(send: SendFn): AgentTool {
5773
throw new Error(`afterMs must be a non-negative finite number`)
5874
}
5975

60-
const result = await send(entityUrl, payload, { type, afterMs })
61-
return asToolResult({ sent: true, entityUrl, type, afterMs, result })
76+
if (self && !opts.selfEntityUrl) {
77+
throw new Error(`self is not available in this context`)
78+
}
79+
if (!self && !entityUrl) {
80+
throw new Error(`provide entityUrl or set self: true`)
81+
}
82+
83+
const targetUrl = self ? opts.selfEntityUrl! : entityUrl!
84+
const result = await send(targetUrl, payload, { type, afterMs })
85+
return asToolResult({
86+
sent: true,
87+
entityUrl,
88+
self,
89+
targetUrl,
90+
type,
91+
afterMs,
92+
result,
93+
})
6294
} catch (err) {
6395
const message = err instanceof Error ? err.message : String(err)
6496
return asToolResult({
6597
sent: false,
6698
error: true,
6799
entityUrl,
100+
self,
68101
type,
69102
afterMs,
70-
message: `Failed to send to ${entityUrl}: ${message}`,
103+
message: `Failed to send to ${self ? `self` : (entityUrl ?? `target`)}: ${message}`,
71104
})
72105
}
73106
},

‎packages/agents-runtime/test/send-tool.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe(`send tool`, () => {
3030
{
3131
sent: true,
3232
entityUrl: `http://localhost:4437/entities/agent-1`,
33+
targetUrl: `http://localhost:4437/entities/agent-1`,
3334
type: `note`,
3435
afterMs: 250,
3536
result: {
@@ -45,6 +46,50 @@ describe(`send tool`, () => {
4546
})
4647
})
4748

49+
it(`sends to the current entity when self is true`, async () => {
50+
const send = vi.fn(async () => ({
51+
sent: true as const,
52+
targetUrl: `/horton/current`,
53+
}))
54+
const tool = createSendTool(send, { selfEntityUrl: `/horton/current` })
55+
56+
const result = await tool.execute?.(`call-1`, {
57+
self: true,
58+
payload: { task: `check_ci` },
59+
type: `scheduled_check`,
60+
afterMs: 300_000,
61+
})
62+
63+
expect(send).toHaveBeenCalledWith(
64+
`/horton/current`,
65+
{ task: `check_ci` },
66+
{ type: `scheduled_check`, afterMs: 300_000 }
67+
)
68+
expect(result).toMatchObject({
69+
details: {},
70+
content: [
71+
{
72+
type: `text`,
73+
text: JSON.stringify(
74+
{
75+
sent: true,
76+
self: true,
77+
targetUrl: `/horton/current`,
78+
type: `scheduled_check`,
79+
afterMs: 300_000,
80+
result: {
81+
sent: true,
82+
targetUrl: `/horton/current`,
83+
},
84+
},
85+
null,
86+
2
87+
),
88+
},
89+
],
90+
})
91+
})
92+
4893
it(`returns structured error results when send fails`, async () => {
4994
const send = vi.fn(async () => {
5095
throw new Error(`server returned 500`)

‎packages/agents/src/agents/horton.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
237237
- web_search: search the web
238238
- fetch_url: fetch and convert a URL to markdown
239239
- spawn_worker: dispatch a subagent for an isolated task
240-
- send: send a message to an Electric Agent/entity by entity URL
240+
- send: send a message to an Electric Agent/entity. To schedule future work for yourself, call send with self: true and afterMs.
241241
${docsTools}${skillsTools}
242242
243243
# Working with files
@@ -300,7 +300,7 @@ export function createHortonTools(
300300
]
301301
: [fetchUrlTool]),
302302
createSpawnWorkerTool(ctx, opts.modelConfig),
303-
createSendTool(ctx.send),
303+
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
304304
...(opts.docsSearchTool ? [opts.docsSearchTool] : []),
305305
]
306306
}

‎packages/agents/src/agents/worker.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ function buildToolsForWorker(
143143
out.push(createSpawnWorkerTool(ctx))
144144
break
145145
case `send`:
146-
out.push(createSendTool(ctx.send))
146+
out.push(createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }))
147147
break
148148
}
149149
}

0 commit comments

Comments
 (0)