@polka-codes/core 0.10.11 → 0.10.16
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/dist/Agent/backoff.d.ts +7 -0
- package/dist/Agent/backoff.js +21 -0
- package/dist/Agent/backoff.js.map +1 -0
- package/dist/Agent/index.d.ts +2 -0
- package/dist/Agent/index.js +3 -0
- package/dist/Agent/index.js.map +1 -0
- package/dist/Agent/parseJsonFromMarkdown.d.ts +8 -0
- package/dist/Agent/parseJsonFromMarkdown.js +34 -0
- package/dist/Agent/parseJsonFromMarkdown.js.map +1 -0
- package/dist/Agent/parseJsonFromMarkdown.test.d.ts +1 -0
- package/dist/Agent/parseJsonFromMarkdown.test.js +70 -0
- package/dist/Agent/parseJsonFromMarkdown.test.js.map +1 -0
- package/dist/Agent/prompts.d.ts +9 -0
- package/dist/Agent/prompts.js +107 -0
- package/dist/Agent/prompts.js.map +1 -0
- package/dist/UsageMeter.d.ts +101 -0
- package/dist/UsageMeter.js +299 -0
- package/dist/UsageMeter.js.map +1 -0
- package/dist/UsageMeter.test.d.ts +4 -0
- package/dist/UsageMeter.test.js +556 -0
- package/dist/UsageMeter.test.js.map +1 -0
- package/dist/config/base.d.ts +68 -0
- package/dist/config/base.js +56 -0
- package/dist/config/base.js.map +1 -0
- package/dist/config/memory.d.ts +24 -0
- package/dist/config/memory.js +36 -0
- package/dist/config/memory.js.map +1 -0
- package/dist/config.d.ts +236 -0
- package/dist/config.js +184 -0
- package/dist/config.js.map +1 -0
- package/dist/errors/base.d.ts +31 -0
- package/dist/errors/base.js +60 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/fs/index.d.ts +2 -0
- package/dist/fs/index.js +3 -0
- package/dist/fs/index.js.map +1 -0
- package/dist/fs/node-provider.d.ts +16 -0
- package/dist/fs/node-provider.js +47 -0
- package/dist/fs/node-provider.js.map +1 -0
- package/dist/fs/provider.d.ts +61 -0
- package/dist/fs/provider.js +3 -0
- package/dist/fs/provider.js.map +1 -0
- package/dist/index.d.ts +20 -191
- package/dist/index.js +21 -4123
- package/dist/index.js.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/types.d.ts +136 -0
- package/dist/memory/types.js +2 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/path.d.ts +9 -0
- package/dist/path.js +68 -0
- package/dist/path.js.map +1 -0
- package/dist/path.test.d.ts +1 -0
- package/dist/path.test.js +82 -0
- package/dist/path.test.js.map +1 -0
- package/dist/pricing/converter.d.ts +6 -0
- package/dist/pricing/converter.js +13 -0
- package/dist/pricing/converter.js.map +1 -0
- package/dist/pricing/converter.test.d.ts +1 -0
- package/dist/pricing/converter.test.js +54 -0
- package/dist/pricing/converter.test.js.map +1 -0
- package/dist/pricing/index.d.ts +2 -0
- package/dist/pricing/index.js +2 -0
- package/dist/pricing/index.js.map +1 -0
- package/dist/pricing/portkey-client.d.ts +2 -0
- package/dist/pricing/portkey-client.js +57 -0
- package/dist/pricing/portkey-client.js.map +1 -0
- package/dist/pricing/pricing-service.d.ts +6 -0
- package/dist/pricing/pricing-service.js +125 -0
- package/dist/pricing/pricing-service.js.map +1 -0
- package/dist/pricing/pricing-service.test.d.ts +1 -0
- package/dist/pricing/pricing-service.test.js +141 -0
- package/dist/pricing/pricing-service.test.js.map +1 -0
- package/dist/pricing/types.d.ts +24 -0
- package/dist/pricing/types.js +2 -0
- package/dist/pricing/types.js.map +1 -0
- package/dist/skills/__tests__/discovery.test.d.ts +1 -0
- package/dist/skills/__tests__/discovery.test.js +254 -0
- package/dist/skills/__tests__/discovery.test.js.map +1 -0
- package/dist/skills/__tests__/validation.test.d.ts +1 -0
- package/dist/skills/__tests__/validation.test.js +221 -0
- package/dist/skills/__tests__/validation.test.js.map +1 -0
- package/dist/skills/constants.d.ts +32 -0
- package/dist/skills/constants.js +50 -0
- package/dist/skills/constants.js.map +1 -0
- package/dist/skills/discovery.d.ts +56 -0
- package/dist/skills/discovery.js +392 -0
- package/dist/skills/discovery.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/tools/index.d.ts +3 -0
- package/dist/skills/tools/index.js +5 -0
- package/dist/skills/tools/index.js.map +1 -0
- package/dist/skills/tools/listSkills.d.ts +54 -0
- package/dist/skills/tools/listSkills.js +52 -0
- package/dist/skills/tools/listSkills.js.map +1 -0
- package/dist/skills/tools/loadSkill.d.ts +52 -0
- package/dist/skills/tools/loadSkill.js +86 -0
- package/dist/skills/tools/loadSkill.js.map +1 -0
- package/dist/skills/tools/readSkillFile.d.ts +43 -0
- package/dist/skills/tools/readSkillFile.js +68 -0
- package/dist/skills/tools/readSkillFile.js.map +1 -0
- package/dist/skills/types.d.ts +83 -0
- package/dist/skills/types.js +42 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/skills/validation.d.ts +30 -0
- package/dist/skills/validation.js +133 -0
- package/dist/skills/validation.js.map +1 -0
- package/dist/tool.d.ts +51 -0
- package/dist/tool.js +2 -0
- package/dist/tool.js.map +1 -0
- package/dist/tools/askFollowupQuestion.d.ts +35 -0
- package/dist/tools/askFollowupQuestion.js +105 -0
- package/dist/tools/askFollowupQuestion.js.map +1 -0
- package/dist/tools/askFollowupQuestion.test.d.ts +1 -0
- package/dist/tools/askFollowupQuestion.test.js +80 -0
- package/dist/tools/askFollowupQuestion.test.js.map +1 -0
- package/dist/tools/executeCommand.d.ts +29 -0
- package/dist/tools/executeCommand.js +82 -0
- package/dist/tools/executeCommand.js.map +1 -0
- package/dist/tools/executeCommand.test.d.ts +1 -0
- package/dist/tools/executeCommand.test.js +60 -0
- package/dist/tools/executeCommand.test.js.map +1 -0
- package/dist/tools/fetchUrl.d.ts +26 -0
- package/dist/tools/fetchUrl.js +85 -0
- package/dist/tools/fetchUrl.js.map +1 -0
- package/dist/tools/index.d.ts +15 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/listFiles.d.ts +35 -0
- package/dist/tools/listFiles.js +61 -0
- package/dist/tools/listFiles.js.map +1 -0
- package/dist/tools/listFiles.test.d.ts +1 -0
- package/dist/tools/listFiles.test.js +59 -0
- package/dist/tools/listFiles.test.js.map +1 -0
- package/dist/tools/provider.d.ts +76 -0
- package/dist/tools/provider.js +60 -0
- package/dist/tools/provider.js.map +1 -0
- package/dist/tools/readBinaryFile.d.ts +26 -0
- package/dist/tools/readBinaryFile.js +52 -0
- package/dist/tools/readBinaryFile.js.map +1 -0
- package/dist/tools/readFile.d.ts +35 -0
- package/dist/tools/readFile.js +128 -0
- package/dist/tools/readFile.js.map +1 -0
- package/dist/tools/readFile.test.d.ts +1 -0
- package/dist/tools/readFile.test.js +37 -0
- package/dist/tools/readFile.test.js.map +1 -0
- package/dist/tools/removeFile.d.ts +26 -0
- package/dist/tools/removeFile.js +49 -0
- package/dist/tools/removeFile.js.map +1 -0
- package/dist/tools/removeFile.test.d.ts +1 -0
- package/dist/tools/removeFile.test.js +32 -0
- package/dist/tools/removeFile.test.js.map +1 -0
- package/dist/tools/renameFile.d.ts +29 -0
- package/dist/tools/renameFile.js +48 -0
- package/dist/tools/renameFile.js.map +1 -0
- package/dist/tools/renameFile.test.d.ts +1 -0
- package/dist/tools/renameFile.test.js +53 -0
- package/dist/tools/renameFile.test.js.map +1 -0
- package/dist/tools/replaceInFile.d.ts +29 -0
- package/dist/tools/replaceInFile.js +233 -0
- package/dist/tools/replaceInFile.js.map +1 -0
- package/dist/tools/replaceInFile.test.d.ts +1 -0
- package/dist/tools/replaceInFile.test.js +79 -0
- package/dist/tools/replaceInFile.test.js.map +1 -0
- package/dist/tools/response-builders.d.ts +64 -0
- package/dist/tools/response-builders.js +88 -0
- package/dist/tools/response-builders.js.map +1 -0
- package/dist/tools/search.d.ts +26 -0
- package/dist/tools/search.js +56 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/search.test.d.ts +1 -0
- package/dist/tools/search.test.js +22 -0
- package/dist/tools/search.test.js.map +1 -0
- package/dist/tools/searchFiles.d.ts +32 -0
- package/dist/tools/searchFiles.js +86 -0
- package/dist/tools/searchFiles.js.map +1 -0
- package/dist/tools/todo.d.ts +37 -0
- package/dist/tools/todo.js +41 -0
- package/dist/tools/todo.js.map +1 -0
- package/dist/tools/utils/index.d.ts +1 -0
- package/dist/tools/utils/index.js +2 -0
- package/dist/tools/utils/index.js.map +1 -0
- package/dist/tools/utils/replaceInFile.d.ts +7 -0
- package/dist/tools/utils/replaceInFile.js +133 -0
- package/dist/tools/utils/replaceInFile.js.map +1 -0
- package/dist/tools/utils/replaceInFile.test.d.ts +1 -0
- package/dist/tools/utils/replaceInFile.test.js +308 -0
- package/dist/tools/utils/replaceInFile.test.js.map +1 -0
- package/dist/tools/utils.d.ts +10 -0
- package/dist/tools/utils.js +27 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/writeToFile.d.ts +29 -0
- package/dist/tools/writeToFile.js +85 -0
- package/dist/tools/writeToFile.js.map +1 -0
- package/dist/tools/writeToFile.test.d.ts +1 -0
- package/dist/tools/writeToFile.test.js +46 -0
- package/dist/tools/writeToFile.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/merge.d.ts +26 -0
- package/dist/utils/merge.js +45 -0
- package/dist/utils/merge.js.map +1 -0
- package/dist/workflow/agent.workflow.d.ts +39 -0
- package/dist/workflow/agent.workflow.js +166 -0
- package/dist/workflow/agent.workflow.js.map +1 -0
- package/dist/workflow/agent.workflow.test.d.ts +1 -0
- package/dist/workflow/agent.workflow.test.js +175 -0
- package/dist/workflow/agent.workflow.test.js.map +1 -0
- package/dist/workflow/control-flow.test.d.ts +1 -0
- package/dist/workflow/control-flow.test.js +323 -0
- package/dist/workflow/control-flow.test.js.map +1 -0
- package/dist/workflow/dynamic-edge-cases.test.d.ts +1 -0
- package/dist/workflow/dynamic-edge-cases.test.js +486 -0
- package/dist/workflow/dynamic-edge-cases.test.js.map +1 -0
- package/dist/workflow/dynamic-types.d.ts +124 -0
- package/dist/workflow/dynamic-types.js +105 -0
- package/dist/workflow/dynamic-types.js.map +1 -0
- package/dist/workflow/dynamic.d.ts +118 -0
- package/dist/workflow/dynamic.js +999 -0
- package/dist/workflow/dynamic.js.map +1 -0
- package/dist/workflow/index.d.ts +6 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/json-ai-types.d.ts +122 -0
- package/dist/workflow/json-ai-types.js +144 -0
- package/dist/workflow/json-ai-types.js.map +1 -0
- package/dist/workflow/json-schema-conversion.test.d.ts +1 -0
- package/dist/workflow/json-schema-conversion.test.js +371 -0
- package/dist/workflow/json-schema-conversion.test.js.map +1 -0
- package/dist/workflow/try-catch.test.d.ts +1 -0
- package/dist/workflow/try-catch.test.js +443 -0
- package/dist/workflow/try-catch.test.js.map +1 -0
- package/dist/workflow/types.d.ts +103 -0
- package/dist/workflow/types.js +17 -0
- package/dist/workflow/types.js.map +1 -0
- package/dist/workflow/workflow.d.ts +29 -0
- package/dist/workflow/workflow.js +57 -0
- package/dist/workflow/workflow.js.map +1 -0
- package/dist/workflow/workflow.test.d.ts +1 -0
- package/dist/workflow/workflow.test.js +189 -0
- package/dist/workflow/workflow.test.js.map +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import { parse } from 'yaml';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { parseJsonFromMarkdown } from '../Agent/parseJsonFromMarkdown';
|
|
4
|
+
import { agentWorkflow } from './agent.workflow';
|
|
5
|
+
import { WorkflowFileSchema, } from './dynamic-types';
|
|
6
|
+
/**
|
|
7
|
+
* Maximum iterations for while loops to prevent infinite loops
|
|
8
|
+
*/
|
|
9
|
+
const MAX_WHILE_LOOP_ITERATIONS = 1000;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a JSON Schema to a Zod schema
|
|
12
|
+
* Supports a subset of JSON SchemaDraft 7
|
|
13
|
+
*
|
|
14
|
+
* This is exported to allow reuse in other parts of the codebase that need to
|
|
15
|
+
* convert JSON schemas to Zod schemas (e.g., MCP server tool schema conversion).
|
|
16
|
+
*/
|
|
17
|
+
export function convertJsonSchemaToZod(schema) {
|
|
18
|
+
// Handle enum types
|
|
19
|
+
if (schema.enum) {
|
|
20
|
+
// JSON Schema enums can contain strings, numbers, or booleans
|
|
21
|
+
// For non-string or mixed enums, use z.union with z.literal
|
|
22
|
+
// For string-only enums, use z.enum for better performance
|
|
23
|
+
const enumValues = schema.enum;
|
|
24
|
+
// Handle empty enum - no valid values
|
|
25
|
+
if (enumValues.length === 0) {
|
|
26
|
+
return z.never();
|
|
27
|
+
}
|
|
28
|
+
// Check if all values are strings
|
|
29
|
+
if (enumValues.every((v) => typeof v === 'string')) {
|
|
30
|
+
return z.enum(enumValues);
|
|
31
|
+
}
|
|
32
|
+
// Mixed or non-string enums: use z.union with z.literal
|
|
33
|
+
const literals = enumValues.map((v) => z.literal(v));
|
|
34
|
+
if (literals.length === 1) {
|
|
35
|
+
return literals[0];
|
|
36
|
+
}
|
|
37
|
+
// z.union can take an array of schemas
|
|
38
|
+
// Cast to any because Zod's union type inference is complex
|
|
39
|
+
return z.union([literals[0], literals[1], ...literals.slice(2)]);
|
|
40
|
+
}
|
|
41
|
+
// Handle union types (type: ["string", "null"])
|
|
42
|
+
if (Array.isArray(schema.type)) {
|
|
43
|
+
const types = schema.type;
|
|
44
|
+
if (types.includes('null') && types.length === 2) {
|
|
45
|
+
const nonNullType = types.find((t) => t !== 'null');
|
|
46
|
+
if (nonNullType === 'string')
|
|
47
|
+
return z.string().nullable();
|
|
48
|
+
if (nonNullType === 'number')
|
|
49
|
+
return z.number().nullable();
|
|
50
|
+
if (nonNullType === 'integer')
|
|
51
|
+
return z
|
|
52
|
+
.number()
|
|
53
|
+
.refine((val) => Number.isInteger(val))
|
|
54
|
+
.nullable();
|
|
55
|
+
if (nonNullType === 'boolean')
|
|
56
|
+
return z.boolean().nullable();
|
|
57
|
+
if (nonNullType === 'object') {
|
|
58
|
+
// Handle object with nullable - need to preserve properties
|
|
59
|
+
const shape = {};
|
|
60
|
+
if (schema.properties) {
|
|
61
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
62
|
+
const propZod = convertJsonSchemaToZod(propSchema);
|
|
63
|
+
const isRequired = schema.required?.includes(propName);
|
|
64
|
+
shape[propName] = isRequired ? propZod : propZod.optional();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return z.object(shape).nullable();
|
|
68
|
+
}
|
|
69
|
+
if (nonNullType === 'array')
|
|
70
|
+
return z.array(z.any()).nullable();
|
|
71
|
+
}
|
|
72
|
+
// Fallback for complex unions
|
|
73
|
+
return z.any();
|
|
74
|
+
}
|
|
75
|
+
const type = schema.type;
|
|
76
|
+
switch (type) {
|
|
77
|
+
case 'string':
|
|
78
|
+
return z.string();
|
|
79
|
+
case 'number':
|
|
80
|
+
return z.number();
|
|
81
|
+
case 'integer':
|
|
82
|
+
// Zod v4 doesn't have .int(), use custom validation
|
|
83
|
+
return z.number().refine((val) => Number.isInteger(val), { message: 'Expected an integer' });
|
|
84
|
+
case 'boolean':
|
|
85
|
+
return z.boolean();
|
|
86
|
+
case 'null':
|
|
87
|
+
return z.null();
|
|
88
|
+
case 'object': {
|
|
89
|
+
const shape = {};
|
|
90
|
+
// Convert properties
|
|
91
|
+
if (schema.properties) {
|
|
92
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
93
|
+
const propZod = convertJsonSchemaToZod(propSchema);
|
|
94
|
+
// Check if property is required
|
|
95
|
+
const isRequired = schema.required?.includes(propName);
|
|
96
|
+
shape[propName] = isRequired ? propZod : propZod.optional();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Handle additionalProperties
|
|
100
|
+
if (schema.additionalProperties === true) {
|
|
101
|
+
// Use passthrough to allow additional properties without validation
|
|
102
|
+
return z.object(shape).passthrough();
|
|
103
|
+
}
|
|
104
|
+
if (typeof schema.additionalProperties === 'object') {
|
|
105
|
+
const additionalSchema = convertJsonSchemaToZod(schema.additionalProperties);
|
|
106
|
+
// Use explicit intersection for additional properties
|
|
107
|
+
// Note: We use z.intersection() instead of .and() for better readability
|
|
108
|
+
// The cast to z.ZodTypeAny is necessary because Zod's intersection types
|
|
109
|
+
// have complex type inference that TypeScript cannot always resolve correctly.
|
|
110
|
+
// This is a known limitation of Zod's type system.
|
|
111
|
+
return z.intersection(z.object(shape), z.record(z.string(), additionalSchema));
|
|
112
|
+
}
|
|
113
|
+
// No additionalProperties (defaults to false) - strict object
|
|
114
|
+
return z.object(shape);
|
|
115
|
+
}
|
|
116
|
+
case 'array': {
|
|
117
|
+
if (!schema.items) {
|
|
118
|
+
return z.array(z.any());
|
|
119
|
+
}
|
|
120
|
+
const itemSchema = convertJsonSchemaToZod(schema.items);
|
|
121
|
+
return z.array(itemSchema);
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return z.any();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Tool groups that can be used in step.tools arrays.
|
|
129
|
+
* - "readonly": File reading operations only
|
|
130
|
+
* - "readwrite": Full file system access
|
|
131
|
+
* - "internet": Network operations (fetch, search)
|
|
132
|
+
* - "all": All available tools (special keyword, not in this map)
|
|
133
|
+
*/
|
|
134
|
+
export const TOOL_GROUPS = {
|
|
135
|
+
readonly: ['readFile', 'readBinaryFile', 'listFiles', 'searchFiles'],
|
|
136
|
+
readwrite: ['readFile', 'readBinaryFile', 'listFiles', 'searchFiles', 'writeToFile', 'replaceInFile', 'removeFile', 'renameFile'],
|
|
137
|
+
internet: ['fetchUrl', 'search'],
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Validate a workflow file for common issues
|
|
141
|
+
*/
|
|
142
|
+
export function validateWorkflowFile(definition) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
// Check each workflow
|
|
145
|
+
for (const [workflowId, workflow] of Object.entries(definition.workflows)) {
|
|
146
|
+
// Validate steps exist
|
|
147
|
+
if (!workflow.steps || workflow.steps.length === 0) {
|
|
148
|
+
errors.push(`Workflow '${workflowId}' has no steps`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Check for break/continue outside loops
|
|
152
|
+
const checkBreakOutsideLoop = (steps, inLoop, path) => {
|
|
153
|
+
for (const step of steps) {
|
|
154
|
+
if (isBreakStep(step) || isContinueStep(step)) {
|
|
155
|
+
if (!inLoop) {
|
|
156
|
+
errors.push(`${path} has break/continue outside of a loop`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (isWhileLoopStep(step)) {
|
|
160
|
+
checkBreakOutsideLoop(step.while.steps, true, `${path}/${step.id}`);
|
|
161
|
+
}
|
|
162
|
+
if (isIfElseStep(step)) {
|
|
163
|
+
if (step.if.thenBranch) {
|
|
164
|
+
checkBreakOutsideLoop(step.if.thenBranch, inLoop, `${path}/${step.id}/then`);
|
|
165
|
+
}
|
|
166
|
+
if (step.if.elseBranch) {
|
|
167
|
+
checkBreakOutsideLoop(step.if.elseBranch, inLoop, `${path}/${step.id}/else`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (isTryCatchStep(step)) {
|
|
171
|
+
checkBreakOutsideLoop(step.try.trySteps, inLoop, `${path}/${step.id}/try`);
|
|
172
|
+
checkBreakOutsideLoop(step.try.catchSteps, inLoop, `${path}/${step.id}/catch`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
checkBreakOutsideLoop(workflow.steps, false, workflowId);
|
|
177
|
+
// Check for runWorkflow calls to non-existent workflows
|
|
178
|
+
const findRunWorkflowCalls = (steps, path) => {
|
|
179
|
+
for (const step of steps) {
|
|
180
|
+
if (isWhileLoopStep(step)) {
|
|
181
|
+
findRunWorkflowCalls(step.while.steps, `${path}/${step.id}`);
|
|
182
|
+
}
|
|
183
|
+
if (isIfElseStep(step)) {
|
|
184
|
+
if (step.if.thenBranch) {
|
|
185
|
+
findRunWorkflowCalls(step.if.thenBranch, `${path}/${step.id}/then`);
|
|
186
|
+
}
|
|
187
|
+
if (step.if.elseBranch) {
|
|
188
|
+
findRunWorkflowCalls(step.if.elseBranch, `${path}/${step.id}/else`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (isTryCatchStep(step)) {
|
|
192
|
+
findRunWorkflowCalls(step.try.trySteps, `${path}/${step.id}/try`);
|
|
193
|
+
findRunWorkflowCalls(step.try.catchSteps, `${path}/${step.id}/catch`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
findRunWorkflowCalls(workflow.steps, workflowId);
|
|
198
|
+
}
|
|
199
|
+
if (errors.length > 0) {
|
|
200
|
+
return { success: false, errors };
|
|
201
|
+
}
|
|
202
|
+
return { success: true };
|
|
203
|
+
}
|
|
204
|
+
export function parseDynamicWorkflowDefinition(source) {
|
|
205
|
+
try {
|
|
206
|
+
const raw = parse(source);
|
|
207
|
+
const validated = WorkflowFileSchema.safeParse(raw);
|
|
208
|
+
if (!validated.success) {
|
|
209
|
+
return { success: false, error: z.prettifyError(validated.error) };
|
|
210
|
+
}
|
|
211
|
+
// Additional validation for structural issues
|
|
212
|
+
const validation = validateWorkflowFile(validated.data);
|
|
213
|
+
if (!validation.success) {
|
|
214
|
+
return { success: false, error: `Workflow validation failed:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}` };
|
|
215
|
+
}
|
|
216
|
+
return { success: true, definition: validated.data };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function validateAndApplyDefaults(workflowId, workflow, input) {
|
|
223
|
+
if (!workflow.inputs || workflow.inputs.length === 0) {
|
|
224
|
+
return input;
|
|
225
|
+
}
|
|
226
|
+
const validatedInput = { ...input };
|
|
227
|
+
const errors = [];
|
|
228
|
+
for (const inputDef of workflow.inputs) {
|
|
229
|
+
const providedValue = input[inputDef.id];
|
|
230
|
+
if (providedValue !== undefined && providedValue !== null) {
|
|
231
|
+
validatedInput[inputDef.id] = providedValue;
|
|
232
|
+
}
|
|
233
|
+
else if (inputDef.default !== undefined && inputDef.default !== null) {
|
|
234
|
+
validatedInput[inputDef.id] = inputDef.default;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
errors.push(`Missing required input '${inputDef.id}'${inputDef.description ? `: ${inputDef.description}` : ''}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (errors.length > 0) {
|
|
241
|
+
throw new Error(`Workflow '${workflowId}' input validation failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
242
|
+
}
|
|
243
|
+
return validatedInput;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Safely evaluate a condition expression with access to input and state
|
|
247
|
+
*
|
|
248
|
+
* Security: When allowUnsafeCodeExecution is false (default), only supports:
|
|
249
|
+
* - Property access: input.foo, state.bar
|
|
250
|
+
* - Comparisons: ==, !=, ===, !==, <, >, <=, >=
|
|
251
|
+
* - Logical operators: &&, ||, !
|
|
252
|
+
* - Parentheses for grouping
|
|
253
|
+
* - Literals: strings, numbers, booleans, null
|
|
254
|
+
*
|
|
255
|
+
* When allowUnsafeCodeExecution is true, arbitrary JavaScript is executed.
|
|
256
|
+
* WARNING: Only set to true for trusted workflow definitions!
|
|
257
|
+
*/
|
|
258
|
+
function evaluateCondition(condition, input, state,
|
|
259
|
+
// SECURITY: Default must remain false for safe evaluation of untrusted workflows
|
|
260
|
+
// Only set to true for trusted, vetted workflow definitions
|
|
261
|
+
allowUnsafeCodeExecution = false, logger) {
|
|
262
|
+
if (allowUnsafeCodeExecution) {
|
|
263
|
+
// SECURITY WARNING: Unsafe code execution allows arbitrary JavaScript execution
|
|
264
|
+
// This should only be used for trusted, vetted workflow definitions
|
|
265
|
+
if (logger) {
|
|
266
|
+
logger.warn(`[SECURITY] Executing unsafe code evaluation for condition: ${condition}. This allows arbitrary JavaScript execution and should only be used for trusted workflows.`);
|
|
267
|
+
}
|
|
268
|
+
// Unsafe mode: use new Function for full JavaScript support
|
|
269
|
+
const functionBody = `
|
|
270
|
+
try {
|
|
271
|
+
return ${condition};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
throw new Error('Condition evaluation failed: ' + (error instanceof Error ? error.message : String(error)));
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
try {
|
|
277
|
+
const fn = new Function('input', 'state', functionBody);
|
|
278
|
+
const result = fn(input, state);
|
|
279
|
+
return Boolean(result);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
throw new Error(`Failed to evaluate condition: ${condition}. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// Safe mode: use a simple, restricted evaluator
|
|
287
|
+
return evaluateConditionSafe(condition, input, state);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Safe condition evaluator that supports a restricted subset of JavaScript
|
|
292
|
+
* Prevents code injection by not using eval or new Function
|
|
293
|
+
*
|
|
294
|
+
* Operator precedence (from lowest to highest):
|
|
295
|
+
* 1. || (logical OR)
|
|
296
|
+
* 2. && (logical AND)
|
|
297
|
+
* 3. ===, !==, ==, !=, >=, <=, >, < (comparisons)
|
|
298
|
+
* 4. ! (negation)
|
|
299
|
+
* 5. (...) (parentheses - highest precedence, evaluated first)
|
|
300
|
+
*/
|
|
301
|
+
function evaluateConditionSafe(condition, input, state) {
|
|
302
|
+
// Trim whitespace
|
|
303
|
+
condition = condition.trim();
|
|
304
|
+
// Handle simple boolean literals
|
|
305
|
+
if (condition === 'true')
|
|
306
|
+
return true;
|
|
307
|
+
if (condition === 'false')
|
|
308
|
+
return false;
|
|
309
|
+
// Handle logical OR (lowest precedence)
|
|
310
|
+
const orIndex = findTopLevelOperator(condition, '||');
|
|
311
|
+
if (orIndex !== -1) {
|
|
312
|
+
const left = condition.slice(0, orIndex).trim();
|
|
313
|
+
const right = condition.slice(orIndex + 2).trim();
|
|
314
|
+
return evaluateConditionSafe(left, input, state) || evaluateConditionSafe(right, input, state);
|
|
315
|
+
}
|
|
316
|
+
// Handle logical AND
|
|
317
|
+
const andIndex = findTopLevelOperator(condition, '&&');
|
|
318
|
+
if (andIndex !== -1) {
|
|
319
|
+
const left = condition.slice(0, andIndex).trim();
|
|
320
|
+
const right = condition.slice(andIndex + 2).trim();
|
|
321
|
+
return evaluateConditionSafe(left, input, state) && evaluateConditionSafe(right, input, state);
|
|
322
|
+
}
|
|
323
|
+
// Handle comparisons
|
|
324
|
+
const comparisonOps = ['===', '!==', '==', '!=', '>=', '<=', '>', '<'];
|
|
325
|
+
for (const op of comparisonOps) {
|
|
326
|
+
const opIndex = findTopLevelOperator(condition, op);
|
|
327
|
+
if (opIndex !== -1) {
|
|
328
|
+
const left = evaluateValue(condition.slice(0, opIndex).trim(), input, state);
|
|
329
|
+
const right = evaluateValue(condition.slice(opIndex + op.length).trim(), input, state);
|
|
330
|
+
return compareValues(left, right, op);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Handle negation (higher precedence than comparisons)
|
|
334
|
+
if (condition.startsWith('!')) {
|
|
335
|
+
return !evaluateConditionSafe(condition.slice(1).trim(), input, state);
|
|
336
|
+
}
|
|
337
|
+
// Handle parentheses (highest precedence)
|
|
338
|
+
if (hasEnclosingParens(condition)) {
|
|
339
|
+
const inner = condition.slice(1, -1);
|
|
340
|
+
return evaluateConditionSafe(inner, input, state);
|
|
341
|
+
}
|
|
342
|
+
// If we get here, it's a simple value
|
|
343
|
+
const value = evaluateValue(condition, input, state);
|
|
344
|
+
return Boolean(value);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Find index of operator at top level (not inside parentheses or string literals)
|
|
348
|
+
*/
|
|
349
|
+
function findTopLevelOperator(expr, op) {
|
|
350
|
+
let parenDepth = 0;
|
|
351
|
+
let inString = false;
|
|
352
|
+
let stringChar = '';
|
|
353
|
+
let escapeNext = false;
|
|
354
|
+
for (let i = 0; i <= expr.length - op.length; i++) {
|
|
355
|
+
const char = expr[i];
|
|
356
|
+
if (escapeNext) {
|
|
357
|
+
escapeNext = false;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (char === '\\') {
|
|
361
|
+
escapeNext = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (!inString && (char === '"' || char === "'")) {
|
|
365
|
+
inString = true;
|
|
366
|
+
stringChar = char;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (inString && char === stringChar) {
|
|
370
|
+
inString = false;
|
|
371
|
+
stringChar = '';
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (inString)
|
|
375
|
+
continue;
|
|
376
|
+
if (char === '(')
|
|
377
|
+
parenDepth++;
|
|
378
|
+
if (char === ')')
|
|
379
|
+
parenDepth--;
|
|
380
|
+
if (parenDepth === 0 && expr.slice(i, i + op.length) === op) {
|
|
381
|
+
return i;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return -1;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Check if expression is wrapped in enclosing parentheses
|
|
388
|
+
* (e.g., "(A && B)" returns true, "(A) && (B)" returns false)
|
|
389
|
+
*/
|
|
390
|
+
function hasEnclosingParens(expr) {
|
|
391
|
+
expr = expr.trim();
|
|
392
|
+
if (!expr.startsWith('(') || !expr.endsWith(')')) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
// Check if the opening and closing parentheses enclose the entire expression
|
|
396
|
+
let depth = 0;
|
|
397
|
+
let inString = false;
|
|
398
|
+
let stringChar = '';
|
|
399
|
+
let escapeNext = false;
|
|
400
|
+
for (let i = 0; i < expr.length; i++) {
|
|
401
|
+
const char = expr[i];
|
|
402
|
+
if (escapeNext) {
|
|
403
|
+
escapeNext = false;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (char === '\\') {
|
|
407
|
+
escapeNext = true;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (!inString && (char === '"' || char === "'")) {
|
|
411
|
+
inString = true;
|
|
412
|
+
stringChar = char;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (inString && char === stringChar) {
|
|
416
|
+
inString = false;
|
|
417
|
+
stringChar = '';
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (inString)
|
|
421
|
+
continue;
|
|
422
|
+
if (char === '(') {
|
|
423
|
+
depth++;
|
|
424
|
+
// First paren is at index 0
|
|
425
|
+
if (i === 0)
|
|
426
|
+
depth = 1;
|
|
427
|
+
}
|
|
428
|
+
if (char === ')') {
|
|
429
|
+
depth--;
|
|
430
|
+
// Last paren should be at the last index
|
|
431
|
+
if (depth === 0 && i === expr.length - 1) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
if (depth === 0 && i < expr.length - 1) {
|
|
435
|
+
// Found closing paren before end of expression
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Evaluate a simple value (property access or literal)
|
|
444
|
+
*/
|
|
445
|
+
function evaluateValue(expr, input, state) {
|
|
446
|
+
expr = expr.trim();
|
|
447
|
+
// String literals (with proper handling of escaped quotes)
|
|
448
|
+
// Use stricter check - ensure entire expression is a quoted string
|
|
449
|
+
const stringMatch = expr.match(/^(["'])(?:(?=(\\?))\2.)*?\1$/);
|
|
450
|
+
if (stringMatch) {
|
|
451
|
+
const quote = stringMatch[1];
|
|
452
|
+
if (quote === '"') {
|
|
453
|
+
// Use JSON.parse for double-quoted strings (handles all JSON escape sequences correctly)
|
|
454
|
+
try {
|
|
455
|
+
return JSON.parse(expr);
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
throw new Error(`Invalid string literal: "${expr}". Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
// Single-quoted strings: convert to double-quoted and use JSON.parse
|
|
463
|
+
// Need to handle both \' and \" escape sequences
|
|
464
|
+
let inner = expr.slice(1, -1);
|
|
465
|
+
// Replace \' with ' (unescape single quotes)
|
|
466
|
+
inner = inner.replace(/\\'/g, "'");
|
|
467
|
+
// Replace \" with " (unescape double quotes)
|
|
468
|
+
inner = inner.replace(/\\"/g, '"');
|
|
469
|
+
// Now escape any double quotes and backslashes for JSON
|
|
470
|
+
const converted = `"${inner.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
471
|
+
try {
|
|
472
|
+
return JSON.parse(converted);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
throw new Error(`Invalid string literal: "${expr}". Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Number literals (more permissive regex)
|
|
480
|
+
if (/^-?\d*\.?\d+(?:[eE][+-]?\d+)?$/.test(expr)) {
|
|
481
|
+
return Number.parseFloat(expr);
|
|
482
|
+
}
|
|
483
|
+
// Boolean literals
|
|
484
|
+
if (expr === 'true')
|
|
485
|
+
return true;
|
|
486
|
+
if (expr === 'false')
|
|
487
|
+
return false;
|
|
488
|
+
if (expr === 'null')
|
|
489
|
+
return null;
|
|
490
|
+
// Property access: input.foo or state.bar.baz
|
|
491
|
+
if (expr.startsWith('input.')) {
|
|
492
|
+
return getNestedProperty(input, expr.slice(6));
|
|
493
|
+
}
|
|
494
|
+
if (expr.startsWith('state.')) {
|
|
495
|
+
return getNestedProperty(state, expr.slice(6));
|
|
496
|
+
}
|
|
497
|
+
// If we get here, the expression is not recognized
|
|
498
|
+
throw new Error(`Unrecognized expression in condition: "${expr}". Valid expressions are: string literals, numbers, boolean literals, null, or property access like "input.foo" or "state.bar"`);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get nested property from object
|
|
502
|
+
*/
|
|
503
|
+
function getNestedProperty(obj, path) {
|
|
504
|
+
const parts = path.split('.');
|
|
505
|
+
let current = obj;
|
|
506
|
+
for (const part of parts) {
|
|
507
|
+
if (current == null)
|
|
508
|
+
return undefined;
|
|
509
|
+
if (typeof current !== 'object')
|
|
510
|
+
return undefined;
|
|
511
|
+
current = current[part];
|
|
512
|
+
}
|
|
513
|
+
return current;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Compare two values using the specified operator
|
|
517
|
+
* For comparison operators, we assume the values are comparable (strings, numbers, etc.)
|
|
518
|
+
*
|
|
519
|
+
* NOTE: Using 'as any' for comparisons because values are typed as 'unknown'
|
|
520
|
+
* This is an unsafe operation that relies on runtime behavior (string/number comparisons work)
|
|
521
|
+
* but violates type safety. Using 'as any' explicitly acknowledges this limitation.
|
|
522
|
+
*/
|
|
523
|
+
function compareValues(left, right, op) {
|
|
524
|
+
switch (op) {
|
|
525
|
+
case '===':
|
|
526
|
+
return left === right;
|
|
527
|
+
case '!==':
|
|
528
|
+
return left !== right;
|
|
529
|
+
case '==':
|
|
530
|
+
return Object.is(left, right);
|
|
531
|
+
case '!=':
|
|
532
|
+
return !Object.is(left, right);
|
|
533
|
+
case '>=':
|
|
534
|
+
return left >= right;
|
|
535
|
+
case '<=':
|
|
536
|
+
return left <= right;
|
|
537
|
+
case '>':
|
|
538
|
+
return left > right;
|
|
539
|
+
case '<':
|
|
540
|
+
return left < right;
|
|
541
|
+
default:
|
|
542
|
+
throw new Error(`Unknown comparison operator: ${op}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function createRunWorkflowFn(args) {
|
|
546
|
+
return async (subWorkflowId, subInput) => {
|
|
547
|
+
const mergedInput = { ...args.input, ...args.state, ...(subInput ?? {}) };
|
|
548
|
+
return await args.runInternal(subWorkflowId, mergedInput, args.context, args.state);
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async function executeStepWithAgent(stepDef, workflowId, input, state, context, options, runInternal) {
|
|
552
|
+
const tools = context.tools;
|
|
553
|
+
if (typeof tools.generateText !== 'function' || typeof tools.invokeTool !== 'function' || typeof tools.taskEvent !== 'function') {
|
|
554
|
+
throw new Error(`Step '${stepDef.id}' in workflow '${workflowId}' requires agent execution, but AgentToolRegistry tools are not available.`);
|
|
555
|
+
}
|
|
556
|
+
if (!options.toolInfo) {
|
|
557
|
+
throw new Error(`Step '${stepDef.id}' in workflow '${workflowId}' requires agent execution, but no toolInfo was provided to DynamicWorkflowRunner.`);
|
|
558
|
+
}
|
|
559
|
+
const rawAllowedToolNames = stepDef.tools;
|
|
560
|
+
let toolsForAgent;
|
|
561
|
+
if (rawAllowedToolNames) {
|
|
562
|
+
const expandedToolNames = new Set();
|
|
563
|
+
let includeAll = false;
|
|
564
|
+
for (const name of rawAllowedToolNames) {
|
|
565
|
+
if (name === 'all') {
|
|
566
|
+
includeAll = true;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (Object.hasOwn(TOOL_GROUPS, name)) {
|
|
570
|
+
for (const tool of TOOL_GROUPS[name]) {
|
|
571
|
+
expandedToolNames.add(tool);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
expandedToolNames.add(name);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (includeAll) {
|
|
579
|
+
toolsForAgent = [...options.toolInfo];
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
toolsForAgent = options.toolInfo.filter((t) => expandedToolNames.has(t.name));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
toolsForAgent = [...options.toolInfo];
|
|
587
|
+
}
|
|
588
|
+
if (!rawAllowedToolNames || rawAllowedToolNames.includes('all') || rawAllowedToolNames.includes('runWorkflow')) {
|
|
589
|
+
toolsForAgent.push({
|
|
590
|
+
name: 'runWorkflow',
|
|
591
|
+
description: 'Run a named sub-workflow defined in the current workflow file.',
|
|
592
|
+
parameters: z.object({
|
|
593
|
+
workflowId: z.string().describe('Sub-workflow id to run'),
|
|
594
|
+
input: z.any().nullish().describe('Optional input object for the sub-workflow'),
|
|
595
|
+
}),
|
|
596
|
+
handler: async () => {
|
|
597
|
+
return { success: false, message: { type: 'error-text', value: 'runWorkflow is virtual.' } };
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const allowedToolNameSet = new Set(toolsForAgent.map((t) => t.name));
|
|
602
|
+
context.logger.debug(`[Agent] Available tools for step '${stepDef.id}': ${toolsForAgent.map((t) => t.name).join(', ')}`);
|
|
603
|
+
const systemPrompt = options.stepSystemPrompt?.({ workflowId, step: stepDef, input, state }) ??
|
|
604
|
+
[
|
|
605
|
+
`You are an AI assistant executing a workflow step.`,
|
|
606
|
+
'',
|
|
607
|
+
'# Instructions',
|
|
608
|
+
'- Execute the task defined in the user message.',
|
|
609
|
+
'- Use the provided tools to accomplish the task.',
|
|
610
|
+
'- Return the step output as valid JSON in markdown.',
|
|
611
|
+
'- Do not ask for user input. If information is missing, make a reasonable assumption or fail.',
|
|
612
|
+
]
|
|
613
|
+
.filter(Boolean)
|
|
614
|
+
.join('\n');
|
|
615
|
+
const userContent = [
|
|
616
|
+
`Workflow: ${workflowId}`,
|
|
617
|
+
`Step: ${stepDef.id}`,
|
|
618
|
+
`Task: ${stepDef.task}`,
|
|
619
|
+
stepDef.expected_outcome ? `Expected outcome: ${stepDef.expected_outcome}` : '',
|
|
620
|
+
`Workflow Input: ${JSON.stringify(input)}`,
|
|
621
|
+
`Current State: ${JSON.stringify(state)}`,
|
|
622
|
+
]
|
|
623
|
+
.filter(Boolean)
|
|
624
|
+
.join('\n');
|
|
625
|
+
const runWorkflow = createRunWorkflowFn({ input, state, context, runInternal });
|
|
626
|
+
const agentTools = {
|
|
627
|
+
generateText: tools.generateText.bind(tools),
|
|
628
|
+
taskEvent: tools.taskEvent.bind(tools),
|
|
629
|
+
invokeTool: async ({ toolName, input: toolInput }) => {
|
|
630
|
+
if (!allowedToolNameSet.has(toolName)) {
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
message: { type: 'error-text', value: `Tool '${toolName}' is not allowed in this step.` },
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (toolName === 'runWorkflow') {
|
|
637
|
+
// Type guard for runWorkflow input
|
|
638
|
+
const runWorkflowInput = toolInput;
|
|
639
|
+
const subWorkflowId = runWorkflowInput?.workflowId;
|
|
640
|
+
const subInput = runWorkflowInput?.input;
|
|
641
|
+
if (typeof subWorkflowId !== 'string') {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
message: { type: 'error-text', value: 'runWorkflow.workflowId must be a string.' },
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const output = await runWorkflow(subWorkflowId, subInput);
|
|
649
|
+
const jsonResult = { type: 'json', value: output };
|
|
650
|
+
return { success: true, message: jsonResult };
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
message: { type: 'error-text', value: error instanceof Error ? error.message : String(error) },
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return await tools.invokeTool({ toolName, input: toolInput });
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
const result = await agentWorkflow({
|
|
663
|
+
tools: toolsForAgent,
|
|
664
|
+
systemPrompt,
|
|
665
|
+
userMessage: [{ role: 'user', content: userContent }],
|
|
666
|
+
maxToolRoundTrips: options.maxToolRoundTrips,
|
|
667
|
+
model: options.model,
|
|
668
|
+
}, { ...context, tools: agentTools });
|
|
669
|
+
if (result.type === 'Exit') {
|
|
670
|
+
// Prefer structured object output
|
|
671
|
+
if (result.object !== undefined) {
|
|
672
|
+
return result.object;
|
|
673
|
+
}
|
|
674
|
+
// Try to parse JSON from message
|
|
675
|
+
const parsed = parseJsonFromMarkdown(result.message);
|
|
676
|
+
if (parsed.success) {
|
|
677
|
+
return parsed.data;
|
|
678
|
+
}
|
|
679
|
+
// If message is a simple string, wrap it in an object for consistency if enabled
|
|
680
|
+
if (options.wrapAgentResultInObject) {
|
|
681
|
+
context.logger.warn(`[Agent] Step '${stepDef.id}' returned plain text instead of JSON. Wrapping in {result: ...}`);
|
|
682
|
+
return { result: result.message };
|
|
683
|
+
}
|
|
684
|
+
return result.message;
|
|
685
|
+
}
|
|
686
|
+
if (result.type === 'Error') {
|
|
687
|
+
throw new Error(`Agent step '${stepDef.id}' in workflow '${workflowId}' failed: ${result.error?.message || 'Unknown error'}`);
|
|
688
|
+
}
|
|
689
|
+
if (result.type === 'UsageExceeded') {
|
|
690
|
+
throw new Error(`Agent step '${stepDef.id}' in workflow '${workflowId}' exceeded usage limits (tokens or rounds)`);
|
|
691
|
+
}
|
|
692
|
+
// Exhaustive check: TypeScript should ensure all result types are handled above
|
|
693
|
+
const _exhaustiveCheck = result;
|
|
694
|
+
throw new Error(`Agent step '${stepDef.id}' in workflow '${workflowId}' exited unexpectedly with unhandled type`);
|
|
695
|
+
}
|
|
696
|
+
async function executeStepWithTimeout(stepDef, workflowId, input, state, context, options, runInternal) {
|
|
697
|
+
const executeStepLogic = async () => {
|
|
698
|
+
context.logger.debug(`[Step] Executing step '${stepDef.id}' with agent`);
|
|
699
|
+
const result = await executeStepWithAgent(stepDef, workflowId, input, state, context, options, runInternal);
|
|
700
|
+
context.logger.debug(`[Step] Agent execution completed for step '${stepDef.id}'`);
|
|
701
|
+
return result;
|
|
702
|
+
};
|
|
703
|
+
// Apply timeout if specified
|
|
704
|
+
if (stepDef.timeout && stepDef.timeout > 0) {
|
|
705
|
+
context.logger.debug(`[Step] Step '${stepDef.id}' has timeout of ${stepDef.timeout}ms`);
|
|
706
|
+
let timeoutId;
|
|
707
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
708
|
+
timeoutId = setTimeout(() => reject(new Error(`Step '${stepDef.id}' in workflow '${workflowId}' timed out after ${stepDef.timeout}ms`)), stepDef.timeout);
|
|
709
|
+
});
|
|
710
|
+
try {
|
|
711
|
+
return await Promise.race([executeStepLogic(), timeoutPromise]);
|
|
712
|
+
}
|
|
713
|
+
finally {
|
|
714
|
+
if (timeoutId)
|
|
715
|
+
clearTimeout(timeoutId);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return await executeStepLogic();
|
|
719
|
+
}
|
|
720
|
+
async function executeStep(stepDef, workflowId, input, state, context, options, runInternal) {
|
|
721
|
+
const result = await executeStepWithTimeout(stepDef, workflowId, input, state, context, options, runInternal);
|
|
722
|
+
// Validate output against schema if provided
|
|
723
|
+
if (stepDef.outputSchema) {
|
|
724
|
+
try {
|
|
725
|
+
context.logger.debug(`[Step] Validating output for step '${stepDef.id}' against schema`);
|
|
726
|
+
// Convert JSON Schema to Zod schema
|
|
727
|
+
// Type assertion is safe here as JsonSchema structure is validated by convertJsonSchemaToZod
|
|
728
|
+
const zodSchema = convertJsonSchemaToZod(stepDef.outputSchema);
|
|
729
|
+
// Validate the result
|
|
730
|
+
const validationResult = zodSchema.safeParse(result);
|
|
731
|
+
if (!validationResult.success) {
|
|
732
|
+
const errorDetails = validationResult.error.issues.map((e) => ` - ${e.path.join('.') || 'root'}: ${e.message}`).join('\n');
|
|
733
|
+
throw new Error(`Output does not match expected schema:\n${errorDetails}`);
|
|
734
|
+
}
|
|
735
|
+
context.logger.debug(`[Step] Output validation successful for step '${stepDef.id}'`);
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
throw new Error(`Step '${stepDef.id}' in workflow '${workflowId}' output validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Check if a step is a break statement
|
|
745
|
+
*/
|
|
746
|
+
function isBreakStep(step) {
|
|
747
|
+
return typeof step === 'object' && step !== null && 'break' in step && step.break === true;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Check if a step is a continue statement
|
|
751
|
+
*/
|
|
752
|
+
function isContinueStep(step) {
|
|
753
|
+
return typeof step === 'object' && step !== null && 'continue' in step && step.continue === true;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Check if a step is a while loop
|
|
757
|
+
*/
|
|
758
|
+
function isWhileLoopStep(step) {
|
|
759
|
+
return typeof step === 'object' && step !== null && 'while' in step;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Check if a step is an if/else branch
|
|
763
|
+
*/
|
|
764
|
+
function isIfElseStep(step) {
|
|
765
|
+
return typeof step === 'object' && step !== null && 'if' in step;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Check if a step is a try/catch block
|
|
769
|
+
*/
|
|
770
|
+
function isTryCatchStep(step) {
|
|
771
|
+
return typeof step === 'object' && step !== null && 'try' in step;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Store step output in state if output key is specified
|
|
775
|
+
*/
|
|
776
|
+
function storeStepOutput(step, result, state) {
|
|
777
|
+
if ('id' in step && step.output) {
|
|
778
|
+
const outputKey = step.output;
|
|
779
|
+
state[outputKey] = result;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get step ID for logging purposes
|
|
784
|
+
*/
|
|
785
|
+
function getStepId(step) {
|
|
786
|
+
if ('id' in step && step.id) {
|
|
787
|
+
return step.id;
|
|
788
|
+
}
|
|
789
|
+
if (isWhileLoopStep(step)) {
|
|
790
|
+
return 'while';
|
|
791
|
+
}
|
|
792
|
+
if (isIfElseStep(step)) {
|
|
793
|
+
return 'if';
|
|
794
|
+
}
|
|
795
|
+
if (isTryCatchStep(step)) {
|
|
796
|
+
return 'try';
|
|
797
|
+
}
|
|
798
|
+
return 'control';
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Execute a single control flow step (basic step, while loop, if/else, break, continue)
|
|
802
|
+
*/
|
|
803
|
+
async function executeControlFlowStep(step, workflowId, input, state, context, options, runInternal, loopDepth, breakFlag, continueFlag) {
|
|
804
|
+
// Handle break statement
|
|
805
|
+
if (isBreakStep(step)) {
|
|
806
|
+
if (loopDepth === 0) {
|
|
807
|
+
throw new Error(`'break' statement found outside of a loop in workflow '${workflowId}'`);
|
|
808
|
+
}
|
|
809
|
+
context.logger.debug(`[ControlFlow] Executing break statement (loop depth: ${loopDepth})`);
|
|
810
|
+
return { result: undefined, shouldBreak: true, shouldContinue: false };
|
|
811
|
+
}
|
|
812
|
+
// Handle continue statement
|
|
813
|
+
if (isContinueStep(step)) {
|
|
814
|
+
if (loopDepth === 0) {
|
|
815
|
+
throw new Error(`'continue' statement found outside of a loop in workflow '${workflowId}'`);
|
|
816
|
+
}
|
|
817
|
+
context.logger.debug(`[ControlFlow] Executing continue statement (loop depth: ${loopDepth})`);
|
|
818
|
+
return { result: undefined, shouldBreak: false, shouldContinue: true };
|
|
819
|
+
}
|
|
820
|
+
// Handle while loop
|
|
821
|
+
if (isWhileLoopStep(step)) {
|
|
822
|
+
context.logger.info(`[ControlFlow] Executing while loop '${step.id}'`);
|
|
823
|
+
context.logger.debug(`[ControlFlow] Condition: ${step.while.condition}`);
|
|
824
|
+
context.logger.debug(`[ControlFlow] Loop body has ${step.while.steps.length} step(s)`);
|
|
825
|
+
let iterationCount = 0;
|
|
826
|
+
let loopResult;
|
|
827
|
+
while (true) {
|
|
828
|
+
iterationCount++;
|
|
829
|
+
if (iterationCount > MAX_WHILE_LOOP_ITERATIONS) {
|
|
830
|
+
throw new Error(`While loop '${step.id}' in workflow '${workflowId}' exceeded maximum iteration limit of ${MAX_WHILE_LOOP_ITERATIONS}`);
|
|
831
|
+
}
|
|
832
|
+
// Evaluate condition
|
|
833
|
+
const conditionResult = evaluateCondition(step.while.condition, input, state, options.allowUnsafeCodeExecution);
|
|
834
|
+
context.logger.debug(`[ControlFlow] While loop '${step.id}' iteration ${iterationCount}: condition = ${conditionResult}`);
|
|
835
|
+
if (!conditionResult) {
|
|
836
|
+
context.logger.info(`[ControlFlow] While loop '${step.id}' terminated after ${iterationCount - 1} iteration(s)`);
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
// Execute loop body steps
|
|
840
|
+
for (const bodyStep of step.while.steps) {
|
|
841
|
+
const { result, shouldBreak, shouldContinue } = await executeControlFlowStep(bodyStep, workflowId, input, state, context, options, runInternal, loopDepth + 1, breakFlag, continueFlag);
|
|
842
|
+
if (shouldBreak) {
|
|
843
|
+
context.logger.debug(`[ControlFlow] Breaking from while loop '${step.id}'`);
|
|
844
|
+
breakFlag.value = false;
|
|
845
|
+
return { result: loopResult, shouldBreak: false, shouldContinue: false };
|
|
846
|
+
}
|
|
847
|
+
if (shouldContinue) {
|
|
848
|
+
context.logger.debug(`[ControlFlow] Continuing to next iteration of while loop '${step.id}'`);
|
|
849
|
+
continueFlag.value = false;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
// Store output if specified
|
|
853
|
+
storeStepOutput(bodyStep, result, state);
|
|
854
|
+
// Last result becomes loop result
|
|
855
|
+
loopResult = result;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// Store loop output if specified
|
|
859
|
+
const outputKey = step.output ?? step.id;
|
|
860
|
+
state[outputKey] = loopResult;
|
|
861
|
+
context.logger.debug(`[ControlFlow] While loop '${step.id}' stored output as '${outputKey}'`);
|
|
862
|
+
return { result: loopResult, shouldBreak: false, shouldContinue: false };
|
|
863
|
+
}
|
|
864
|
+
// Handle if/else branch
|
|
865
|
+
if (isIfElseStep(step)) {
|
|
866
|
+
const ifStep = step;
|
|
867
|
+
context.logger.info(`[ControlFlow] Executing if/else branch '${ifStep.id}'`);
|
|
868
|
+
context.logger.debug(`[ControlFlow] Condition: ${ifStep.if.condition}`);
|
|
869
|
+
context.logger.debug(`[ControlFlow] Then branch has ${ifStep.if.thenBranch.length} step(s)`);
|
|
870
|
+
if (ifStep.if.elseBranch) {
|
|
871
|
+
context.logger.debug(`[ControlFlow] Else branch has ${ifStep.if.elseBranch.length} step(s)`);
|
|
872
|
+
}
|
|
873
|
+
const conditionResult = evaluateCondition(ifStep.if.condition, input, state, options.allowUnsafeCodeExecution);
|
|
874
|
+
context.logger.debug(`[ControlFlow] If/else '${ifStep.id}' condition = ${conditionResult}`);
|
|
875
|
+
const branchSteps = conditionResult ? ifStep.if.thenBranch : (ifStep.if.elseBranch ?? []);
|
|
876
|
+
const branchName = conditionResult ? 'then' : ifStep.if.elseBranch ? 'else' : 'else (empty)';
|
|
877
|
+
context.logger.info(`[ControlFlow] Taking '${branchName}' branch of '${ifStep.id}'`);
|
|
878
|
+
let branchResult;
|
|
879
|
+
for (const branchStep of branchSteps) {
|
|
880
|
+
const { result, shouldBreak, shouldContinue } = await executeControlFlowStep(branchStep, workflowId, input, state, context, options, runInternal, loopDepth, breakFlag, continueFlag);
|
|
881
|
+
// Propagate break/continue from within branches
|
|
882
|
+
if (shouldBreak || shouldContinue) {
|
|
883
|
+
return { result, shouldBreak, shouldContinue };
|
|
884
|
+
}
|
|
885
|
+
// Store output if specified
|
|
886
|
+
storeStepOutput(branchStep, result, state);
|
|
887
|
+
// Last result becomes branch result
|
|
888
|
+
branchResult = result;
|
|
889
|
+
}
|
|
890
|
+
// Store branch output if specified
|
|
891
|
+
const outputKey = ifStep.output ?? ifStep.id;
|
|
892
|
+
state[outputKey] = branchResult;
|
|
893
|
+
context.logger.debug(`[ControlFlow] If/else '${ifStep.id}' stored output as '${outputKey}'`);
|
|
894
|
+
return { result: branchResult, shouldBreak: false, shouldContinue: false };
|
|
895
|
+
}
|
|
896
|
+
// Handle try/catch block
|
|
897
|
+
if (isTryCatchStep(step)) {
|
|
898
|
+
const tryStep = step;
|
|
899
|
+
context.logger.info(`[ControlFlow] Executing try/catch block '${tryStep.id}'`);
|
|
900
|
+
context.logger.debug(`[ControlFlow] Try block has ${tryStep.try.trySteps.length} step(s)`);
|
|
901
|
+
context.logger.debug(`[ControlFlow] Catch block has ${tryStep.try.catchSteps.length} step(s)`);
|
|
902
|
+
let tryResult;
|
|
903
|
+
let caughtError;
|
|
904
|
+
try {
|
|
905
|
+
// Execute try steps
|
|
906
|
+
for (const tryStepItem of tryStep.try.trySteps) {
|
|
907
|
+
const { result } = await executeControlFlowStep(tryStepItem, workflowId, input, state, context, options, runInternal, loopDepth, breakFlag, continueFlag);
|
|
908
|
+
// Store output if specified
|
|
909
|
+
storeStepOutput(tryStepItem, result, state);
|
|
910
|
+
// Last result becomes try result
|
|
911
|
+
tryResult = result;
|
|
912
|
+
}
|
|
913
|
+
// Store try/catch output if specified
|
|
914
|
+
const outputKey = tryStep.output ?? tryStep.id;
|
|
915
|
+
state[outputKey] = tryResult;
|
|
916
|
+
context.logger.debug(`[ControlFlow] Try/catch '${tryStep.id}' completed successfully`);
|
|
917
|
+
return { result: tryResult, shouldBreak: false, shouldContinue: false };
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
921
|
+
context.logger.warn(`[ControlFlow] Try/catch '${tryStep.id}' caught error: ${caughtError.message}`);
|
|
922
|
+
// Execute catch steps
|
|
923
|
+
let catchResult;
|
|
924
|
+
for (const catchStepItem of tryStep.try.catchSteps) {
|
|
925
|
+
const { result } = await executeControlFlowStep(catchStepItem, workflowId, input, state, context, options, runInternal, loopDepth, breakFlag, continueFlag);
|
|
926
|
+
// Store output if specified
|
|
927
|
+
storeStepOutput(catchStepItem, result, state);
|
|
928
|
+
// Last result becomes catch result
|
|
929
|
+
catchResult = result;
|
|
930
|
+
}
|
|
931
|
+
// Store try/catch output if specified
|
|
932
|
+
const outputKey = tryStep.output ?? tryStep.id;
|
|
933
|
+
state[outputKey] = catchResult;
|
|
934
|
+
context.logger.debug(`[ControlFlow] Try/catch '${tryStep.id}' caught error and executed catch block`);
|
|
935
|
+
return { result: catchResult, shouldBreak: false, shouldContinue: false };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// Handle basic step (must be WorkflowStepDefinition at this point)
|
|
939
|
+
const stepDef = step;
|
|
940
|
+
const stepResult = await executeStep(stepDef, workflowId, input, state, context, options, runInternal);
|
|
941
|
+
return { result: stepResult, shouldBreak: false, shouldContinue: false };
|
|
942
|
+
}
|
|
943
|
+
export function createDynamicWorkflow(definition, options = {}) {
|
|
944
|
+
if (typeof definition === 'string') {
|
|
945
|
+
const res = parseDynamicWorkflowDefinition(definition);
|
|
946
|
+
if (!res.success) {
|
|
947
|
+
throw new Error(res.error);
|
|
948
|
+
}
|
|
949
|
+
definition = res.definition;
|
|
950
|
+
}
|
|
951
|
+
const runInternal = async (workflowId, input, context, inheritedState) => {
|
|
952
|
+
const workflow = definition.workflows[workflowId];
|
|
953
|
+
if (!workflow) {
|
|
954
|
+
const builtIn = options.builtInWorkflows?.[workflowId];
|
|
955
|
+
if (builtIn) {
|
|
956
|
+
context.logger.info(`[Workflow] Delegating to built-in workflow '${workflowId}'`);
|
|
957
|
+
// Built-in workflows are typed as WorkflowFn<any, any, any>, so we need to cast context
|
|
958
|
+
// TODO: Improve built-in workflow typing to preserve context type constraints
|
|
959
|
+
return await builtIn(input, context);
|
|
960
|
+
}
|
|
961
|
+
throw new Error(`Workflow '${workflowId}' not found`);
|
|
962
|
+
}
|
|
963
|
+
// Validate inputs and apply defaults
|
|
964
|
+
const validatedInput = validateAndApplyDefaults(workflowId, workflow, input);
|
|
965
|
+
context.logger.info(`[Workflow] Starting workflow '${workflowId}'`);
|
|
966
|
+
context.logger.debug(`[Workflow] Input: ${JSON.stringify(validatedInput)}`);
|
|
967
|
+
context.logger.debug(`[Workflow] Inherited state: ${JSON.stringify(inheritedState)}`);
|
|
968
|
+
context.logger.debug(`[Workflow] Steps: ${workflow.steps.map((s) => ('id' in s ? s.id : '<control flow>')).join(', ')}`);
|
|
969
|
+
const state = { ...inheritedState };
|
|
970
|
+
let lastOutput;
|
|
971
|
+
const breakFlag = { value: false };
|
|
972
|
+
const continueFlag = { value: false };
|
|
973
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
974
|
+
const stepDef = workflow.steps[i];
|
|
975
|
+
const stepId = getStepId(stepDef);
|
|
976
|
+
context.logger.info(`[Workflow] Step ${i + 1}/${workflow.steps.length}: ${stepId}`);
|
|
977
|
+
// Execute control flow step
|
|
978
|
+
const { result } = await executeControlFlowStep(stepDef, workflowId, validatedInput, state, context, options, runInternal, 0, // loop depth
|
|
979
|
+
breakFlag, continueFlag);
|
|
980
|
+
lastOutput = result;
|
|
981
|
+
// Store output if specified
|
|
982
|
+
storeStepOutput(stepDef, result, state);
|
|
983
|
+
if ('id' in stepDef && stepDef.output) {
|
|
984
|
+
context.logger.debug(`[Workflow] Step output stored as '${stepDef.output}': ${typeof lastOutput === 'object' ? JSON.stringify(lastOutput).substring(0, 200) : lastOutput}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
context.logger.info(`[Workflow] Completed workflow '${workflowId}'`);
|
|
988
|
+
if (workflow.output) {
|
|
989
|
+
context.logger.debug(`[Workflow] Returning output field: ${workflow.output}`);
|
|
990
|
+
return state[workflow.output];
|
|
991
|
+
}
|
|
992
|
+
context.logger.debug(`[Workflow] Returning full state with keys: ${Object.keys(state).join(', ')}`);
|
|
993
|
+
return state;
|
|
994
|
+
};
|
|
995
|
+
return async (workflowId, input, context) => {
|
|
996
|
+
return await runInternal(workflowId, input, context, {});
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
//# sourceMappingURL=dynamic.js.map
|