Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions packages/agents-runtime/src/tools/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ type SendFn = (
opts?: { type?: string; afterMs?: number }
) => Promise<SendResult>

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: [
Expand All @@ -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.`,
}),
Expand All @@ -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
Expand All @@ -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}`,
})
}
},
Expand Down
45 changes: 45 additions & 0 deletions packages/agents-runtime/test/send-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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`)
Expand Down
4 changes: 2 additions & 2 deletions packages/agents/src/agents/horton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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] : []),
]
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agents/src/agents/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Loading