Skip to content

Commit ef7dd3d

Browse files
committed
fix(managed-agent): address review findings on session lifecycle ops
- List Events kept the OLDEST slice when capped, dropping the agent's most recent reply. Paging is exhaustive again and the cap now keeps the newest N after ordering, with a `truncated` flag so callers know it is a tail. - listPaginated returned whole pages past maxItems; it now trims to the exact cap. - A whitespace-only title passed the update guard and would have cleared an existing session title. Blank is now treated as not provided. - Interrupt had no request timeout and could hang; bounded at 15s while still honoring the workflow signal. - Custom-tool gates were surfaced by Get Session but could not be answered, since they need user.custom_tool_result rather than a confirmation. Adds a Respond To Custom Tool operation and a `kind` on each pending gate so a workflow routes to the right one. - Docs rendered a raw ${DEFAULT_EVENT_LIMIT} placeholder for the default.
1 parent 2bd7fd5 commit ef7dd3d

15 files changed

Lines changed: 417 additions & 33 deletions

File tree

apps/docs/content/docs/en/integrations/managed_agent.mdx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Read a Managed Agent session: status, stop reason, token usage, metadata, and an
125125
| `status` | string | Session status — 'idle', 'running', 'rescheduling', or 'terminated'. |
126126
| `stopReason` | string | Why the session last stopped, e.g. 'end_turn' or 'requires_action'. |
127127
| `requiresAction` | boolean | True when the session is waiting on a tool confirmation or custom tool result. |
128-
| `pendingTools` | json | Tool calls awaiting approval \[\{id, eventType, name, input\}\]. Pass each id to Respond To Tool Confirmation. |
128+
| `pendingTools` | json | Blocking tool calls — \[\{id, eventType, kind, name, input\}\]. Route by kind: 'confirmation' ids go to Respond To Tool Confirmation, 'custom_tool_result' ids go to Respond To Custom Tool. |
129129
| `metadata` | json | Session metadata. |
130130
| `title` | string | Session title. |
131131
| `inputTokens` | number | Cumulative input tokens. |
@@ -140,7 +140,7 @@ Read a Managed Agent session's event history and the agent's reply text.
140140
| Parameter | Type | Required | Description |
141141
| --------- | ---- | -------- | ----------- |
142142
| `eventTypes` | array | No | Optional event-type filter, e.g. \['agent.message'\]. Omit to return every event. |
143-
| `limit` | number | No | Maximum events to return \(default $\{DEFAULT_EVENT_LIMIT\}\). |
143+
| `limit` | number | No | Maximum events to return, keeping the most recent \(default 500\). |
144144

145145
#### Output
146146

@@ -150,6 +150,7 @@ Read a Managed Agent session's event history and the agent's reply text.
150150
| `events` | json | Session events, oldest first. |
151151
| `count` | number | Number of events returned. |
152152
| `assistantText` | string | Concatenated text of every persisted agent.message, in order. |
153+
| `truncated` | boolean | True when the limit was hit and older events were dropped. |
153154

154155
### `managed_agent_update_session`
155156

@@ -195,7 +196,7 @@ Allow or deny the tool calls a Managed Agent session is waiting on before it can
195196

196197
| Parameter | Type | Required | Description |
197198
| --------- | ---- | -------- | ----------- |
198-
| `toolUseIds` | array | Yes | Blocking tool-use EVENT ids, from Get Session pendingTools\[\].id \(not toolu_ ids\). |
199+
| `toolUseIds` | array | Yes | Blocking tool-use EVENT ids, from Get Session pendingTools\[\].id where kind is 'confirmation' \(not toolu_ ids\). |
199200
| `decision` | string | Yes | 'allow' to let the tools run, or 'deny' to reject them. |
200201
| `denyMessage` | string | No | Reason surfaced to the agent. Only sent when the decision is deny. |
201202

@@ -207,6 +208,25 @@ Allow or deny the tool calls a Managed Agent session is waiting on before it can
207208
| `decision` | string | The decision applied — 'allow' or 'deny'. |
208209
| `confirmedToolUseIds` | json | The tool-use event ids that were answered. |
209210

