@decocms/bindings 1.4.0 → 1.4.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/package.json +6 -5
- package/src/core/binder.ts +1 -21
- package/src/core/client/index.ts +0 -1
- package/src/core/client/mcp-client.ts +0 -35
- package/src/core/client/mcp.ts +7 -15
- package/src/core/client/proxy.ts +1 -10
- package/src/core/plugins.ts +10 -0
- package/src/core/server-plugin.ts +2 -2
- package/src/index.ts +22 -0
- package/src/well-known/brand.ts +151 -0
- package/src/well-known/language-model.ts +0 -1
- package/src/well-known/workflow.ts +80 -17
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/bindings",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "tsc --noEmit",
|
|
7
7
|
"test": "bun test"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@modelcontextprotocol/sdk": "1.
|
|
11
|
-
"@tanstack/react-router": "1.
|
|
12
|
-
"react": "^19.2.
|
|
10
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
11
|
+
"@tanstack/react-router": "1.169.2",
|
|
12
|
+
"react": "^19.2.6",
|
|
13
13
|
"zod": "^4.0.0",
|
|
14
14
|
"zod-from-json-schema": "^0.5.2"
|
|
15
15
|
},
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"./plugin-router": "./src/core/plugin-router.tsx",
|
|
33
33
|
"./ai-gateway": "./src/well-known/ai-gateway.ts",
|
|
34
34
|
"./server-plugin": "./src/core/server-plugin.ts",
|
|
35
|
-
"./trigger": "./src/well-known/trigger.ts"
|
|
35
|
+
"./trigger": "./src/well-known/trigger.ts",
|
|
36
|
+
"./brand": "./src/well-known/brand.ts"
|
|
36
37
|
},
|
|
37
38
|
"engines": {
|
|
38
39
|
"node": ">=24.0.0"
|
package/src/core/binder.ts
CHANGED
|
@@ -24,7 +24,6 @@ export interface ToolBinder<
|
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
25
|
TInput = any,
|
|
26
26
|
TReturn extends object | null | boolean = object,
|
|
27
|
-
TStreamable extends boolean = boolean,
|
|
28
27
|
> {
|
|
29
28
|
/** The name of the tool (e.g., "DECO_CHAT_CHANNELS_JOIN") */
|
|
30
29
|
name: TName;
|
|
@@ -33,12 +32,7 @@ export interface ToolBinder<
|
|
|
33
32
|
inputSchema: ZodType<TInput>;
|
|
34
33
|
|
|
35
34
|
/** Optional Zod schema for validating tool output */
|
|
36
|
-
outputSchema?:
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Whether this tool is streamable.
|
|
40
|
-
*/
|
|
41
|
-
streamable?: TStreamable;
|
|
35
|
+
outputSchema?: ZodType<TReturn>;
|
|
42
36
|
|
|
43
37
|
/**
|
|
44
38
|
* Whether this tool is optional in the binding.
|
|
@@ -103,13 +97,6 @@ export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
|
|
|
103
97
|
forClient: (client: ServerClient): MCPClientFetchStub<TDefinition> => {
|
|
104
98
|
return createMCPFetchStub<TDefinition>({
|
|
105
99
|
client,
|
|
106
|
-
streamable: binder.reduce(
|
|
107
|
-
(acc, tool) => {
|
|
108
|
-
acc[tool.name] = tool.streamable === true;
|
|
109
|
-
return acc;
|
|
110
|
-
},
|
|
111
|
-
{} as Record<string, boolean>,
|
|
112
|
-
),
|
|
113
100
|
});
|
|
114
101
|
},
|
|
115
102
|
forConnection: (
|
|
@@ -117,13 +104,6 @@ export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
|
|
|
117
104
|
): MCPClientFetchStub<TDefinition> => {
|
|
118
105
|
return createMCPFetchStub<TDefinition>({
|
|
119
106
|
connection: mcpConnection,
|
|
120
|
-
streamable: binder.reduce(
|
|
121
|
-
(acc, tool) => {
|
|
122
|
-
acc[tool.name] = tool.streamable === true;
|
|
123
|
-
return acc;
|
|
124
|
-
},
|
|
125
|
-
{} as Record<string, boolean>,
|
|
126
|
-
),
|
|
127
107
|
});
|
|
128
108
|
},
|
|
129
109
|
};
|
package/src/core/client/index.ts
CHANGED
|
@@ -47,11 +47,6 @@ export interface ServerClient {
|
|
|
47
47
|
callTool: Client["callTool"];
|
|
48
48
|
listTools: () => Promise<ListToolsResult>;
|
|
49
49
|
};
|
|
50
|
-
callStreamableTool?: (
|
|
51
|
-
tool: string,
|
|
52
|
-
args: Record<string, unknown>,
|
|
53
|
-
signal?: AbortSignal,
|
|
54
|
-
) => Promise<Response>;
|
|
55
50
|
}
|
|
56
51
|
export const createServerClient = async (
|
|
57
52
|
mcpServer: { connection: MCPConnection; name?: string },
|
|
@@ -73,36 +68,6 @@ export const createServerClient = async (
|
|
|
73
68
|
|
|
74
69
|
return {
|
|
75
70
|
client,
|
|
76
|
-
callStreamableTool: (tool, args, signal) => {
|
|
77
|
-
if (mcpServer.connection.type !== "HTTP") {
|
|
78
|
-
throw new Error("HTTP connection required");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const headers = new Headers(extraHeaders);
|
|
82
|
-
|
|
83
|
-
if (!headers.has("Authorization")) {
|
|
84
|
-
headers.set("Authorization", `Bearer ${mcpServer.connection.token}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const [key, value] of Object.entries(
|
|
88
|
-
mcpServer.connection.headers ?? {},
|
|
89
|
-
)) {
|
|
90
|
-
headers.set(key, value);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const url = new URL(mcpServer.connection.url);
|
|
94
|
-
// Trim trailing slashes from pathname, ensuring it starts with '/'
|
|
95
|
-
const trimmedPath = url.pathname.replace(/\/+$/, "") || "/";
|
|
96
|
-
url.pathname = `${trimmedPath}/call-tool/${encodeURIComponent(tool)}`;
|
|
97
|
-
|
|
98
|
-
return fetch(url.href, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
redirect: "manual",
|
|
101
|
-
body: JSON.stringify(args),
|
|
102
|
-
headers,
|
|
103
|
-
signal,
|
|
104
|
-
});
|
|
105
|
-
},
|
|
106
71
|
};
|
|
107
72
|
};
|
|
108
73
|
|
package/src/core/client/mcp.ts
CHANGED
|
@@ -3,12 +3,6 @@ import { z } from "zod";
|
|
|
3
3
|
import type { MCPConnection } from "../connection";
|
|
4
4
|
import { createMCPClientProxy } from "./proxy";
|
|
5
5
|
export type { ServerClient } from "./mcp-client";
|
|
6
|
-
export const isStreamableToolBinder = (
|
|
7
|
-
toolBinder: ToolBinder,
|
|
8
|
-
): toolBinder is ToolBinder<string, any, any, true> => {
|
|
9
|
-
return toolBinder.streamable === true;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
6
|
// Default fetcher instance with API_SERVER_URL and API_HEADERS
|
|
13
7
|
export const MCPClient = new Proxy(
|
|
14
8
|
{} as {
|
|
@@ -69,13 +63,13 @@ export type MCPClientFetchStub<TDefinition extends readonly ToolBinder[]> = {
|
|
|
69
63
|
}[]
|
|
70
64
|
>;
|
|
71
65
|
} & {
|
|
72
|
-
[K in TDefinition[number] as K["name"]]: K
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
[K in TDefinition[number] as K["name"]]: K extends ToolBinder<
|
|
67
|
+
string,
|
|
68
|
+
infer TInput,
|
|
69
|
+
infer TReturn
|
|
70
|
+
>
|
|
71
|
+
? (params: TInput, init?: RequestInit) => Promise<Awaited<TReturn>>
|
|
72
|
+
: never;
|
|
79
73
|
};
|
|
80
74
|
|
|
81
75
|
export interface MCPClientRaw {
|
|
@@ -93,7 +87,6 @@ export type JSONSchemaToZodConverter = (jsonSchema: any) => z.ZodTypeAny;
|
|
|
93
87
|
|
|
94
88
|
export interface CreateStubForClientAPIOptions {
|
|
95
89
|
client: ServerClient;
|
|
96
|
-
streamable?: Record<string, boolean>;
|
|
97
90
|
debugId?: () => string;
|
|
98
91
|
getErrorByStatusCode?: (
|
|
99
92
|
statusCode: number,
|
|
@@ -105,7 +98,6 @@ export interface CreateStubForClientAPIOptions {
|
|
|
105
98
|
|
|
106
99
|
export interface CreateStubForConnectionAPIOptions {
|
|
107
100
|
connection: MCPConnection;
|
|
108
|
-
streamable?: Record<string, boolean>;
|
|
109
101
|
debugId?: () => string;
|
|
110
102
|
createServerClient?: (
|
|
111
103
|
mcpServer: { connection: MCPConnection; name?: string },
|
package/src/core/client/proxy.ts
CHANGED
|
@@ -74,16 +74,7 @@ export function createMCPClientProxy<T extends Record<string, unknown>>(
|
|
|
74
74
|
? { "x-trace-debug-id": debugId }
|
|
75
75
|
: undefined;
|
|
76
76
|
|
|
77
|
-
const { client
|
|
78
|
-
|
|
79
|
-
if (options?.streamable?.[String(toolName)]) {
|
|
80
|
-
if (!callStreamableTool) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Tool ${String(toolName)} requires streaming support but client doesn't provide callStreamableTool`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
return await callStreamableTool(String(toolName), args);
|
|
86
|
-
}
|
|
77
|
+
const { client } = await createClient(extraHeaders);
|
|
87
78
|
|
|
88
79
|
const { structuredContent, isError, content } = await client.callTool({
|
|
89
80
|
name: String(toolName),
|
package/src/core/plugins.ts
CHANGED
|
@@ -29,6 +29,13 @@ export interface RegisterEmptyStateParams {
|
|
|
29
29
|
component: ReactNode;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface RegisterSettingsSidebarItemParams {
|
|
33
|
+
key: string;
|
|
34
|
+
icon: ReactNode;
|
|
35
|
+
label: string;
|
|
36
|
+
to: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export interface PluginSetupContext {
|
|
33
40
|
parentRoute: AnyRoute;
|
|
34
41
|
routing: {
|
|
@@ -37,6 +44,9 @@ export interface PluginSetupContext {
|
|
|
37
44
|
};
|
|
38
45
|
registerRootSidebarItem: (params: RegisterRootSidebarItemParams) => void;
|
|
39
46
|
registerSidebarGroup: (params: RegisterSidebarGroupParams) => void;
|
|
47
|
+
registerSettingsSidebarItem: (
|
|
48
|
+
params: RegisterSettingsSidebarItemParams,
|
|
49
|
+
) => void;
|
|
40
50
|
registerPluginRoutes: (route: AnyRoute[]) => void;
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -16,9 +16,9 @@ import type { Hono } from "hono";
|
|
|
16
16
|
import type { Kysely } from "kysely";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Subset of
|
|
19
|
+
* Subset of StudioContext exposed to server plugin tool handlers.
|
|
20
20
|
*
|
|
21
|
-
* Plugins receive the full
|
|
21
|
+
* Plugins receive the full StudioContext at runtime but should only depend on
|
|
22
22
|
* these properties. This keeps the plugin contract stable and avoids coupling
|
|
23
23
|
* plugins to Mesh internals (db, vault, tracer, etc.).
|
|
24
24
|
*/
|
package/src/index.ts
CHANGED
|
@@ -123,3 +123,25 @@ export {
|
|
|
123
123
|
|
|
124
124
|
// Re-export workflow binding types
|
|
125
125
|
export { WORKFLOWS_COLLECTION_BINDING } from "./well-known/workflow";
|
|
126
|
+
|
|
127
|
+
// Re-export brand binding types (for reading org brand context)
|
|
128
|
+
export {
|
|
129
|
+
BrandColorsSchema,
|
|
130
|
+
type BrandColors,
|
|
131
|
+
BrandFontsSchema,
|
|
132
|
+
type BrandFonts,
|
|
133
|
+
BrandAssetsSchema,
|
|
134
|
+
type BrandAssets,
|
|
135
|
+
BrandSchema,
|
|
136
|
+
type Brand,
|
|
137
|
+
BrandGetInputSchema,
|
|
138
|
+
type BrandGetInput,
|
|
139
|
+
type BrandGetOutput,
|
|
140
|
+
BrandListInputSchema,
|
|
141
|
+
type BrandListInput,
|
|
142
|
+
BrandListOutputSchema,
|
|
143
|
+
type BrandListOutput,
|
|
144
|
+
BRAND_BINDING,
|
|
145
|
+
BrandBinding,
|
|
146
|
+
type BrandBindingClient,
|
|
147
|
+
} from "./well-known/brand";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand Well-Known Binding
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface for reading brand context (colors, fonts, assets,
|
|
5
|
+
* voice) from an organization. External MCPs that need brand awareness
|
|
6
|
+
* can declare a dependency on this binding and call BRAND_GET / BRAND_LIST
|
|
7
|
+
* to pull structured brand data on demand.
|
|
8
|
+
*
|
|
9
|
+
* This binding includes:
|
|
10
|
+
* - BRAND_GET: Get a brand by ID (or the org default)
|
|
11
|
+
* - BRAND_LIST: List all active brands (optional)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { bindingClient, type ToolBinder } from "../core/binder";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Brand Sub-Schemas
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export const BrandColorsSchema = z.object({
|
|
22
|
+
primary: z.string().optional().describe("Primary brand color (hex)"),
|
|
23
|
+
secondary: z.string().optional().describe("Secondary brand color (hex)"),
|
|
24
|
+
accent: z.string().optional().describe("Accent / highlight color (hex)"),
|
|
25
|
+
background: z.string().optional().describe("Background color (hex)"),
|
|
26
|
+
foreground: z.string().optional().describe("Foreground / text color (hex)"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type BrandColors = z.infer<typeof BrandColorsSchema>;
|
|
30
|
+
|
|
31
|
+
export const BrandFontsSchema = z.object({
|
|
32
|
+
heading: z.string().optional().describe("Font family for headings"),
|
|
33
|
+
body: z.string().optional().describe("Font family for body text"),
|
|
34
|
+
code: z.string().optional().describe("Font family for code / monospace"),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type BrandFonts = z.infer<typeof BrandFontsSchema>;
|
|
38
|
+
|
|
39
|
+
export const BrandAssetsSchema = z.object({
|
|
40
|
+
logo: z.string().optional().describe("Logo URL"),
|
|
41
|
+
favicon: z.string().optional().describe("Favicon URL"),
|
|
42
|
+
ogImage: z.string().optional().describe("Open Graph image URL"),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type BrandAssets = z.infer<typeof BrandAssetsSchema>;
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Brand Schema
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
export const BrandSchema = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
name: z.string().describe("Brand / company name"),
|
|
54
|
+
domain: z.string().optional().describe("Company domain (e.g. example.com)"),
|
|
55
|
+
colors: BrandColorsSchema.optional().describe("Semantic color palette"),
|
|
56
|
+
fonts: BrandFontsSchema.optional().describe("Font families by role"),
|
|
57
|
+
assets: BrandAssetsSchema.optional().describe("Visual identity assets"),
|
|
58
|
+
overview: z.string().optional().describe("Company overview / description"),
|
|
59
|
+
tagline: z.string().optional().describe("Brand tagline"),
|
|
60
|
+
tone: z.string().optional().describe("Tone of voice description"),
|
|
61
|
+
metadata: z
|
|
62
|
+
.record(z.string(), z.unknown())
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Extra design tokens and metadata"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type Brand = z.infer<typeof BrandSchema>;
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// BRAND_GET Schemas
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
export const BrandGetInputSchema = z.object({
|
|
74
|
+
id: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Brand ID. Omit to get the default brand."),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type BrandGetInput = z.infer<typeof BrandGetInputSchema>;
|
|
81
|
+
|
|
82
|
+
export type BrandGetOutput = z.infer<typeof BrandSchema>;
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// BRAND_LIST Schemas
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export const BrandListInputSchema = z.object({});
|
|
89
|
+
|
|
90
|
+
export type BrandListInput = z.infer<typeof BrandListInputSchema>;
|
|
91
|
+
|
|
92
|
+
export const BrandListOutputSchema = z.object({
|
|
93
|
+
items: z.array(BrandSchema),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export type BrandListOutput = z.infer<typeof BrandListOutputSchema>;
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Brand Binding
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Brand Binding
|
|
104
|
+
*
|
|
105
|
+
* Defines the interface for reading brand context from an organization.
|
|
106
|
+
*
|
|
107
|
+
* Required tools:
|
|
108
|
+
* - BRAND_GET: Get a single brand by ID, or the default brand when no ID
|
|
109
|
+
*
|
|
110
|
+
* Optional tools:
|
|
111
|
+
* - BRAND_LIST: List all active brands
|
|
112
|
+
*/
|
|
113
|
+
export const BRAND_BINDING = [
|
|
114
|
+
{
|
|
115
|
+
name: "BRAND_GET" as const,
|
|
116
|
+
inputSchema: BrandGetInputSchema,
|
|
117
|
+
outputSchema: BrandSchema,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "BRAND_LIST" as const,
|
|
121
|
+
inputSchema: BrandListInputSchema,
|
|
122
|
+
outputSchema: BrandListOutputSchema,
|
|
123
|
+
opt: true,
|
|
124
|
+
},
|
|
125
|
+
] satisfies ToolBinder[];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Brand Binding Client
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import { BrandBinding } from "@decocms/bindings/brand";
|
|
133
|
+
*
|
|
134
|
+
* const client = BrandBinding.forConnection(connection);
|
|
135
|
+
*
|
|
136
|
+
* // Get the default brand
|
|
137
|
+
* const brand = await client.BRAND_GET({});
|
|
138
|
+
*
|
|
139
|
+
* // Get a specific brand
|
|
140
|
+
* const brand = await client.BRAND_GET({ id: "acme-corp" });
|
|
141
|
+
*
|
|
142
|
+
* // List all brands
|
|
143
|
+
* const { items } = await client.BRAND_LIST({});
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export const BrandBinding = bindingClient(BRAND_BINDING);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Type helper for the Brand binding client
|
|
150
|
+
*/
|
|
151
|
+
export type BrandBindingClient = ReturnType<typeof BrandBinding.forConnection>;
|
|
@@ -21,22 +21,22 @@ export const ToolCallActionSchema = z.object({
|
|
|
21
21
|
transformCode: z
|
|
22
22
|
.string()
|
|
23
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(
|
|
25
|
-
The
|
|
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(stepInput): Output { ... }\`
|
|
25
|
+
The stepInput will match with the tool call outputSchema. If transformCode is not provided, the tool call result will be used as the step output.
|
|
26
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
27
|
});
|
|
28
28
|
export type ToolCallAction = z.infer<typeof ToolCallActionSchema>;
|
|
29
29
|
|
|
30
30
|
export const CodeActionSchema = z.object({
|
|
31
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(
|
|
33
|
-
The
|
|
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(stepInput): Output { ... }\`
|
|
33
|
+
The stepInput is the resolved value of the references in the input field. Example:
|
|
34
34
|
{
|
|
35
35
|
"input": {
|
|
36
36
|
"name": "@Step_1.name",
|
|
37
37
|
"age": "@Step_2.age"
|
|
38
38
|
},
|
|
39
|
-
"code": "export default function(
|
|
39
|
+
"code": "export default function(stepInput): Output { return { result: \`\${stepInput.name} is \${stepInput.age} years old.\` } }"
|
|
40
40
|
}
|
|
41
41
|
`,
|
|
42
42
|
),
|
|
@@ -66,19 +66,60 @@ export type StepAction = z.infer<typeof StepActionSchema>;
|
|
|
66
66
|
/**
|
|
67
67
|
* Step Config Schema - Optional configuration for retry, timeout, and looping
|
|
68
68
|
*/
|
|
69
|
+
/**
|
|
70
|
+
* Step Condition Schema - Structured condition for bail and future `when` support.
|
|
71
|
+
* Evaluates a resolved @ref against an optional operator.
|
|
72
|
+
* If no operator is specified, checks for truthiness.
|
|
73
|
+
*/
|
|
74
|
+
export const StepConditionSchema = z.object({
|
|
75
|
+
ref: z
|
|
76
|
+
.string()
|
|
77
|
+
.describe(
|
|
78
|
+
"@ref to resolve and evaluate, e.g. '@validate.shouldStop' or '@input.skipProcessing'",
|
|
79
|
+
),
|
|
80
|
+
eq: z
|
|
81
|
+
.unknown()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Step bails if resolved value equals this"),
|
|
84
|
+
neq: z
|
|
85
|
+
.unknown()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Step bails if resolved value does NOT equal this"),
|
|
88
|
+
gt: z
|
|
89
|
+
.number()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Step bails if resolved value is greater than this"),
|
|
92
|
+
lt: z
|
|
93
|
+
.number()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Step bails if resolved value is less than this"),
|
|
96
|
+
});
|
|
97
|
+
export type StepCondition = z.infer<typeof StepConditionSchema>;
|
|
98
|
+
|
|
69
99
|
export const StepConfigSchema = z.object({
|
|
70
100
|
maxAttempts: z
|
|
71
101
|
.number()
|
|
102
|
+
.int()
|
|
103
|
+
.min(1)
|
|
104
|
+
.max(10)
|
|
72
105
|
.optional()
|
|
73
106
|
.describe("Max retry attempts on failure (default: 1, no retries)"),
|
|
74
107
|
backoffMs: z
|
|
75
108
|
.number()
|
|
109
|
+
.int()
|
|
110
|
+
.min(100)
|
|
111
|
+
.max(60_000)
|
|
76
112
|
.optional()
|
|
77
113
|
.describe("Initial delay between retries in ms (doubles each attempt)"),
|
|
78
114
|
timeoutMs: z
|
|
79
115
|
.number()
|
|
116
|
+
.int()
|
|
117
|
+
.min(100)
|
|
118
|
+
.max(86_400_000)
|
|
80
119
|
.optional()
|
|
81
|
-
.describe(
|
|
120
|
+
.describe(
|
|
121
|
+
"Max execution time in ms before step fails (default: 30000). Upper bound 24h.",
|
|
122
|
+
),
|
|
82
123
|
onError: z
|
|
83
124
|
.enum(["fail", "continue"])
|
|
84
125
|
.optional()
|
|
@@ -102,16 +143,21 @@ export type StepConfig = z.infer<typeof StepConfigSchema>;
|
|
|
102
143
|
* - @ctx.execution_id → current workflow execution ID
|
|
103
144
|
*/
|
|
104
145
|
|
|
105
|
-
type JsonSchema = {
|
|
146
|
+
export type JsonSchema = {
|
|
106
147
|
type?: string;
|
|
107
|
-
properties?: Record<string,
|
|
148
|
+
properties?: Record<string, JsonSchema>;
|
|
108
149
|
required?: string[];
|
|
109
150
|
description?: string;
|
|
110
151
|
additionalProperties?: boolean | Record<string, unknown>;
|
|
111
152
|
additionalItems?: boolean | Record<string, unknown>;
|
|
112
153
|
items?: JsonSchema;
|
|
154
|
+
format?: string;
|
|
155
|
+
enum?: string[];
|
|
156
|
+
maxLength?: number;
|
|
157
|
+
anyOf?: JsonSchema[];
|
|
158
|
+
[key: string]: unknown;
|
|
113
159
|
};
|
|
114
|
-
const JsonSchemaSchema: z.ZodType<JsonSchema> = z.lazy(() =>
|
|
160
|
+
export const JsonSchemaSchema: z.ZodType<JsonSchema> = z.lazy(() =>
|
|
115
161
|
z
|
|
116
162
|
.object({
|
|
117
163
|
type: z.string().optional(),
|
|
@@ -127,7 +173,7 @@ const JsonSchemaSchema: z.ZodType<JsonSchema> = z.lazy(() =>
|
|
|
127
173
|
items: JsonSchemaSchema.optional(),
|
|
128
174
|
})
|
|
129
175
|
.passthrough(),
|
|
130
|
-
)
|
|
176
|
+
) as unknown as z.ZodType<JsonSchema>;
|
|
131
177
|
|
|
132
178
|
/**
|
|
133
179
|
* Step names that are reserved by the @ref system and cannot be used as step names.
|
|
@@ -170,6 +216,12 @@ export const StepSchema = z.object({
|
|
|
170
216
|
.describe("max parallel iterations. default is 1 (sequential)"),
|
|
171
217
|
})
|
|
172
218
|
.optional(),
|
|
219
|
+
bail: z
|
|
220
|
+
.union([z.literal(true), StepConditionSchema])
|
|
221
|
+
.optional()
|
|
222
|
+
.describe(
|
|
223
|
+
"Early return: if true, the workflow exits with this step's output on successful completion. If a condition object, the workflow exits only when the condition is met. Bail is ignored on step error.",
|
|
224
|
+
),
|
|
173
225
|
});
|
|
174
226
|
|
|
175
227
|
export type Step = z.infer<typeof StepSchema>;
|
|
@@ -324,6 +376,10 @@ export const WorkflowSchema = BaseCollectionEntitySchema.extend({
|
|
|
324
376
|
.optional()
|
|
325
377
|
.describe("Human-readable summary of what this workflow does"),
|
|
326
378
|
|
|
379
|
+
input_schema: JsonSchemaSchema.optional().describe(
|
|
380
|
+
"Optional JSON Schema describing the expected input for this workflow.",
|
|
381
|
+
),
|
|
382
|
+
|
|
327
383
|
steps: z
|
|
328
384
|
.array(StepSchema)
|
|
329
385
|
.describe(
|
|
@@ -370,7 +426,7 @@ export const DEFAULT_TOOL_STEP: Omit<Step, "name"> = {
|
|
|
370
426
|
interface Input {
|
|
371
427
|
|
|
372
428
|
}
|
|
373
|
-
export default function(
|
|
429
|
+
export default function(stepInput) { return stepInput.result }`,
|
|
374
430
|
},
|
|
375
431
|
input: {
|
|
376
432
|
modelId: "anthropic/claude-4.5-haiku",
|
|
@@ -399,10 +455,10 @@ export const DEFAULT_CODE_STEP: Step = {
|
|
|
399
455
|
interface Output {
|
|
400
456
|
result: unknown;
|
|
401
457
|
}
|
|
402
|
-
|
|
403
|
-
export default async function(
|
|
458
|
+
|
|
459
|
+
export default async function(stepInput: Input): Promise<Output> {
|
|
404
460
|
return {
|
|
405
|
-
result:
|
|
461
|
+
result: stepInput.example
|
|
406
462
|
}
|
|
407
463
|
}`,
|
|
408
464
|
},
|
|
@@ -477,6 +533,7 @@ export const WORKFLOW_EXECUTION_BINDING = createCollectionBindings(
|
|
|
477
533
|
export interface DAGStep {
|
|
478
534
|
name: string;
|
|
479
535
|
input?: unknown;
|
|
536
|
+
bail?: true | StepCondition;
|
|
480
537
|
}
|
|
481
538
|
|
|
482
539
|
/**
|
|
@@ -491,7 +548,7 @@ export function getAllRefs(input: unknown): string[] {
|
|
|
491
548
|
|
|
492
549
|
function traverse(value: unknown) {
|
|
493
550
|
if (typeof value === "string") {
|
|
494
|
-
const matches = value.match(/@(
|
|
551
|
+
const matches = value.match(/@([a-zA-Z_][a-zA-Z0-9_-]*)/g);
|
|
495
552
|
if (matches) {
|
|
496
553
|
refs.push(...matches.map((m) => m.substring(1))); // Remove @ prefix
|
|
497
554
|
}
|
|
@@ -523,13 +580,13 @@ export function getStepDependencies(
|
|
|
523
580
|
function traverse(value: unknown) {
|
|
524
581
|
if (typeof value === "string") {
|
|
525
582
|
// Match @stepName or @stepName.something patterns
|
|
526
|
-
const matches = value.match(/@(
|
|
583
|
+
const matches = value.match(/@([a-zA-Z_][a-zA-Z0-9_-]*)/g);
|
|
527
584
|
if (matches) {
|
|
528
585
|
for (const match of matches) {
|
|
529
586
|
const refName = match.substring(1); // Remove @
|
|
530
587
|
// Only count as dependency if it references another step
|
|
531
588
|
// (not "item", "index", "input" from forEach or workflow input)
|
|
532
|
-
if (allStepNames.has(refName)) {
|
|
589
|
+
if (allStepNames.has(refName) && refName !== step.name) {
|
|
533
590
|
deps.push(refName);
|
|
534
591
|
}
|
|
535
592
|
}
|
|
@@ -542,6 +599,12 @@ export function getStepDependencies(
|
|
|
542
599
|
}
|
|
543
600
|
|
|
544
601
|
traverse(step.input);
|
|
602
|
+
|
|
603
|
+
// Extract dependency from bail condition ref
|
|
604
|
+
if (step.bail && step.bail !== true) {
|
|
605
|
+
traverse(step.bail.ref);
|
|
606
|
+
}
|
|
607
|
+
|
|
545
608
|
return [...new Set(deps)];
|
|
546
609
|
}
|
|
547
610
|
|