Skip to content

Commit f88b08d

Browse files
committed
improvement(managed-agents): complete session inputs/outputs; trim block templates
- add memory instructions + file mount_path inputs; bound metadata (16 pairs) - surface cumulative token usage (inputTokens/outputTokens) as outputs - validate the full session-create schema against live docs - remove BlockMeta templates
1 parent 6496608 commit f88b08d

11 files changed

Lines changed: 230 additions & 55 deletions

File tree

apps/sim/app/api/tools/managed-agent/run/route.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
9494
vaultIds: body.vaults,
9595
memoryStoreId: body.memoryStoreId,
9696
memoryAccess: body.memoryAccess,
97-
fileIds: body.fileIds,
97+
memoryInstructions: body.memoryInstructions,
98+
files: body.files,
9899
sessionParameters: body.sessionParameters,
99100
signal: request.signal,
100101
})
@@ -114,6 +115,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
114115

115116
return NextResponse.json({
116117
success: true,
117-
output: { content: result.content, sessionId: result.sessionId ?? '' },
118+
output: {
119+
content: result.content,
120+
sessionId: result.sessionId ?? '',
121+
...(result.inputTokens !== undefined ? { inputTokens: result.inputTokens } : {}),
122+
...(result.outputTokens !== undefined ? { outputTokens: result.outputTokens } : {}),
123+
},
118124
})
119125
})

apps/sim/blocks/blocks/managed_agent.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,23 @@ export const ManagedAgentBlock: BlockConfig = {
103103
condition: { field: 'memoryStoreId', value: '', not: true },
104104
description: 'read_write pushes changes back on session exit; read_only never writes.',
105105
},
106+
{
107+
id: 'memoryInstructions',
108+
title: 'Memory instructions',
109+
type: 'long-input',
110+
required: false,
111+
placeholder: 'Optional — how the agent should use this memory store',
112+
condition: { field: 'memoryStoreId', value: '', not: true },
113+
description: 'Per-attachment guidance rendered into the memory section of the system prompt.',
114+
},
106115
{
107116
id: 'files',
108117
title: 'Files',
109118
type: 'table',
110119
required: false,
111-
columns: ['File ID'],
120+
columns: ['File ID', 'Mount path'],
112121
description:
113-
'Files-API file ids (file_...) to attach to the session as file resources (cloud environments).',
122+
'Files-API file ids (file_...) to attach as file resources (cloud environments). Mount path is optional.',
114123
},
115124
{
116125
id: 'sessionParameters',
@@ -142,27 +151,22 @@ export const ManagedAgentBlock: BlockConfig = {
142151
type: 'string',
143152
description: "Memory store access mode — 'read_write' (default) or 'read_only'.",
144153
},
145-
files: { type: 'json', description: 'Files-API file ids to attach as file resources.' },
154+
memoryInstructions: {
155+
type: 'string',
156+
description: 'Per-attachment guidance for how the agent should use the memory store.',
157+
},
158+
files: { type: 'json', description: 'File attachments — [{fileId, mountPath?}].' },
146159
sessionParameters: { type: 'json', description: 'Session metadata (key/value).' },
147160
},
148161
outputs: {
149162
content: { type: 'string', description: "The Managed Agent's final assistant text." },
150163
sessionId: { type: 'string', description: 'Anthropic session id, for logs and linking.' },
164+
inputTokens: { type: 'number', description: 'Cumulative input tokens for the session.' },
165+
outputTokens: { type: 'number', description: 'Cumulative output tokens for the session.' },
151166
},
152167
}
153168

154169
export const ManagedAgentBlockMeta = {
155170
tags: ['agentic', 'llm'],
156171
url: 'https://platform.claude.com/',
157-
templates: [
158-
{
159-
icon: ClaudeIcon,
160-
title: 'Delegate a task to a Claude Managed Agent',
161-
prompt:
162-
"Build a workflow that opens a Claude Platform Managed Agent session, optionally attaches a memory store and files, and captures the agent's response.",
163-
modules: ['agent', 'workflows'],
164-
category: 'engineering',
165-
tags: ['automation', 'analysis'],
166-
},
167-
],
168172
} as const satisfies BlockMeta