211+
### `managed_agent_respond_custom_tool`
212+
213+
Return the result of a custom tool a Managed Agent session is waiting on so it can continue.
214+
215+
#### Input
216+
217+
| Parameter | Type | Required | Description |
218+
| --------- | ---- | -------- | ----------- |
219+
| `customToolUseIds` | array | Yes | Custom tool-use EVENT ids, from Get Session pendingTools\[\].id where kind is 'custom_tool_result'. |
220+
| `result` | string | Yes | The tool's output, returned to the agent as text. |
221+
| `isError` | boolean | No | Mark the result as a failure so the agent can adjust its approach. |
222+
223+
#### Output
224+
225+
| Parameter | Type | Description |
226+
| --------- | ---- | ----------- |
227+
| `sessionId` | string | The session that was answered. |
228+
| `answeredToolUseIds` | json | The custom tool-use event ids that were answered. |
229+
210230
### `managed_agent_archive_session`
211231

212232
Archive a Managed Agent session, preserving its history. Not reversible.

apps/sim/blocks/blocks/managed_agent.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe('Managed Agent block — operation routing', () => {
8585
['update_session', 'managed_agent_update_session'],
8686
['interrupt_session', 'managed_agent_interrupt_session'],
8787
['respond_tool_confirmation', 'managed_agent_respond_tool_confirmation'],
88+
['respond_custom_tool', 'managed_agent_respond_custom_tool'],
8889
['archive_session', 'managed_agent_archive_session'],
8990
['delete_session', 'managed_agent_delete_session'],
9091
])('maps %s to %s', (operation, toolId) => {
@@ -112,6 +113,7 @@ describe('Managed Agent block — per-operation field visibility', () => {
112113
'update_session',
113114
'interrupt_session',
114115
'respond_tool_confirmation',
116+
'respond_custom_tool',
115117
'archive_session',
116118
'delete_session',
117119
]) {
@@ -153,6 +155,17 @@ describe('Managed Agent block — per-operation field visibility', () => {
153155
expect(isVisible('denyMessage', { operation: 'send_message', decision: 'deny' })).toBe(false)
154156
})
155157

158+
it('separates confirmation fields from custom-tool-result fields', () => {
159+
// A confirmation cannot unblock a custom tool, so the two operations must
160+
// not share inputs — otherwise a workflow can silently answer the wrong way.
161+
expect(isVisible('toolUseIds', { operation: 'respond_tool_confirmation' })).toBe(true)
162+
expect(isVisible('toolUseIds', { operation: 'respond_custom_tool' })).toBe(false)
163+
expect(isVisible('customToolUseIds', { operation: 'respond_custom_tool' })).toBe(true)
164+
expect(isVisible('customToolUseIds', { operation: 'respond_tool_confirmation' })).toBe(false)
165+
expect(isVisible('result', { operation: 'respond_custom_tool' })).toBe(true)
166+
expect(isVisible('decision', { operation: 'respond_custom_tool' })).toBe(false)
167+
})
168+
156169
it('shows metadata for update session but not the agent config fields', () => {
157170
expect(isVisible('sessionParameters', { operation: 'update_session' })).toBe(true)
158171
expect(isVisible('title', { operation: 'update_session' })).toBe(true)
1.35 KB
Binary file not shown.

apps/sim/lib/integrations/integrations.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2938,6 +2938,10 @@
29382938
"name": "Respond to tool confirmation",
29392939
"description": "Allow or deny the tool calls a Managed Agent session is waiting on before it can continue."
29402940
},
2941+
{
2942+
"name": "Respond to custom tool",
2943+
"description": "Return the result of a custom tool a Managed Agent session is waiting on so it can continue."
2944+
},
29412945
{
29422946
"name": "Archive session",
29432947
"description": "Archive a Managed Agent session, preserving its history. Not reversible."
@@ -2947,7 +2951,7 @@
29472951
"description": "Permanently delete a Managed Agent session, its events, and its sandbox. Not reversible."
29482952
}
29492953
],
2950-
"operationCount": 10,
2954+
"operationCount": 11,
29512955
"triggers": [],
29522956
"triggerCount": 0,
29532957
"authType": "api-key",

apps/sim/lib/managed-agents/session-client.test.ts

Lines changed: 125 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
listSessionEvents,
1010
parseSessionSnapshot,
1111
resolvePendingToolGates,
12+
sendCustomToolResults,
1213
sendToolConfirmations,
1314
updateSession,
1415
} from '@/lib/managed-agents/session-client'
@@ -402,10 +403,17 @@ describe('resolvePendingToolGates', () => {
402403
})
403404

404405
expect(gates).toEqual([
405-
{ id: 'sevt_1', eventType: 'agent.tool_use', name: 'bash', input: { command: 'ls' } },
406+
{
407+
id: 'sevt_1',
408+
eventType: 'agent.tool_use',
409+
kind: 'confirmation',
410+
name: 'bash',
411+
input: { command: 'ls' },
412+
},
406413
{
407414
id: 'sevt_2',
408415
eventType: 'agent.mcp_tool_use',
416+
kind: 'confirmation',
409417
name: 'create_issue',
410418
input: { title: 'x' },
411419
},
@@ -442,6 +450,36 @@ describe('resolvePendingToolGates', () => {
442450
expect(gates).toEqual([{ id: 'sevt_1' }, { id: 'sevt_2' }])
443451
})
444452

453+
it('labels a custom-tool gate as needing a custom tool result, not a confirmation', async () => {
454+
// A confirmation cannot unblock a custom tool — the agent is waiting on the
455+
// tool's actual output — so the kind must route callers to the right op.
456+
global.fetch = vi.fn(async () =>
457+
Response.json({
458+
data: [{ id: 'sevt_9', type: 'agent.custom_tool_use', name: 'lookup_order' }],
459+
next_page: null,
460+
})
461+
) as unknown as typeof fetch
462+
463+
const gates = await resolvePendingToolGates({
464+
apiKey: 'sk-ant-fake',
465+
sessionId: 'sesn_1',
466+
eventIds: ['sevt_9'],
467+
})
468+
expect(gates[0]?.kind).toBe('custom_tool_result')
469+
})
470+
471+
it('omits kind when the event could not be resolved', async () => {
472+
global.fetch = vi.fn(async () => {
473+
throw new Error('network down')
474+
}) as unknown as typeof fetch
475+
const gates = await resolvePendingToolGates({
476+
apiKey: 'sk-ant-fake',
477+
sessionId: 'sesn_1',
478+
eventIds: ['sevt_1'],
479+
})
480+
expect(gates[0]).toEqual({ id: 'sevt_1' })
481+
})
482+
445483
it('short-circuits with no ids', async () => {
446484
const spy = vi.fn() as unknown as typeof fetch
447485
global.fetch = spy
@@ -458,28 +496,51 @@ describe('listSessionEvents — bounded reads', () => {
458496
global.fetch = originalFetch
459497
})
460498

461-
const pagedFetch = () =>
462-
vi.fn(async () =>
463-
Response.json({
499+
/** Emits `pages` pages of 100 chronologically-increasing events. */
500+
const pagedFetch = (pages: number) => {
501+
let page = 0
502+
return vi.fn(async () => {
503+
const offset = page * 100
504+
page += 1
505+
return Response.json({
464506
data: Array.from({ length: 100 }, (_, i) => ({
465-
id: `e${i}`,
507+
id: `e${offset + i}`,
466508
type: 'agent.message',
467-
processed_at: '2026-01-01T00:00:00Z',
509+
processed_at: new Date(Date.UTC(2026, 0, 1) + (offset + i) * 1000).toISOString(),
468510
})),
469-
next_page: 'cursor',
511+
next_page: page < pages ? `cursor-${page}` : null,
470512
})
471-
) as unknown as typeof fetch
513+
}) as unknown as typeof fetch
514+
}
472515

473-
it('stops paging once maxItems is reached', async () => {
474-
const spy = pagedFetch()
475-
global.fetch = spy
516+
it('returns exactly maxItems, never a whole extra page', async () => {
517+
global.fetch = pagedFetch(3)
476518
const events = await listSessionEvents({
477519
apiKey: 'sk-ant-fake',
478520
sessionId: 'sesn_1',
479521
maxItems: 250,
480522
})
481-
// 3 pages of 100 covers 250; a 4th would exceed the cap.
482-
expect((spy as unknown as ReturnType<typeof vi.fn>).mock.calls).toHaveLength(3)
523+
expect(events).toHaveLength(250)
524+
})
525+
526+
it('keeps the NEWEST events when capping, not the oldest', async () => {
527+
// Ascending history: e0 (oldest) .. e249 (newest). A cap of 10 must return
528+
// the last ten — capping the fetch instead would return e0..e9 and silently
529+
// drop the agent's most recent reply, which is what callers read this for.
530+
global.fetch = pagedFetch(3)
531+
const events = await listSessionEvents({
532+
apiKey: 'sk-ant-fake',
533+
sessionId: 'sesn_1',
534+
maxItems: 10,
535+
})
536+
expect(events).toHaveLength(10)
537+
expect(events[0]?.id).toBe('e290')
538+
expect(events.at(-1)?.id).toBe('e299')
539+
})
540+
541+
it('returns the whole history when uncapped', async () => {
542+
global.fetch = pagedFetch(3)
543+
const events = await listSessionEvents({ apiKey: 'sk-ant-fake', sessionId: 'sesn_1' })
483544
expect(events).toHaveLength(300)
484545
})
485546

@@ -497,3 +558,54 @@ describe('listSessionEvents — bounded reads', () => {
497558
expect(new URL(url).searchParams.getAll('types[]')).toEqual(['agent.message', 'agent.tool_use'])
498559
})
499560
})
561+
562+
describe('sendCustomToolResults', () => {
563+
const originalFetch = global.fetch
564+
afterEach(() => {
565+
global.fetch = originalFetch
566+
})
567+
568+
it('sends a user.custom_tool_result per pending call', async () => {
569+
const spy = vi.fn(async () => Response.json({})) as unknown as typeof fetch
570+
global.fetch = spy
571+
await sendCustomToolResults({
572+
apiKey: 'sk-ant-fake',
573+
sessionId: 'sesn_1',
574+
results: [{ customToolUseId: 'sevt_9', content: 'order #42 shipped', isError: false }],
575+
})
576+
const [, init] = (spy as unknown as ReturnType<typeof vi.fn>).mock.calls[0] as [
577+
string,
578+
RequestInit,
579+
]
580+
expect(JSON.parse(init.body as string)).toEqual({
581+
events: [
582+
{
583+
type: 'user.custom_tool_result',
584+
custom_tool_use_id: 'sevt_9',
585+
content: [{ type: 'text', text: 'order #42 shipped' }],
586+
is_error: false,
587+
},
588+
],
589+
})
590+
})
591+
592+
it('defaults is_error to false and honors an explicit failure', async () => {
593+
const spy = vi.fn(async () => Response.json({})) as unknown as typeof fetch
594+
global.fetch = spy
595+
await sendCustomToolResults({
596+
apiKey: 'sk-ant-fake',
597+
sessionId: 'sesn_1',
598+
results: [
599+
{ customToolUseId: 'a', content: 'ok' },
600+
{ customToolUseId: 'b', content: 'lookup failed', isError: true },
601+
],
602+
})
603+
const [, init] = (spy as unknown as ReturnType<typeof vi.fn>).mock.calls[0] as [
604+
string,
605+
RequestInit,
606+
]
607+
const events = JSON.parse(init.body as string).events
608+
expect(events[0].is_error).toBe(false)
609+
expect(events[1].is_error).toBe(true)
610+
})
611+
})

0 commit comments

Comments
 (0)