@decocms/bindings 1.0.1-alpha.4 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +8 -11
- package/src/core/binder.ts +15 -76
- package/src/core/client/index.ts +10 -0
- package/src/core/client/mcp-client.ts +18 -5
- package/src/core/client/mcp.ts +48 -11
- package/src/core/client/proxy.ts +64 -48
- package/src/index.ts +57 -0
- package/src/well-known/assistant.ts +87 -0
- package/src/well-known/collections.ts +84 -99
- package/src/well-known/event-bus.ts +454 -0
- package/src/well-known/event-subscriber.ts +259 -0
- package/src/well-known/language-model.ts +216 -5
- package/src/well-known/mcp.ts +2 -1
- package/src/well-known/prompt.ts +110 -0
- package/src/well-known/registry.ts +128 -0
- package/src/well-known/workflow.ts +669 -0
- package/test/index.test.ts +3 -2
- package/test/mcp.test.ts +1 -1
- package/src/core/subset.ts +0 -514
- package/src/well-known/agent.ts +0 -60
- package/vitest.config.ts +0 -8
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows Well-Known Binding
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface for workflow providers.
|
|
5
|
+
* Any MCP that implements this binding can expose configurable workflows,
|
|
6
|
+
* executions, step results, and events via collection bindings.
|
|
7
|
+
*
|
|
8
|
+
* This binding uses collection bindings for LIST and GET operations (read-only).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { type Binder, bindingClient, type ToolBinder } from "../core/binder";
|
|
13
|
+
import {
|
|
14
|
+
BaseCollectionEntitySchema,
|
|
15
|
+
createCollectionBindings,
|
|
16
|
+
} from "./collections";
|
|
17
|
+
export const ToolCallActionSchema = z.object({
|
|
18
|
+
toolName: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Name of the tool to invoke on that connection"),
|
|
21
|
+
transformCode: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(`Pure TypeScript function for data transformation of the tool call result. Must be a TypeScript file that declares the Output interface and exports a default function: \`interface Output { ... } export default async function(input): Output { ... }\`
|
|
25
|
+
The input will match with the tool call outputSchema. If transformCode is not provided, the tool call result will be used as the step output.
|
|
26
|
+
Providing an transformCode is recommended because it both allows you to transform the data and validate it against a JSON Schema - tools are ephemeral and may return unexpected data.`),
|
|
27
|
+
});
|
|
28
|
+
export type ToolCallAction = z.infer<typeof ToolCallActionSchema>;
|
|
29
|
+
|
|
30
|
+
export const CodeActionSchema = z.object({
|
|
31
|
+
code: z.string().describe(
|
|
32
|
+
`Pure TypeScript function for data transformation. Useful to merge data from multiple steps and transform it. Must be a TypeScript file that declares the Output interface and exports a default function: \`interface Output { ... } export default async function(input): Output { ... }\`
|
|
33
|
+
The input is the resolved value of the references in the input field. Example:
|
|
34
|
+
{
|
|
35
|
+
"input": {
|
|
36
|
+
"name": "@Step_1.name",
|
|
37
|
+
"age": "@Step_2.age"
|
|
38
|
+
},
|
|
39
|
+
"code": "export default function(input): Output { return { result: \`\${input.name} is \${input.age} years old.\` } }"
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
export type CodeAction = z.infer<typeof CodeActionSchema>;
|
|
45
|
+
|
|
46
|
+
export const WaitForSignalActionSchema = z.object({
|
|
47
|
+
signalName: z
|
|
48
|
+
.string()
|
|
49
|
+
.describe(
|
|
50
|
+
"Signal name to wait for (e.g., 'approval'). Execution pauses until SEND_SIGNAL is called with this name.",
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
export type WaitForSignalAction = z.infer<typeof WaitForSignalActionSchema>;
|
|
54
|
+
|
|
55
|
+
export const StepActionSchema = z.union([
|
|
56
|
+
ToolCallActionSchema.describe("Call an external tool via MCP connection. "),
|
|
57
|
+
CodeActionSchema.describe(
|
|
58
|
+
"Run pure TypeScript code for data transformation. Useful to merge data from multiple steps and transform it.",
|
|
59
|
+
),
|
|
60
|
+
// WaitForSignalActionSchema.describe(
|
|
61
|
+
// "Pause execution until an external signal is received (human-in-the-loop)",
|
|
62
|
+
// ),
|
|
63
|
+
]);
|
|
64
|
+
export type StepAction = z.infer<typeof StepActionSchema>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Step Config Schema - Optional configuration for retry, timeout, and looping
|
|
68
|
+
*/
|
|
69
|
+
export const StepConfigSchema = z.object({
|
|
70
|
+
maxAttempts: z
|
|
71
|
+
.number()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Max retry attempts on failure (default: 1, no retries)"),
|
|
74
|
+
backoffMs: z
|
|
75
|
+
.number()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Initial delay between retries in ms (doubles each attempt)"),
|
|
78
|
+
timeoutMs: z
|
|
79
|
+
.number()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Max execution time in ms before step fails (default: 30000)"),
|
|
82
|
+
});
|
|
83
|
+
export type StepConfig = z.infer<typeof StepConfigSchema>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Step Schema - A single unit of work in a workflow
|
|
87
|
+
*
|
|
88
|
+
* Action types:
|
|
89
|
+
* - Tool call: Invoke an external tool via MCP connection
|
|
90
|
+
* - Code: Run pure TypeScript for data transformation
|
|
91
|
+
* - Wait for signal: Pause until external input (human-in-the-loop)
|
|
92
|
+
*
|
|
93
|
+
* Data flow uses @ref syntax:
|
|
94
|
+
* - @input.field → workflow input
|
|
95
|
+
* - @stepName.field → output from a previous step
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
type JsonSchema = {
|
|
99
|
+
type?: string;
|
|
100
|
+
properties?: Record<string, unknown>;
|
|
101
|
+
required?: string[];
|
|
102
|
+
description?: string;
|
|
103
|
+
additionalProperties?: boolean;
|
|
104
|
+
additionalItems?: boolean;
|
|
105
|
+
items?: JsonSchema;
|
|
106
|
+
};
|
|
107
|
+
const JsonSchemaSchema: z.ZodType<JsonSchema> = z.lazy(() =>
|
|
108
|
+
z
|
|
109
|
+
.object({
|
|
110
|
+
type: z.string().optional(),
|
|
111
|
+
properties: z.record(z.unknown()).optional(),
|
|
112
|
+
required: z.array(z.string()).optional(),
|
|
113
|
+
description: z.string().optional(),
|
|
114
|
+
additionalProperties: z.boolean().optional(),
|
|
115
|
+
additionalItems: z.boolean().optional(),
|
|
116
|
+
items: JsonSchemaSchema.optional(),
|
|
117
|
+
})
|
|
118
|
+
.passthrough(),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export const StepSchema = z.object({
|
|
122
|
+
name: z
|
|
123
|
+
.string()
|
|
124
|
+
.min(1)
|
|
125
|
+
.describe(
|
|
126
|
+
"Unique identifier for this step. Other steps reference its output as @name.field",
|
|
127
|
+
),
|
|
128
|
+
description: z.string().optional().describe("What this step does"),
|
|
129
|
+
action: StepActionSchema,
|
|
130
|
+
input: z
|
|
131
|
+
.record(z.unknown())
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(
|
|
134
|
+
"Data passed to the action. Use @ref for dynamic values: @input.field (workflow input), @stepName.field (previous step output), @item/@index (loop context). Example: { 'userId': '@input.user_id', 'data': '@fetch.result' }",
|
|
135
|
+
),
|
|
136
|
+
outputSchema: JsonSchemaSchema.optional().describe(
|
|
137
|
+
"Optional JSON Schema describing the expected output of the step.",
|
|
138
|
+
),
|
|
139
|
+
config: StepConfigSchema.optional().describe("Retry and timeout settings"),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export type Step = z.infer<typeof StepSchema>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Workflow Execution Status
|
|
146
|
+
*
|
|
147
|
+
* States:
|
|
148
|
+
* - pending: Created but not started
|
|
149
|
+
* - running: Currently executing
|
|
150
|
+
* - completed: Successfully finished
|
|
151
|
+
* - cancelled: Manually cancelled
|
|
152
|
+
*/
|
|
153
|
+
|
|
154
|
+
const WorkflowExecutionStatusEnum = z
|
|
155
|
+
.enum(["enqueued", "running", "success", "error", "failed", "cancelled"])
|
|
156
|
+
.default("enqueued");
|
|
157
|
+
export type WorkflowExecutionStatus = z.infer<
|
|
158
|
+
typeof WorkflowExecutionStatusEnum
|
|
159
|
+
>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Workflow Execution Schema
|
|
163
|
+
*
|
|
164
|
+
* Includes lock columns and retry tracking.
|
|
165
|
+
*/
|
|
166
|
+
export const WorkflowExecutionSchema = BaseCollectionEntitySchema.extend({
|
|
167
|
+
steps: z
|
|
168
|
+
.array(StepSchema)
|
|
169
|
+
.describe("Steps that make up the workflow")
|
|
170
|
+
.describe("Workflow that was executed"),
|
|
171
|
+
gateway_id: z
|
|
172
|
+
.string()
|
|
173
|
+
.describe("ID of the gateway that will be used to execute the workflow"),
|
|
174
|
+
status: WorkflowExecutionStatusEnum.describe(
|
|
175
|
+
"Current status of the workflow execution",
|
|
176
|
+
),
|
|
177
|
+
input: z
|
|
178
|
+
.record(z.unknown())
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Input data for the workflow execution"),
|
|
181
|
+
output: z
|
|
182
|
+
.unknown()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe("Output data for the workflow execution"),
|
|
185
|
+
completed_at_epoch_ms: z
|
|
186
|
+
.number()
|
|
187
|
+
.nullish()
|
|
188
|
+
.describe("Timestamp of when the workflow execution completed"),
|
|
189
|
+
start_at_epoch_ms: z
|
|
190
|
+
.number()
|
|
191
|
+
.nullish()
|
|
192
|
+
.describe("Timestamp of when the workflow execution started or will start"),
|
|
193
|
+
timeout_ms: z
|
|
194
|
+
.number()
|
|
195
|
+
.nullish()
|
|
196
|
+
.describe("Timeout in milliseconds for the workflow execution"),
|
|
197
|
+
deadline_at_epoch_ms: z
|
|
198
|
+
.number()
|
|
199
|
+
.nullish()
|
|
200
|
+
.describe(
|
|
201
|
+
"Deadline for the workflow execution - when the workflow execution will be cancelled if it is not completed. This is read-only and is set by the workflow engine when an execution is created.",
|
|
202
|
+
),
|
|
203
|
+
error: z
|
|
204
|
+
.unknown()
|
|
205
|
+
.describe("Error that occurred during the workflow execution"),
|
|
206
|
+
});
|
|
207
|
+
export type WorkflowExecution = z.infer<typeof WorkflowExecutionSchema>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Event Type Enum
|
|
211
|
+
*
|
|
212
|
+
* Event types for the unified events table:
|
|
213
|
+
* - signal: External signal (human-in-the-loop)
|
|
214
|
+
* - timer: Durable sleep wake-up
|
|
215
|
+
* - message: Inter-workflow communication (send/recv)
|
|
216
|
+
* - output: Published value (setEvent/getEvent)
|
|
217
|
+
* - step_started: Observability - step began
|
|
218
|
+
* - step_completed: Observability - step finished
|
|
219
|
+
* - workflow_started: Workflow began execution
|
|
220
|
+
* - workflow_completed: Workflow finished
|
|
221
|
+
*/
|
|
222
|
+
export const EventTypeEnum = z.enum([
|
|
223
|
+
"signal",
|
|
224
|
+
"timer",
|
|
225
|
+
"message",
|
|
226
|
+
"output",
|
|
227
|
+
"step_started",
|
|
228
|
+
"step_completed",
|
|
229
|
+
"workflow_started",
|
|
230
|
+
"workflow_completed",
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
export type EventType = z.infer<typeof EventTypeEnum>;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Workflow Event Schema
|
|
237
|
+
*
|
|
238
|
+
* Unified events table for signals, timers, messages, and observability.
|
|
239
|
+
*/
|
|
240
|
+
export const WorkflowEventSchema = BaseCollectionEntitySchema.extend({
|
|
241
|
+
execution_id: z.string(),
|
|
242
|
+
type: EventTypeEnum,
|
|
243
|
+
name: z.string().nullish(),
|
|
244
|
+
payload: z.unknown().optional(),
|
|
245
|
+
visible_at: z.number().nullish(),
|
|
246
|
+
consumed_at: z.number().nullish(),
|
|
247
|
+
source_execution_id: z.string().nullish(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
export type WorkflowEvent = z.infer<typeof WorkflowEventSchema>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Workflow Schema - A sequence of steps that execute with data flowing between them
|
|
254
|
+
*
|
|
255
|
+
* Key concepts:
|
|
256
|
+
* - Steps run in parallel unless they reference each other via @ref
|
|
257
|
+
* - Use @ref to wire data: @input.field, @stepName.field, @item (in loops)
|
|
258
|
+
* - Execution order is auto-determined from @ref dependencies
|
|
259
|
+
*
|
|
260
|
+
* Example: 2 parallel fetches + 1 merge step
|
|
261
|
+
* {
|
|
262
|
+
* "title": "Fetch and Merge",
|
|
263
|
+
* "steps": [
|
|
264
|
+
* { "name": "fetch_users", "action": { "connectionId": "api", "toolName": "getUsers" } },
|
|
265
|
+
* { "name": "fetch_orders", "action": { "connectionId": "api", "toolName": "getOrders" } },
|
|
266
|
+
* { "name": "merge", "action": { "code": "..." }, "input": { "users": "@fetch_users.data", "orders": "@fetch_orders.data" } }
|
|
267
|
+
* ]
|
|
268
|
+
* }
|
|
269
|
+
* → fetch_users and fetch_orders run in parallel; merge waits for both
|
|
270
|
+
*/
|
|
271
|
+
export const WorkflowSchema = BaseCollectionEntitySchema.extend({
|
|
272
|
+
description: z
|
|
273
|
+
.string()
|
|
274
|
+
.optional()
|
|
275
|
+
.describe("Human-readable summary of what this workflow does"),
|
|
276
|
+
|
|
277
|
+
steps: z
|
|
278
|
+
.array(StepSchema)
|
|
279
|
+
.describe(
|
|
280
|
+
"Ordered list of steps. Execution order is auto-determined by @ref dependencies: steps with no @ref dependencies run in parallel; steps referencing @stepName wait for that step to complete.",
|
|
281
|
+
),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export type Workflow = z.infer<typeof WorkflowSchema>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* WORKFLOW Collection Binding
|
|
288
|
+
*
|
|
289
|
+
* Collection bindings for workflows (read-only).
|
|
290
|
+
* Provides LIST and GET operations for workflows.
|
|
291
|
+
*/
|
|
292
|
+
export const WORKFLOWS_COLLECTION_BINDING = createCollectionBindings(
|
|
293
|
+
"workflow",
|
|
294
|
+
WorkflowSchema,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const DEFAULT_STEP_CONFIG: StepConfig = {
|
|
298
|
+
maxAttempts: 1,
|
|
299
|
+
timeoutMs: 30000,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// export const DEFAULT_WAIT_FOR_SIGNAL_STEP: Omit<Step, "name"> = {
|
|
303
|
+
// action: {
|
|
304
|
+
// signalName: "approve_output",
|
|
305
|
+
// },
|
|
306
|
+
// outputSchema: {
|
|
307
|
+
// type: "object",
|
|
308
|
+
// properties: {
|
|
309
|
+
// approved: {
|
|
310
|
+
// type: "boolean",
|
|
311
|
+
// description: "Whether the output was approved",
|
|
312
|
+
// },
|
|
313
|
+
// },
|
|
314
|
+
// },
|
|
315
|
+
// };
|
|
316
|
+
export const DEFAULT_TOOL_STEP: Omit<Step, "name"> = {
|
|
317
|
+
action: {
|
|
318
|
+
toolName: "LLM_DO_GENERATE",
|
|
319
|
+
transformCode: `
|
|
320
|
+
interface Input {
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
export default function(input) { return input.result }`,
|
|
324
|
+
},
|
|
325
|
+
input: {
|
|
326
|
+
modelId: "anthropic/claude-4.5-haiku",
|
|
327
|
+
prompt: "Write a haiku about the weather.",
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
config: DEFAULT_STEP_CONFIG,
|
|
331
|
+
outputSchema: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
result: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "The result of the step",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
export const DEFAULT_CODE_STEP: Step = {
|
|
342
|
+
name: "Initial Step",
|
|
343
|
+
action: {
|
|
344
|
+
code: `
|
|
345
|
+
interface Input {
|
|
346
|
+
example: string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
interface Output {
|
|
350
|
+
result: unknown;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export default async function(input: Input): Promise<Output> {
|
|
354
|
+
return {
|
|
355
|
+
result: input.example
|
|
356
|
+
}
|
|
357
|
+
}`,
|
|
358
|
+
},
|
|
359
|
+
config: DEFAULT_STEP_CONFIG,
|
|
360
|
+
outputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
result: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "The result of the step",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
required: ["result"],
|
|
369
|
+
description:
|
|
370
|
+
"The output of the step. This is a JSON Schema describing the expected output of the step.",
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export const createDefaultWorkflow = (id?: string): Workflow => ({
|
|
375
|
+
id: id || crypto.randomUUID(),
|
|
376
|
+
title: "Default Workflow",
|
|
377
|
+
description: "The default workflow for the toolkit",
|
|
378
|
+
steps: [DEFAULT_CODE_STEP],
|
|
379
|
+
created_at: new Date().toISOString(),
|
|
380
|
+
updated_at: new Date().toISOString(),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
export const WORKFLOW_EXECUTIONS_COLLECTION_BINDING = createCollectionBindings(
|
|
384
|
+
"workflow_execution",
|
|
385
|
+
WorkflowExecutionSchema,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* WORKFLOWS Binding
|
|
390
|
+
*
|
|
391
|
+
* Defines the interface for workflow providers.
|
|
392
|
+
* Any MCP that implements this binding can provide configurable workflows.
|
|
393
|
+
*
|
|
394
|
+
* Required tools:
|
|
395
|
+
* - COLLECTION_WORKFLOW_LIST: List available workflows with their configurations
|
|
396
|
+
* - COLLECTION_WORKFLOW_GET: Get a single workflow by ID (includes steps and triggers)
|
|
397
|
+
*/
|
|
398
|
+
export const WORKFLOW_COLLECTIONS_BINDINGS = [
|
|
399
|
+
...WORKFLOWS_COLLECTION_BINDING,
|
|
400
|
+
...WORKFLOW_EXECUTIONS_COLLECTION_BINDING,
|
|
401
|
+
] as const satisfies Binder;
|
|
402
|
+
|
|
403
|
+
export const WORKFLOW_BINDING = [
|
|
404
|
+
...WORKFLOW_COLLECTIONS_BINDINGS,
|
|
405
|
+
] satisfies ToolBinder[];
|
|
406
|
+
|
|
407
|
+
export const WorkflowBinding = bindingClient(WORKFLOW_BINDING);
|
|
408
|
+
|
|
409
|
+
export const WORKFLOW_EXECUTION_BINDING = createCollectionBindings(
|
|
410
|
+
"workflow_execution",
|
|
411
|
+
WorkflowExecutionSchema,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* DAG (Directed Acyclic Graph) utilities for workflow step execution
|
|
416
|
+
*
|
|
417
|
+
* Pure TypeScript functions for analyzing step dependencies and grouping
|
|
418
|
+
* steps into execution levels for parallel execution.
|
|
419
|
+
*
|
|
420
|
+
* Can be used in both frontend (visualization) and backend (execution).
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Minimal step interface for DAG computation.
|
|
425
|
+
* This allows the DAG utilities to work with any step-like object.
|
|
426
|
+
*/
|
|
427
|
+
export interface DAGStep {
|
|
428
|
+
name: string;
|
|
429
|
+
input?: unknown;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Extract all @ref references from a value recursively.
|
|
434
|
+
* Finds patterns like @stepName or @stepName.field
|
|
435
|
+
*
|
|
436
|
+
* @param input - Any value that might contain @ref strings
|
|
437
|
+
* @returns Array of unique reference names (without @ prefix)
|
|
438
|
+
*/
|
|
439
|
+
export function getAllRefs(input: unknown): string[] {
|
|
440
|
+
const refs: string[] = [];
|
|
441
|
+
|
|
442
|
+
function traverse(value: unknown) {
|
|
443
|
+
if (typeof value === "string") {
|
|
444
|
+
const matches = value.match(/@(\w+)/g);
|
|
445
|
+
if (matches) {
|
|
446
|
+
refs.push(...matches.map((m) => m.substring(1))); // Remove @ prefix
|
|
447
|
+
}
|
|
448
|
+
} else if (Array.isArray(value)) {
|
|
449
|
+
value.forEach(traverse);
|
|
450
|
+
} else if (typeof value === "object" && value !== null) {
|
|
451
|
+
Object.values(value).forEach(traverse);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
traverse(input);
|
|
456
|
+
return [...new Set(refs)].sort(); // Dedupe and sort for consistent results
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get the dependencies of a step (other steps it references).
|
|
461
|
+
* Only returns dependencies that are actual step names (filters out built-ins like "item", "index", "input").
|
|
462
|
+
*
|
|
463
|
+
* @param step - The step to analyze
|
|
464
|
+
* @param allStepNames - Set of all step names in the workflow
|
|
465
|
+
* @returns Array of step names this step depends on
|
|
466
|
+
*/
|
|
467
|
+
export function getStepDependencies(
|
|
468
|
+
step: DAGStep,
|
|
469
|
+
allStepNames: Set<string>,
|
|
470
|
+
): string[] {
|
|
471
|
+
const deps: string[] = [];
|
|
472
|
+
|
|
473
|
+
function traverse(value: unknown) {
|
|
474
|
+
if (typeof value === "string") {
|
|
475
|
+
// Match @stepName or @stepName.something patterns
|
|
476
|
+
const matches = value.match(/@(\w+)/g);
|
|
477
|
+
if (matches) {
|
|
478
|
+
for (const match of matches) {
|
|
479
|
+
const refName = match.substring(1); // Remove @
|
|
480
|
+
// Only count as dependency if it references another step
|
|
481
|
+
// (not "item", "index", "input" from forEach or workflow input)
|
|
482
|
+
if (allStepNames.has(refName)) {
|
|
483
|
+
deps.push(refName);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} else if (Array.isArray(value)) {
|
|
488
|
+
value.forEach(traverse);
|
|
489
|
+
} else if (typeof value === "object" && value !== null) {
|
|
490
|
+
Object.values(value).forEach(traverse);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
traverse(step.input);
|
|
495
|
+
return [...new Set(deps)];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Build edges for the DAG: [fromStep, toStep][]
|
|
500
|
+
*/
|
|
501
|
+
export function buildDagEdges(steps: Step[]): [string, string][] {
|
|
502
|
+
const stepNames = new Set(steps.map((s) => s.name));
|
|
503
|
+
const edges: [string, string][] = [];
|
|
504
|
+
|
|
505
|
+
for (const step of steps) {
|
|
506
|
+
const deps = getStepDependencies(step, stepNames);
|
|
507
|
+
for (const dep of deps) {
|
|
508
|
+
edges.push([dep, step.name]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return edges;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Compute topological levels for all steps.
|
|
517
|
+
* Level 0 = no dependencies on other steps
|
|
518
|
+
* Level N = depends on at least one step at level N-1
|
|
519
|
+
*
|
|
520
|
+
* @param steps - Array of steps to analyze
|
|
521
|
+
* @returns Map from step name to level number
|
|
522
|
+
*/
|
|
523
|
+
export function computeStepLevels<T extends DAGStep>(
|
|
524
|
+
steps: T[],
|
|
525
|
+
): Map<string, number> {
|
|
526
|
+
const stepNames = new Set(steps.map((s) => s.name));
|
|
527
|
+
const levels = new Map<string, number>();
|
|
528
|
+
|
|
529
|
+
// Build dependency map
|
|
530
|
+
const depsMap = new Map<string, string[]>();
|
|
531
|
+
for (const step of steps) {
|
|
532
|
+
depsMap.set(step.name, getStepDependencies(step, stepNames));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Compute level for each step (with memoization)
|
|
536
|
+
function getLevel(stepName: string, visited: Set<string>): number {
|
|
537
|
+
if (levels.has(stepName)) return levels.get(stepName)!;
|
|
538
|
+
if (visited.has(stepName)) return 0; // Cycle detection
|
|
539
|
+
|
|
540
|
+
visited.add(stepName);
|
|
541
|
+
const deps = depsMap.get(stepName) || [];
|
|
542
|
+
|
|
543
|
+
if (deps.length === 0) {
|
|
544
|
+
levels.set(stepName, 0);
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const maxDepLevel = Math.max(...deps.map((d) => getLevel(d, visited)));
|
|
549
|
+
const level = maxDepLevel + 1;
|
|
550
|
+
levels.set(stepName, level);
|
|
551
|
+
return level;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
for (const step of steps) {
|
|
555
|
+
getLevel(step.name, new Set());
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return levels;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Group steps by their execution level.
|
|
563
|
+
* Steps at the same level have no dependencies on each other and can run in parallel.
|
|
564
|
+
*
|
|
565
|
+
* @param steps - Array of steps to group
|
|
566
|
+
* @returns Array of step arrays, where index is the level
|
|
567
|
+
*/
|
|
568
|
+
export function groupStepsByLevel<T extends DAGStep>(steps: T[]): T[][] {
|
|
569
|
+
const levels = computeStepLevels(steps);
|
|
570
|
+
const maxLevel = Math.max(...Array.from(levels.values()), -1);
|
|
571
|
+
|
|
572
|
+
const grouped: T[][] = [];
|
|
573
|
+
for (let level = 0; level <= maxLevel; level++) {
|
|
574
|
+
const stepsAtLevel = steps.filter((s) => levels.get(s.name) === level);
|
|
575
|
+
if (stepsAtLevel.length > 0) {
|
|
576
|
+
grouped.push(stepsAtLevel);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return grouped;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Get the dependency signature for a step (for grouping steps with same deps).
|
|
585
|
+
*
|
|
586
|
+
* @param step - The step to get signature for
|
|
587
|
+
* @returns Comma-separated sorted list of dependencies
|
|
588
|
+
*/
|
|
589
|
+
export function getRefSignature(step: DAGStep): string {
|
|
590
|
+
const inputRefs = getAllRefs(step.input);
|
|
591
|
+
const allRefs = [...new Set([...inputRefs])].sort();
|
|
592
|
+
return allRefs.join(",");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Build a dependency graph for visualization.
|
|
597
|
+
* Returns edges as [fromStep, toStep] pairs.
|
|
598
|
+
*
|
|
599
|
+
* @param steps - Array of steps
|
|
600
|
+
* @returns Array of [source, target] pairs representing edges
|
|
601
|
+
*/
|
|
602
|
+
export function buildDependencyEdges<T extends DAGStep>(
|
|
603
|
+
steps: T[],
|
|
604
|
+
): [string, string][] {
|
|
605
|
+
const stepNames = new Set(steps.map((s) => s.name));
|
|
606
|
+
const edges: [string, string][] = [];
|
|
607
|
+
|
|
608
|
+
for (const step of steps) {
|
|
609
|
+
const deps = getStepDependencies(step, stepNames);
|
|
610
|
+
for (const dep of deps) {
|
|
611
|
+
edges.push([dep, step.name]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return edges;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Validate that there are no cycles in the step dependencies.
|
|
620
|
+
*
|
|
621
|
+
* @param steps - Array of steps to validate
|
|
622
|
+
* @returns Object with isValid and optional error message
|
|
623
|
+
*/
|
|
624
|
+
export function validateNoCycles<T extends DAGStep>(
|
|
625
|
+
steps: T[],
|
|
626
|
+
): { isValid: boolean; error?: string } {
|
|
627
|
+
const stepNames = new Set(steps.map((s) => s.name));
|
|
628
|
+
const depsMap = new Map<string, string[]>();
|
|
629
|
+
|
|
630
|
+
for (const step of steps) {
|
|
631
|
+
depsMap.set(step.name, getStepDependencies(step, stepNames));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const visited = new Set<string>();
|
|
635
|
+
const recursionStack = new Set<string>();
|
|
636
|
+
|
|
637
|
+
function hasCycle(stepName: string, path: string[]): string[] | null {
|
|
638
|
+
if (recursionStack.has(stepName)) {
|
|
639
|
+
return [...path, stepName];
|
|
640
|
+
}
|
|
641
|
+
if (visited.has(stepName)) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
visited.add(stepName);
|
|
646
|
+
recursionStack.add(stepName);
|
|
647
|
+
|
|
648
|
+
const deps = depsMap.get(stepName) || [];
|
|
649
|
+
for (const dep of deps) {
|
|
650
|
+
const cycle = hasCycle(dep, [...path, stepName]);
|
|
651
|
+
if (cycle) return cycle;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
recursionStack.delete(stepName);
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
for (const step of steps) {
|
|
659
|
+
const cycle = hasCycle(step.name, []);
|
|
660
|
+
if (cycle) {
|
|
661
|
+
return {
|
|
662
|
+
isValid: false,
|
|
663
|
+
error: `Circular dependency detected: ${cycle.join(" -> ")}`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return { isValid: true };
|
|
669
|
+
}
|
package/test/index.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
createBindingChecker,
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type ToolBinder,
|
|
7
7
|
} from "../src/index";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Skipping tests for now
|
|
10
|
+
describe.skip("@decocms/bindings", () => {
|
|
10
11
|
describe("ToolBinder type", () => {
|
|
11
12
|
it("should define a valid tool binder", () => {
|
|
12
13
|
const toolBinder: ToolBinder = {
|
package/test/mcp.test.ts
CHANGED