apps/sim/lib/api/contracts/managed-agents.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ export type ListManagedAgentOptions = ContractJsonResponse<typeof listManagedAge
5151
* clean shapes in `request.body` before dispatch, so the route validates
5252
* strict types.
5353
*/
54+
/** `metadata` is capped by the API at 16 pairs, keys ≤64, values ≤512 chars. */
55+
const sessionMetadataSchema = z.record(z.string(), z.string()).superRefine((value, ctx) => {
56+
const entries = Object.entries(value)
57+
if (entries.length > 16) {
58+
ctx.addIssue({ code: 'custom', message: 'At most 16 metadata pairs are allowed.' })
59+
}
60+
for (const [key, val] of entries) {
61+
if (key.length > 64) {
62+
ctx.addIssue({ code: 'custom', message: `Metadata key "${key}" exceeds 64 characters.` })
63+
}
64+
if (val.length > 512) {
65+
ctx.addIssue({
66+
code: 'custom',
67+
message: `Metadata value for "${key}" exceeds 512 characters.`,
68+
})
69+
}
70+
}
71+
})
72+
5473
export const runManagedAgentBodySchema = z.object({
5574
agent: z.string().min(1, 'agent is required'),
5675
environment: z.string().min(1, 'environment is required'),
@@ -59,8 +78,12 @@ export const runManagedAgentBodySchema = z.object({
5978
vaultsAck: z.boolean().optional(),
6079
memoryStoreId: z.string().optional(),
6180
memoryAccess: z.enum(['read_write', 'read_only']).optional(),
62-
fileIds: z.array(z.string().min(1)).max(100).optional(),
63-
sessionParameters: z.record(z.string(), z.string()).optional(),
81+
memoryInstructions: z.string().max(4096).optional(),
82+
files: z
83+
.array(z.object({ fileId: z.string().min(1), mountPath: z.string().min(1).optional() }))
84+
.max(100)
85+
.optional(),
86+
sessionParameters: sessionMetadataSchema.optional(),
6487
})
6588
export type RunManagedAgentBody = z.input<typeof runManagedAgentBodySchema>
6689

@@ -79,7 +102,12 @@ export const runManagedAgentContract = defineRouteContract({
79102
mode: 'json',
80103
schema: z.object({
81104
success: z.literal(true),
82-
output: z.object({ content: z.string(), sessionId: z.string() }),
105+
output: z.object({
106+
content: z.string(),
107+
sessionId: z.string(),
108+
inputTokens: z.number().optional(),
109+
outputTokens: z.number().optional(),
110+
}),
83111
}),
84112
},
85113
})

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { mocks } = vi.hoisted(() => ({
1111
sendSessionEvents: vi.fn(),
1212
openSessionStream: vi.fn(),
1313
listSessionEvents: vi.fn(),
14+
getSessionUsage: vi.fn(),
1415
readSSEEvents: vi.fn(),
1516
sleep: vi.fn(),
1617
},
@@ -22,6 +23,7 @@ vi.mock('@/lib/managed-agents/session-client', () => ({
2223
sendSessionEvents: mocks.sendSessionEvents,
2324
openSessionStream: mocks.openSessionStream,
2425
listSessionEvents: mocks.listSessionEvents,
26+
getSessionUsage: mocks.getSessionUsage,
2527
}))
2628
vi.mock('@/lib/core/utils/sse', () => ({ readSSEEvents: mocks.readSSEEvents }))
2729
vi.mock('@sim/utils/helpers', () => ({ sleep: mocks.sleep }))
@@ -60,6 +62,7 @@ beforeEach(() => {
6062
mocks.createSession.mockResolvedValue({ id: 'sess_1' })
6163
mocks.sendUserMessage.mockResolvedValue(undefined)
6264
mocks.openSessionStream.mockResolvedValue({})
65+
mocks.getSessionUsage.mockResolvedValue(null)
6366
})
6467

6568
describe('runManagedAgentSession', () => {
@@ -78,6 +81,26 @@ describe('runManagedAgentSession', () => {
7881
expect(mocks.listSessionEvents).not.toHaveBeenCalled()
7982
})
8083

84+
it('surfaces cumulative token usage on success (best-effort)', async () => {
85+
scriptStreamBatches([
86+
[
87+
msg('e1', 'ok'),
88+
{ id: 'e2', type: 'session.status_idle', stop_reason: { type: 'end_turn' } },
89+
],
90+
])
91+
mocks.getSessionUsage.mockResolvedValue({ inputTokens: 120, outputTokens: 45 })
92+
93+
const result = await runManagedAgentSession({ ...BASE })
94+
95+
expect(result).toEqual({
96+
ok: true,
97+
content: 'ok',
98+
sessionId: 'sess_1',
99+
inputTokens: 120,
100+
outputTokens: 45,
101+
})
102+
})
103+
81104
it('does NOT false-timeout after requires_action followed by progress then a quiet reconnect', async () => {
82105
// Stream 1: only a requires_action idle (busy), then the stream closes.
83106
// Stream 2: closes immediately with nothing new.

apps/sim/lib/managed-agents/run-session.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type AnthropicSessionEvent,
77
type CreateSessionInput,
88
createSession,
9+
getSessionUsage,
910
listSessionEvents,
1011
openSessionStream,
1112
sendSessionEvents,
@@ -48,7 +49,8 @@ export interface RunManagedAgentInput {
4849
vaultIds?: string[]
4950
memoryStoreId?: string
5051
memoryAccess?: 'read_write' | 'read_only'
51-
fileIds?: string[]
52+
memoryInstructions?: string
53+
files?: Array<{ fileId: string; mountPath?: string }>
5254
sessionParameters?: Record<string, string>
5355
signal?: AbortSignal
5456
}
@@ -58,6 +60,8 @@ export interface RunManagedAgentResult {
5860
content: string
5961
sessionId?: string
6062
error?: string
63+
inputTokens?: number
64+
outputTokens?: number
6165
}
6266

6367
export async function runManagedAgentSession(
@@ -77,7 +81,10 @@ export async function runManagedAgentSession(
7781
...(input.vaultIds && input.vaultIds.length > 0 ? { vaultIds: input.vaultIds } : {}),
7882
...(input.memoryStoreId ? { memoryStoreId: input.memoryStoreId } : {}),
7983
...(input.memoryStoreId && input.memoryAccess ? { memoryAccess: input.memoryAccess } : {}),
80-
...(input.fileIds && input.fileIds.length > 0 ? { fileIds: input.fileIds } : {}),
84+
...(input.memoryStoreId && input.memoryInstructions
85+
? { memoryInstructions: input.memoryInstructions }
86+
: {}),
87+
...(input.files && input.files.length > 0 ? { files: input.files } : {}),
8188
...(input.sessionParameters && Object.keys(input.sessionParameters).length > 0
8289
? { sessionParameters: input.sessionParameters }
8390
: {}),
@@ -209,7 +216,17 @@ export async function runManagedAgentSession(
209216
error: terminal?.reason ?? 'Reconnect iteration cap reached without a terminal state.',
210217
}
211218
}
212-
return { ok: true, content: assistantText.value, sessionId }
219+
220+
// Best-effort: surface cumulative token usage. A failed lookup never fails
221+
// the run — the assistant text is already the result.
222+
const usage = await getSessionUsage({ apiKey, sessionId, signal })
223+
return {
224+
ok: true,
225+
content: assistantText.value,
226+
sessionId,
227+
...(usage?.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
228+
...(usage?.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
229+
}
213230
}
214231

215232
/** Tracks progress across `requires_action` idle events. */

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,29 @@ describe('buildSessionCreatePayload — resources', () => {
5151
])
5252
})
5353

54-
it('attaches file resources by id (no mount path)', () => {
55-
const payload = buildSessionCreatePayload({ ...BASE, fileIds: ['file_1', 'file_2'] })
54+
it('includes memory instructions when provided', () => {
55+
const payload = buildSessionCreatePayload({
56+
...BASE,
57+
memoryStoreId: 'memstore_01',
58+
memoryInstructions: 'check before starting',
59+
})
5660
expect(payload.resources).toEqual([
57-
{ type: 'file', file_id: 'file_1' },
61+
{
62+
type: 'memory_store',
63+
memory_store_id: 'memstore_01',
64+
access: 'read_write',
65+
instructions: 'check before starting',
66+
},
67+
])
68+
})
69+
70+
it('attaches file resources with an optional mount path', () => {
71+
const payload = buildSessionCreatePayload({
72+
...BASE,
73+
files: [{ fileId: 'file_1', mountPath: '/data/one' }, { fileId: 'file_2' }],
74+
})
75+
expect(payload.resources).toEqual([
76+
{ type: 'file', file_id: 'file_1', mount_path: '/data/one' },
5877
{ type: 'file', file_id: 'file_2' },
5978
])
6079
})
@@ -63,7 +82,7 @@ describe('buildSessionCreatePayload — resources', () => {
6382
const payload = buildSessionCreatePayload({
6483
...BASE,
6584
memoryStoreId: 'memstore_01',
66-
fileIds: ['file_1'],
85+
files: [{ fileId: 'file_1' }],
6786
})
6887
expect(payload.resources).toEqual([
6988
{ type: 'memory_store', memory_store_id: 'memstore_01', access: 'read_write' },
@@ -73,7 +92,7 @@ describe('buildSessionCreatePayload — resources', () => {
7392

7493
it('omits `resources` when nothing is attached', () => {
7594
expect(buildSessionCreatePayload({ ...BASE }).resources).toBeUndefined()
76-
expect(buildSessionCreatePayload({ ...BASE, fileIds: [] }).resources).toBeUndefined()
95+
expect(buildSessionCreatePayload({ ...BASE, files: [] }).resources).toBeUndefined()
7796
})
7897
})
7998

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

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ export interface CreateSessionInput extends SessionAuth {
5757
memoryStoreId?: string
5858
/** Access mode on the attached memory store. Ignored when `memoryStoreId` is unset. */
5959
memoryAccess?: 'read_write' | 'read_only'
60-
/** Files-API file ids (`file_...`) attached as `file` session resources. */
61-
fileIds?: string[]
60+
/** Per-attachment guidance rendered into the memory section of the system prompt. */
61+
memoryInstructions?: string
62+
/** Files-API files (`file_...`) attached as `file` session resources. */
63+
files?: Array<{ fileId: string; mountPath?: string }>
6264
/** Arbitrary session metadata (wire name: `metadata`). */
6365
sessionParameters?: Record<string, string>
6466
}
@@ -67,6 +69,12 @@ export interface CreateSessionResult {
6769
id: string
6870
}
6971

72+
/** Cumulative token usage returned on the session resource. */
73+
export interface SessionUsage {
74+
inputTokens?: number
75+
outputTokens?: number
76+
}
77+
7078
/**
7179
* Standard header set for Managed Agents calls. `beta` overrides the default
7280
* managed-agents beta for memory-store endpoints. Only ONE beta value is ever
@@ -102,15 +110,20 @@ export function buildSessionCreatePayload(input: CreateSessionInput): Record<str
102110

103111
const resources: Array<Record<string, unknown>> = []
104112
if (input.memoryStoreId) {
105-
resources.push({
113+
const memory: Record<string, unknown> = {
106114
type: 'memory_store',
107115
memory_store_id: input.memoryStoreId,
108116
access: input.memoryAccess ?? 'read_write',
109-
})
117+
}
118+
if (input.memoryInstructions) memory.instructions = input.memoryInstructions
119+
resources.push(memory)
110120
}
111-
if (input.fileIds && input.fileIds.length > 0) {
112-
for (const fileId of input.fileIds) {
113-
if (fileId) resources.push({ type: 'file', file_id: fileId })
121+
if (input.files && input.files.length > 0) {
122+
for (const file of input.files) {
123+
if (!file.fileId) continue
124+
const entry: Record<string, unknown> = { type: 'file', file_id: file.fileId }
125+
if (file.mountPath) entry.mount_path = file.mountPath
126+
resources.push(entry)
114127
}
115128
}
116129
if (resources.length > 0) payload.resources = resources
@@ -273,3 +286,30 @@ export async function managedAgentsList<T>(
273286
beta: input.beta,
274287
})
275288
}
289+
290+
/**
291+
* GET /v1/sessions/{id} — retrieves the session resource. Used after a run
292+
* completes to surface cumulative token usage. Returns `null` on any error so
293+
* the caller can treat usage as best-effort without failing the run.
294+
*/
295+
export async function getSessionUsage(
296+
input: SessionAuth & { sessionId: string }
297+
): Promise<SessionUsage | null> {
298+
try {
299+
const resp = await fetch(`${ANTHROPIC_API_BASE}/v1/sessions/${input.sessionId}`, {
300+
method: 'GET',
301+
headers: managedAgentsHeaders(input.apiKey),
302+
signal: input.signal,
303+
})
304+
if (!resp.ok) return null
305+
const body = (await resp.json()) as {
306+
usage?: { input_tokens?: unknown; output_tokens?: unknown }
307+
}
308+
const usage: SessionUsage = {}
309+
if (typeof body.usage?.input_tokens === 'number') usage.inputTokens = body.usage.input_tokens
310+
if (typeof body.usage?.output_tokens === 'number') usage.outputTokens = body.usage.output_tokens
311+
return usage
312+
} catch {
313+
return null
314+
}
315+
}

0 commit comments

Comments
 (0)