Skip to content

Commit 2b629cb

Browse files
feat(api): publish agent tool input schema
1 parent 27a608e commit 2b629cb

6 files changed

Lines changed: 812 additions & 15 deletions

File tree

apps/docs/openapi-v2-workflows.json

Lines changed: 348 additions & 9 deletions
Large diffs are not rendered by default.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
v2AgentToolInputSchema,
7+
v2ApplyWorkflowOperationsBodySchema,
8+
} from '@/lib/api/contracts/v2/workflows'
9+
10+
describe('v2AgentToolInputSchema', () => {
11+
it('accepts catalog integration, custom-tool reference, and MCP tool shapes', () => {
12+
const tools = [
13+
{
14+
type: 'cloudwatch',
15+
operation: 'describe_alarm_history',
16+
usageControl: 'auto',
17+
params: { region: 'us-east-1' },
18+
},
19+
{
20+
type: 'custom-tool',
21+
customToolId: 'cst_123',
22+
usageControl: 'force',
23+
},
24+
{
25+
type: 'mcp',
26+
params: { serverId: 'mcp_123', toolName: 'search_docs', collection: 'incidents' },
27+
usageControl: 'none',
28+
},
29+
]
30+
31+
expect(v2AgentToolInputSchema.parse(tools)).toEqual(tools)
32+
})
33+
34+
it('keeps the legacy inline custom-tool shape available for workflow round trips', () => {
35+
const tools = [
36+
{
37+
type: 'custom-tool',
38+
schema: {
39+
type: 'function',
40+
function: {
41+
name: 'lookup_incident',
42+
description: 'Look up an incident.',
43+
parameters: { type: 'object', properties: { id: { type: 'string' } } },
44+
},
45+
},
46+
code: 'return params.id',
47+
},
48+
]
49+
50+
expect(v2AgentToolInputSchema.parse(tools)).toEqual(tools)
51+
})
52+
53+
it.each([
54+
[{ type: 'custom-tool', usageControl: 'auto' }],
55+
[{ type: 'mcp', params: { serverId: 'mcp_123' }, usageControl: 'auto' }],
56+
[{ type: 'slack', operation: 'send', usageControl: 'sometimes' }],
57+
])('rejects a malformed reserved tool shape', (tools) => {
58+
expect(v2AgentToolInputSchema.safeParse(tools).success).toBe(false)
59+
})
60+
})
61+
62+
describe('workflow operation Agent tools contract', () => {
63+
it('publishes and validates tools under params.inputs without closing other catalog inputs', () => {
64+
const body = {
65+
operations: [
66+
{
67+
operation_type: 'add',
68+
block_id: 'triage',
69+
params: {
70+
type: 'agent',
71+
name: 'Triage',
72+
inputs: {
73+
model: 'gpt-5',
74+
tools: [
75+
{
76+
type: 'cloudwatch',
77+
operation: 'describe_alarms',
78+
params: { region: 'us-west-2' },
79+
usageControl: 'auto',
80+
futureMetadata: { preserved: true },
81+
},
82+
],
83+
},
84+
futureOperationSetting: true,
85+
},
86+
},
87+
],
88+
}
89+
90+
expect(v2ApplyWorkflowOperationsBodySchema.parse(body)).toEqual({
91+
...body,
92+
atomic: false,
93+
layout: 'targeted',
94+
})
95+
})
96+
97+
it('rejects malformed Agent tools before the edit engine runs', () => {
98+
const parsed = v2ApplyWorkflowOperationsBodySchema.safeParse({
99+
operations: [
100+
{
101+
operation_type: 'add',
102+
block_id: 'triage',
103+
params: {
104+
type: 'agent',
105+
name: 'Triage',
106+
inputs: { tools: [{ type: 'mcp', params: { serverId: 'mcp_123' } }] },
107+
},
108+
},
109+
],
110+
})
111+
112+
expect(parsed.success).toBe(false)
113+
})
114+
})

apps/sim/lib/api/contracts/v2/openapi/workflows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ const declaredRoutes = [
364364
workflowOperation({
365365
operationId: 'applyWorkflowOperations',
366366
summary: 'Apply Workflow Operations',
367-
description: `Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.\n\nBest-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in \`skipped\`, each with a machine-readable \`type\`; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. \`deferred\` is **not** a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.\n\nSet \`atomic\` to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers \`409\` with \`error.details.code: "OPERATIONS_NOT_APPLIED"\`, the same \`skipped\` array, and a \`droppedInputs\` array, having persisted nothing.\n\nA \`block_id\` you supply on an \`add\` or \`insert_into_subflow\` is only a label unless it is already a UUID: the engine mints one and returns the pairing in \`mintedBlockIds\`. References between operations in the same batch are remapped for you, so \`triage\` can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.\n\nOperation \`params\` is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: \`inputs\` keyed by sub-block id, with \`retry\`, \`triggerMode\` and \`advancedMode\` beside it rather than inside it, and \`connections\` keyed by source handle. \`GET /blocks/{blockId}\` publishes the inputs a given block type accepts.\n\n\`lint\` is advisory and never blocks the write. \`lint.fieldIssues\` is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and \`lint.unresolvedReferences\` names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only \`inputValidationErrors\` lists inputs that were actually dropped.\n\nAs with \`PUT /workflows/{workflowId}/state\`, this changes only the draft; deploy to publish it. ${WORKSPACE_API_KEY_DENIED}\n\nSet \`?dryRun=true\` to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and \`lint\` findings the committed write would, with \`dryRun: true\` — but \`needsRedeployment\` describes the state before the write, and warnings raised by persistence itself are necessarily absent.`,
367+
description: `Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.\n\nBest-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in \`skipped\`, each with a machine-readable \`type\`; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. \`deferred\` is **not** a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.\n\nSet \`atomic\` to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers \`409\` with \`error.details.code: "OPERATIONS_NOT_APPLIED"\`, the same \`skipped\` array, and a \`droppedInputs\` array, having persisted nothing.\n\nA \`block_id\` you supply on an \`add\` or \`insert_into_subflow\` is only a label unless it is already a UUID: the engine mints one and returns the pairing in \`mintedBlockIds\`. References between operations in the same batch are remapped for you, so \`triage\` can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.\n\nOperation \`params\` is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: \`inputs\` keyed by sub-block id, with \`retry\`, \`triggerMode\` and \`advancedMode\` beside it rather than inside it, and \`connections\` keyed by source handle. \`GET /blocks/{blockId}\` publishes the inputs a given block type accepts. The Agent block’s \`inputs.tools\` value is the important exception to that open catalog shape: it is published here as the named \`AgentToolInput\` union, covering catalog integrations, workspace custom tools, and MCP tools.\n\n\`lint\` is advisory and never blocks the write. \`lint.fieldIssues\` is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and \`lint.unresolvedReferences\` names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only \`inputValidationErrors\` lists inputs that were actually dropped.\n\nAs with \`PUT /workflows/{workflowId}/state\`, this changes only the draft; deploy to publish it. ${WORKSPACE_API_KEY_DENIED}\n\nSet \`?dryRun=true\` to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and \`lint\` findings the committed write would, with \`dryRun: true\` — but \`needsRedeployment\` describes the state before the write, and warnings raised by persistence itself are necessarily absent.`,
368368
errors: RESOURCE_MUTATION_ERRORS,
369369
success: jsonSuccess('The batch was applied.'),
370370
}),

0 commit comments

Comments
 (0)