@pikku/core 0.12.8 → 0.12.10

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/env.d.ts +1 -0
  3. package/dist/env.js +1 -0
  4. package/dist/errors/errors.d.ts +14 -0
  5. package/dist/errors/errors.js +27 -0
  6. package/dist/function/function-runner.d.ts +3 -1
  7. package/dist/function/function-runner.js +5 -1
  8. package/dist/handle-error.js +2 -1
  9. package/dist/middleware/auth-bearer.js +10 -1
  10. package/dist/middleware/auth-cookie.js +34 -25
  11. package/dist/middleware/timeout.js +11 -5
  12. package/dist/pikku-state.js +1 -1
  13. package/dist/services/credential-service.d.ts +40 -0
  14. package/dist/services/credential-service.js +1 -0
  15. package/dist/services/credential-wire-service.d.ts +13 -0
  16. package/dist/services/credential-wire-service.js +31 -0
  17. package/dist/services/index.d.ts +5 -0
  18. package/dist/services/index.js +3 -0
  19. package/dist/services/local-content.d.ts +7 -4
  20. package/dist/services/local-content.js +42 -12
  21. package/dist/services/local-credential-service.d.ts +10 -0
  22. package/dist/services/local-credential-service.js +33 -0
  23. package/dist/services/local-secrets.js +2 -2
  24. package/dist/services/typed-credential-service.d.ts +27 -0
  25. package/dist/services/typed-credential-service.js +40 -0
  26. package/dist/testing/service-tests.d.ts +8 -0
  27. package/dist/testing/service-tests.js +107 -1
  28. package/dist/types/core.types.d.ts +7 -0
  29. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
  30. package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
  31. package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
  32. package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
  33. package/dist/wirings/channel/local/local-channel-runner.js +11 -6
  34. package/dist/wirings/credential/credential.types.d.ts +24 -0
  35. package/dist/wirings/credential/credential.types.js +1 -0
  36. package/dist/wirings/credential/index.d.ts +3 -0
  37. package/dist/wirings/credential/index.js +2 -0
  38. package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
  39. package/dist/wirings/credential/validate-credential-definitions.js +38 -0
  40. package/dist/wirings/credential/wire-credential.d.ts +48 -0
  41. package/dist/wirings/credential/wire-credential.js +47 -0
  42. package/dist/wirings/http/http-runner.js +8 -2
  43. package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
  44. package/dist/wirings/oauth2/index.d.ts +2 -1
  45. package/dist/wirings/oauth2/index.js +1 -1
  46. package/dist/wirings/oauth2/oauth2-client.js +4 -8
  47. package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
  48. package/dist/wirings/oauth2/oauth2-routes.js +146 -0
  49. package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
  50. package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
  51. package/dist/wirings/workflow/graph/graph-runner.js +87 -7
  52. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  53. package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
  54. package/dist/wirings/workflow/workflow.types.d.ts +4 -0
  55. package/package.json +2 -1
  56. package/src/env.ts +1 -0
  57. package/src/errors/errors.ts +35 -0
  58. package/src/function/function-runner.ts +11 -0
  59. package/src/handle-error.ts +2 -1
  60. package/src/middleware/auth-bearer.ts +10 -1
  61. package/src/middleware/auth-cookie.ts +11 -4
  62. package/src/middleware/timeout.ts +14 -7
  63. package/src/pikku-state.ts +1 -1
  64. package/src/services/credential-service.ts +44 -0
  65. package/src/services/credential-wire-service.ts +44 -0
  66. package/src/services/index.ts +12 -0
  67. package/src/services/local-content.ts +61 -13
  68. package/src/services/local-credential-service.ts +41 -0
  69. package/src/services/local-secrets.test.ts +2 -2
  70. package/src/services/local-secrets.ts +2 -2
  71. package/src/services/typed-credential-service.ts +75 -0
  72. package/src/testing/service-tests.ts +140 -1
  73. package/src/types/core.types.ts +7 -0
  74. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
  75. package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
  76. package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
  77. package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
  78. package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
  79. package/src/wirings/channel/local/local-channel-runner.ts +11 -6
  80. package/src/wirings/credential/credential.types.ts +28 -0
  81. package/src/wirings/credential/index.ts +8 -0
  82. package/src/wirings/credential/validate-credential-definitions.ts +64 -0
  83. package/src/wirings/credential/wire-credential.ts +49 -0
  84. package/src/wirings/http/http-runner.ts +11 -2
  85. package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
  86. package/src/wirings/oauth2/index.ts +2 -1
  87. package/src/wirings/oauth2/oauth2-client.ts +4 -8
  88. package/src/wirings/oauth2/oauth2-routes.ts +234 -0
  89. package/src/wirings/oauth2/oauth2.types.ts +0 -27
  90. package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
  91. package/src/wirings/workflow/graph/graph-runner.ts +117 -8
  92. package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
  93. package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
  94. package/src/wirings/workflow/workflow.types.ts +4 -0
  95. package/tsconfig.tsbuildinfo +1 -1
  96. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
  97. package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
  98. package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
@@ -13,92 +13,222 @@ import {
13
13
  computeEntryNodeIds,
14
14
  } from '../workflow/graph/graph-validation.js'
15
15
 
16
- export function buildDynamicWorkflowInstructions(tools: string[], mode: 'read' | 'always' | 'ask'): string {
17
- if (mode === 'read') {
18
- return (
19
- '\n\n## Existing Workflows\n\n' +
20
- 'You can list and execute previously saved workflows using listAgentWorkflows and executeAgentWorkflow.\n' +
21
- 'Use listAgentWorkflows to discover what\'s available, then executeAgentWorkflow to run them.'
22
- )
16
+ function formatSchemaType(schema: any, depth = 0): string {
17
+ if (!schema) return 'any'
18
+ if (schema.enum)
19
+ return schema.enum.map((v: any) => JSON.stringify(v)).join(' | ')
20
+ if (schema.type === 'array') {
21
+ const itemType = schema.items
22
+ ? formatSchemaType(schema.items, depth + 1)
23
+ : 'any'
24
+ return `${itemType}[]`
23
25
  }
26
+ if (schema.type === 'object' && schema.properties) {
27
+ if (depth > 1) return 'object'
28
+ const fields = Object.entries(schema.properties)
29
+ .map(([k, v]: [string, any]) => `${k}: ${formatSchemaType(v, depth + 1)}`)
30
+ .join(', ')
31
+ return `{${fields}}`
32
+ }
33
+ return schema.type || 'any'
34
+ }
24
35
 
25
- const modeGuidance = mode === 'always'
26
- ? 'When a user requests a complex multi-step task, prefer creating a workflow over making sequential tool calls.\n' +
27
- 'Check if a suitable workflow already exists with listAgentWorkflows first.\n' +
28
- 'If none exists, create one with createAgentWorkflow, save with saveAgentWorkflow, then execute.\n\n'
29
- : 'When you receive a request that would benefit from a reusable multi-step workflow,\n' +
30
- 'suggest creating one to the user. Explain the benefits (reusability, reliability,\n' +
31
- 'can run in background) and wait for confirmation before creating.\n' +
32
- 'Do NOT create workflows automatically — always propose and get user approval first.\n' +
33
- 'For one-off tasks, just use the tools directly.\n\n'
34
-
35
- const toolSchemaLines: string[] = []
36
-
37
- for (const toolName of tools) {
38
- let fnMeta: any
39
- let schemas: Map<string, any>
36
+ function resolveToolMetaForWorkflow(toolName: string): {
37
+ fnMeta: any
38
+ inputSchema: any
39
+ outputSchema: any
40
+ schemas: Map<string, any>
41
+ } | null {
42
+ const resolved = toolName.includes(':') ? resolveNamespace(toolName) : null
43
+ let fnMeta: any
44
+ let schemas: Map<string, any>
45
+
46
+ if (resolved) {
47
+ fnMeta = pikkuState(resolved.package, 'function', 'meta')[resolved.function]
48
+ schemas = pikkuState(resolved.package, 'misc', 'schemas')
49
+ } else {
50
+ const rpcMeta = pikkuState(null, 'rpc', 'meta')
51
+ const pikkuFuncId = rpcMeta[toolName]
52
+ if (!pikkuFuncId) return null
53
+ fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
54
+ schemas = pikkuState(null, 'misc', 'schemas')
55
+ }
40
56
 
41
- const resolved = toolName.includes(':') ? resolveNamespace(toolName) : null
57
+ if (!fnMeta) return null
42
58
 
43
- if (resolved) {
44
- fnMeta = pikkuState(resolved.package, 'function', 'meta')[
45
- resolved.function
46
- ]
47
- schemas = pikkuState(resolved.package, 'misc', 'schemas')
48
- } else {
49
- const rpcMeta = pikkuState(null, 'rpc', 'meta')
50
- const pikkuFuncId = rpcMeta[toolName]
51
- if (!pikkuFuncId) continue
52
- fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
53
- schemas = pikkuState(null, 'misc', 'schemas')
54
- }
59
+ const inputSchema = fnMeta.inputSchemaName
60
+ ? schemas.get(fnMeta.inputSchemaName)
61
+ : null
62
+ const outputSchema = fnMeta.outputSchemaName
63
+ ? schemas.get(fnMeta.outputSchemaName)
64
+ : null
55
65
 
56
- if (!fnMeta) continue
66
+ return { fnMeta, inputSchema, outputSchema, schemas }
67
+ }
57
68
 
58
- const inputSchema = fnMeta.inputSchemaName
59
- ? schemas.get(fnMeta.inputSchemaName)
60
- : null
61
- const outputSchema = fnMeta.outputSchemaName
62
- ? schemas.get(fnMeta.outputSchemaName)
63
- : null
69
+ function buildDetailedToolSchemas(toolNames: string[]): string {
70
+ const lines: string[] = []
71
+ for (const toolName of toolNames) {
72
+ const meta = resolveToolMetaForWorkflow(toolName)
73
+ if (!meta) continue
74
+ const { inputSchema, outputSchema } = meta
64
75
 
65
- const toolDescription = fnMeta.description || fnMeta.title || ''
66
76
  const inputProps = inputSchema?.properties
67
77
  ? Object.entries(inputSchema.properties)
68
78
  .map(
69
79
  ([k, v]: [string, any]) =>
70
- `${k}${inputSchema.required?.includes(k) ? '' : '?'}: ${v.type || 'any'}`
80
+ `${k}${inputSchema.required?.includes(k) ? '' : '?'}: ${formatSchemaType(v)}`
71
81
  )
72
82
  .join(', ')
73
83
  : ''
74
- const outputProps = outputSchema?.properties
75
- ? Object.entries(outputSchema.properties)
76
- .map(([k, v]: [string, any]) => `${k}: ${v.type || 'any'}`)
77
- .join(', ')
78
- : 'any'
84
+ const outputPaths = outputSchema ? collectOutputPaths(outputSchema) : []
85
+
86
+ lines.push(
87
+ `**${toolName}**\n` +
88
+ ` input: {${inputProps}}\n` +
89
+ (outputPaths.length > 0
90
+ ? ` output paths: ${outputPaths.join(', ')}`
91
+ : ` output: void`)
92
+ )
93
+ }
94
+ return lines.join('\n')
95
+ }
96
+
97
+ function collectOutputPaths(schema: any, prefix = ''): string[] {
98
+ if (!schema?.properties) return []
99
+ const paths: string[] = []
100
+ for (const [key, prop] of Object.entries(schema.properties) as [
101
+ string,
102
+ any,
103
+ ][]) {
104
+ const path = prefix ? `${prefix}.${key}` : key
105
+ const type = prop.type || 'any'
106
+ if (prop.type === 'object' && prop.properties) {
107
+ paths.push(...collectOutputPaths(prop, path))
108
+ } else {
109
+ paths.push(`${path}: ${type}`)
110
+ }
111
+ }
112
+ return paths
113
+ }
114
+
115
+ export function buildDynamicWorkflowInstructions(
116
+ tools: string[],
117
+ mode: 'read' | 'always' | 'ask'
118
+ ): string {
119
+ if (mode === 'read') {
120
+ return (
121
+ '\n\n## Existing Workflows\n\n' +
122
+ 'You can list and execute previously saved workflows using listAgentWorkflows and executeAgentWorkflow.\n' +
123
+ "Use listAgentWorkflows to discover what's available, then executeAgentWorkflow to run them."
124
+ )
125
+ }
126
+
127
+ const modeGuidance =
128
+ mode === 'always'
129
+ ? 'When a user requests a complex multi-step task, prefer creating a workflow over making sequential tool calls.\n' +
130
+ 'Check if a suitable workflow already exists with listAgentWorkflows first.\n' +
131
+ 'If none exists, create one with createAgentWorkflow, save with saveAgentWorkflow, then execute.\n\n'
132
+ : 'When you receive a request that would benefit from a reusable multi-step workflow,\n' +
133
+ 'suggest creating one to the user. Explain the benefits (reusability, reliability,\n' +
134
+ 'can run in background) and wait for confirmation before creating.\n' +
135
+ 'Do NOT create workflows automatically — always propose and get user approval first.\n' +
136
+ 'For one-off tasks, just use the tools directly.\n\n'
137
+
138
+ const toolSummaryLines: string[] = []
139
+
140
+ for (const toolName of tools) {
141
+ const meta = resolveToolMetaForWorkflow(toolName)
142
+ if (!meta) continue
143
+ const { fnMeta, inputSchema, outputSchema } = meta
79
144
 
80
- toolSchemaLines.push(
81
- `- ${toolName}(input: {${inputProps}}) → {${outputProps}}${toolDescription ? ` — ${toolDescription}` : ''}`
145
+ const desc = fnMeta.description || fnMeta.title || ''
146
+ const inputFields = inputSchema?.properties
147
+ ? Object.keys(inputSchema.properties).join(', ')
148
+ : ''
149
+ const outputFields = outputSchema?.properties
150
+ ? Object.keys(outputSchema.properties).join(', ')
151
+ : ''
152
+
153
+ toolSummaryLines.push(
154
+ `| \`${toolName}\` | ${desc || '-'} | ${inputFields || '-'} | ${outputFields || '-'} |`
82
155
  )
83
156
  }
84
157
 
85
- return (
86
- '\n\n## Workflow Creation\n\n' +
87
- modeGuidance +
88
- 'You can create workflows that chain your tools together. Use createAgentWorkflow to validate and preview a workflow, then saveAgentWorkflow to save it, then executeAgentWorkflow to run it.\n\n' +
89
- '### Tool Schemas:\n' +
90
- toolSchemaLines.join('\n') +
91
- '\n\n### Workflow Format:\n' +
92
- '- Each node has: rpcName (tool name), input (with $ref to wire outputs), next (flow control), onError\n' +
93
- '- Use {"$ref": "nodeId", "path": "fieldName"} to wire a previous node\'s output field to this node\'s input\n' +
94
- '- Use {"$ref": "trigger", "path": "fieldName"} to extract a field from the workflow\'s trigger input. IMPORTANT: Always include "path" — never pass {"$ref": "trigger"} without "path", or the entire trigger object will be used as the value.\n' +
95
- '- Use {"$ref": "nodeId", "path": "fieldName"} to extract a field from a previous node\'s output. Always include "path" to select the specific field.\n' +
96
- '- next can be a string (single next node), array (parallel), or object {branchKey: nextNode} for branching\n' +
97
- '\n### Example (add todo from trigger title, then list):\n' +
98
- '{"add":{"rpcName":"todos:addTodo","input":{"title":{"$ref":"trigger","path":"title"}},"next":"list"},"list":{"rpcName":"todos:listTodos","input":{}}}\n' +
99
- '\n### Example (add todo, sleep, complete using addTodo result id):\n' +
100
- '{"add":{"rpcName":"todos:addTodo","input":{"title":{"$ref":"trigger","path":"title"}},"next":"wait"},"wait":{"rpcName":"sleep","input":{"seconds":5},"next":"complete"},"complete":{"rpcName":"todos:completeTodo","input":{"id":{"$ref":"add","path":"id"}}}}'
101
- )
158
+ const sections: string[] = [
159
+ '\n\n## Workflow Creation\n',
160
+ modeGuidance,
161
+
162
+ '### When to Create a Workflow\n' +
163
+ 'Create a workflow when:\n' +
164
+ '- The task has 2+ steps with data dependencies between them\n' +
165
+ '- The task will be repeated (workflows are reusable)\n' +
166
+ '- Steps can fail independently and need error handling\n' +
167
+ '- Steps can run in parallel for better performance\n' +
168
+ 'Do NOT create a workflow for single tool calls just call the tool directly.\n',
169
+
170
+ '### Process\n' +
171
+ '1. Check if a suitable workflow exists: `listAgentWorkflows`\n' +
172
+ '2. If the request is too vague to build a reliable workflow, ask the user to be more specific. For example, if a user says "make a workflow that does stuff with my data" — ask them what steps they need and what data flows between them.\n' +
173
+ '3. Plan the graph: identify steps, dependencies, parallel paths, and failure modes\n' +
174
+ '4. Create and validate: `createAgentWorkflow` — if validation fails, fix the errors and retry\n' +
175
+ '5. Present the workflow to the user and get approval\n' +
176
+ '6. Activate: `saveAgentWorkflow`\n' +
177
+ '7. Run: `executeAgentWorkflow`\n',
178
+
179
+ '### Available Tools\n' +
180
+ '| Tool | Description | Input fields | Output fields |\n' +
181
+ '|------|-------------|-------------|---------------|\n' +
182
+ toolSummaryLines.join('\n') +
183
+ '\n\n' +
184
+ 'Full schemas (input types, output paths) are returned by `createAgentWorkflow` after validation.\n',
185
+
186
+ '### Graph Format Reference\n\n' +
187
+ '**Node:** `{ rpcName, input?, next?, onError? }`\n\n' +
188
+ '**Data wiring (input field values):**\n' +
189
+ '- `{"$ref": "trigger", "path": "fieldName"}` — extract field from trigger input (ALWAYS include `path`)\n' +
190
+ '- `{"$ref": "nodeId", "path": "fieldName"}` — extract field from a previous node\'s output (ALWAYS include `path`)\n' +
191
+ '- `path` supports dot notation for nested fields — use the "output paths" listed above to find the correct path\n' +
192
+ '- Example: if a tool\'s output paths show `todo.id: string`, use `{"$ref": "nodeId", "path": "todo.id"}`\n' +
193
+ '- `{"$template": {"parts": ["Hello ", "!"], "expressions": [{"$ref": "trigger", "path": "name"}]}}` — string interpolation\n' +
194
+ '- Static values passed directly: `{"count": 10}`\n\n' +
195
+ '**Flow control (`next`):**\n' +
196
+ '- `"next": "b"` — sequential\n' +
197
+ '- `"next": ["b", "c"]` — parallel fan-out\n' +
198
+ '- `"next": {"ok": "b", "fail": "c"}` — conditional branch (node calls `graph.branch("ok")` at runtime)\n' +
199
+ '- Omit `next` for terminal nodes. Entry nodes are auto-detected.\n\n' +
200
+ '**Error handling:**\n' +
201
+ '- `"onError": "handler"` — route to error handler on failure\n' +
202
+ '- Error handler receives `{ error: { message: string } }` as input\n' +
203
+ '- Without `onError`, a node failure fails the entire workflow\n\n' +
204
+ '**Sub-workflows:** if `rpcName` matches an existing workflow name, it runs as a child workflow with its own execution context.\n\n' +
205
+ '**Trigger input:** the workflow receives input when executed. Entry nodes extract fields via `$ref: "trigger"`. Document what fields your workflow expects in the `description`.\n',
206
+
207
+ '### Design Principles\n' +
208
+ '1. **Map dependencies first** — before writing nodes, identify which steps depend on which outputs\n' +
209
+ '2. **Parallelize independent work** — steps with no data dependency should fan out via array `next`\n' +
210
+ '3. **Add error handlers for external calls** — any node calling an external service should have `onError`\n' +
211
+ "4. **Minimize blast radius** — isolate risky nodes so their failure doesn't cascade\n" +
212
+ '5. **Name nodes by intent** — `fetchUsers`, `sendAlert` not `step1`, `step2`\n' +
213
+ '6. **Keep graphs shallow** — prefer wide parallel graphs over deep sequential chains when possible\n',
214
+
215
+ '### Common Mistakes\n' +
216
+ '- `{"$ref": "trigger"}` without `path` — passes entire object, use `{"$ref": "trigger", "path": "field"}`\n' +
217
+ '- Wrong path depth: if output paths show `todo.id`, use `"path": "todo.id"` not `"path": "id"`\n' +
218
+ '- Type mismatch: wiring a string output to a number input\n' +
219
+ '- Dangling `onError`/`next` references to nodes not in the graph\n' +
220
+ '- Cycles: if A→B→A, there are no entry nodes\n',
221
+
222
+ '### Examples\n\n' +
223
+ '**Sequential with nested output path (if addTodo output paths show `todo.id`):**\n' +
224
+ '```json\n{"add":{"rpcName":"addTodo","input":{"title":{"$ref":"trigger","path":"title"}},"next":"complete"},"complete":{"rpcName":"completeTodo","input":{"id":{"$ref":"add","path":"todo.id"}}}}\n```\n\n' +
225
+ '**Error handling:**\n' +
226
+ '```json\n{"deploy":{"rpcName":"deploy","input":{"env":{"$ref":"trigger","path":"env"}},"next":"notify","onError":"rollback"},"notify":{"rpcName":"alert","input":{"message":"Deployed"}},"rollback":{"rpcName":"rollback","input":{},"next":"alertFail"},"alertFail":{"rpcName":"alert","input":{"message":"Rolled back"}}}\n```\n\n' +
227
+ '**Parallel fan-out:**\n' +
228
+ '```json\n{"fetchA":{"rpcName":"getA","input":{},"next":"combine"},"fetchB":{"rpcName":"getB","input":{},"next":"combine"},"combine":{"rpcName":"graph:merge","input":{"items":[{"$ref":"fetchA","path":"data"},{"$ref":"fetchB","path":"data"}]}}}\n```',
229
+ ]
230
+
231
+ return sections.join('\n')
102
232
  }
103
233
 
104
234
  export function buildWorkflowTools(
@@ -112,200 +242,211 @@ export function buildWorkflowTools(
112
242
  const tools: AIAgentToolDef[] = []
113
243
 
114
244
  if (mode !== 'read') {
115
- tools.push({
116
- name: 'createAgentWorkflow',
117
- description:
118
- 'Validate and create a draft workflow graph that chains tools together. Call saveAgentWorkflow to activate it after the user approves.',
119
- inputSchema: {
120
- type: 'object',
121
- properties: {
122
- name: {
123
- type: 'string',
124
- description:
125
- 'Short name for the workflow (will be prefixed with ai:{agentName}:)',
126
- },
127
- description: {
128
- type: 'string',
129
- description: 'What this workflow does',
130
- },
131
- nodes: {
132
- type: 'string',
133
- description:
134
- 'JSON string of a map of nodeId to node config. Each node has: rpcName (tool name), input (with $ref to wire outputs), next (string|string[]|{branchKey: string}), onError (string|string[]). Example: {"list":{"rpcName":"todos:listTodos","input":{}}}',
245
+ tools.push({
246
+ name: 'createAgentWorkflow',
247
+ description:
248
+ 'Validate and create a draft workflow DAG that chains tools together. The graph is validated for wiring correctness and type compatibility. Call saveAgentWorkflow to activate it after the user approves.',
249
+ inputSchema: {
250
+ type: 'object',
251
+ properties: {
252
+ name: {
253
+ type: 'string',
254
+ description:
255
+ 'Short name for the workflow (will be prefixed with ai:{agentName}:)',
256
+ },
257
+ description: {
258
+ type: 'string',
259
+ description: 'What this workflow does',
260
+ },
261
+ nodes: {
262
+ type: 'string',
263
+ description:
264
+ 'JSON string of nodeIdnode map. Node fields: rpcName (tool name), input (use $ref+path to wire data), next (flow control), onError (error routing). Must have 2+ nodes.',
265
+ },
135
266
  },
267
+ required: ['name', 'nodes'],
136
268
  },
137
- required: ['name', 'nodes'],
138
- },
139
- execute: async (input: unknown) => {
140
- const raw = input as {
141
- name: string
142
- description?: string
143
- nodes: string | Record<string, any>
144
- }
145
- const name = raw.name.replace(/[^a-zA-Z0-9_-]/g, '-')
146
- let nodes: Record<string, any>
147
- if (typeof raw.nodes === 'string') {
148
- try {
149
- nodes = JSON.parse(raw.nodes)
150
- } catch {
151
- return { error: 'Invalid JSON in nodes field' }
269
+ execute: async (input: unknown) => {
270
+ const raw = input as {
271
+ name: string
272
+ description?: string
273
+ nodes: string | Record<string, any>
152
274
  }
153
- } else {
154
- nodes = raw.nodes
155
- }
156
-
157
- if (Object.keys(nodes).length < 2) {
158
- return {
159
- error:
160
- 'A workflow must have at least 2 nodes. A single node is just a tool call — use the tool directly instead.',
275
+ const name = raw.name.replace(/[^a-zA-Z0-9_-]/g, '-')
276
+ let nodes: Record<string, any>
277
+ if (typeof raw.nodes === 'string') {
278
+ try {
279
+ nodes = JSON.parse(raw.nodes)
280
+ } catch {
281
+ return { error: 'Invalid JSON in nodes field' }
282
+ }
283
+ } else {
284
+ nodes = raw.nodes
161
285
  }
162
- }
163
286
 
164
- const validationErrors = validateWorkflowWiring(nodes, toolNames)
165
- if (validationErrors.length > 0) {
166
- return {
167
- error: 'Workflow validation failed',
168
- errors: validationErrors,
287
+ if (Object.keys(nodes).length < 2) {
288
+ return {
289
+ error:
290
+ 'A workflow must have at least 2 nodes. A single node is just a tool call — use the tool directly instead.',
291
+ }
169
292
  }
170
- }
171
293
 
172
- const entryNodeIds = computeEntryNodeIds(nodes)
173
- if (entryNodeIds.length === 0) {
174
- return {
175
- error:
176
- 'No entry nodes found. Every node is referenced by another node, creating a cycle.',
294
+ const usedTools = [
295
+ ...new Set(
296
+ Object.values(nodes)
297
+ .map((n: any) => n.rpcName)
298
+ .filter(Boolean)
299
+ ),
300
+ ]
301
+ const toolSchemas = buildDetailedToolSchemas(usedTools)
302
+
303
+ const validationErrors = validateWorkflowWiring(nodes, toolNames)
304
+ if (validationErrors.length > 0) {
305
+ return {
306
+ error: 'Workflow validation failed',
307
+ errors: validationErrors,
308
+ toolSchemas,
309
+ }
177
310
  }
178
- }
179
311
 
180
- const fullName = `ai:${agentName}:${name}`
181
-
182
- const graphHash = hashString(canonicalJSON({ nodes, entryNodeIds }))
183
- const graph: WorkflowRuntimeMeta = {
184
- name: fullName,
185
- pikkuFuncId: fullName,
186
- source: 'ai-agent',
187
- description: raw.description,
188
- nodes,
189
- entryNodeIds,
190
- graphHash,
191
- }
312
+ const entryNodeIds = computeEntryNodeIds(nodes)
313
+ if (entryNodeIds.length === 0) {
314
+ return {
315
+ error:
316
+ 'No entry nodes found. Every node is referenced by another node, creating a cycle.',
317
+ }
318
+ }
192
319
 
193
- const singletonServices = getSingletonServices()
194
- if (!singletonServices?.workflowService) {
195
- return { error: 'Workflow service not available' }
196
- }
320
+ const fullName = `ai:${agentName}:${name}`
321
+
322
+ const graphHash = hashString(canonicalJSON({ nodes, entryNodeIds }))
323
+ const graph: WorkflowRuntimeMeta = {
324
+ name: fullName,
325
+ pikkuFuncId: fullName,
326
+ source: 'ai-agent',
327
+ description: raw.description,
328
+ nodes,
329
+ entryNodeIds,
330
+ graphHash,
331
+ }
197
332
 
198
- await singletonServices.workflowService.upsertWorkflowVersion(
199
- fullName,
200
- graphHash,
201
- graph,
202
- 'ai-agent',
203
- 'draft'
204
- )
333
+ const singletonServices = getSingletonServices()
334
+ if (!singletonServices?.workflowService) {
335
+ return { error: 'Workflow service not available' }
336
+ }
205
337
 
206
- return {
207
- valid: true,
208
- workflowName: fullName,
209
- graphHash,
210
- entryNodes: entryNodeIds,
211
- nodeCount: Object.keys(nodes).length,
212
- message: `Workflow '${fullName}' validated and saved as draft. Present this to the user and call saveAgentWorkflow to activate it.`,
213
- }
214
- },
215
- })
338
+ await singletonServices.workflowService.upsertWorkflowVersion(
339
+ fullName,
340
+ graphHash,
341
+ graph,
342
+ 'ai-agent',
343
+ 'draft'
344
+ )
216
345
 
217
- tools.push({
218
- name: 'saveAgentWorkflow',
219
- description:
220
- 'Activate a previously created draft workflow so it can be executed. Requires user approval.',
221
- inputSchema: {
222
- type: 'object',
223
- properties: {
224
- name: {
225
- type: 'string',
226
- description:
227
- 'Full workflow name returned by createAgentWorkflow (e.g. ai:agentName:myWorkflow)',
228
- },
229
- graphHash: {
230
- type: 'string',
231
- description: 'Graph hash returned by createAgentWorkflow',
346
+ return {
347
+ valid: true,
348
+ workflowName: fullName,
349
+ graphHash,
350
+ entryNodes: entryNodeIds,
351
+ nodeCount: Object.keys(nodes).length,
352
+ toolSchemas,
353
+ message: `Workflow '${fullName}' validated and saved as draft. Check the toolSchemas to verify your $ref paths match the output paths. Then present this to the user and call saveAgentWorkflow to activate it.`,
354
+ }
355
+ },
356
+ })
357
+
358
+ tools.push({
359
+ name: 'saveAgentWorkflow',
360
+ description:
361
+ 'Activate a previously created draft workflow so it can be executed. Requires user approval.',
362
+ inputSchema: {
363
+ type: 'object',
364
+ properties: {
365
+ name: {
366
+ type: 'string',
367
+ description:
368
+ 'Full workflow name returned by createAgentWorkflow (e.g. ai:agentName:myWorkflow)',
369
+ },
370
+ graphHash: {
371
+ type: 'string',
372
+ description: 'Graph hash returned by createAgentWorkflow',
373
+ },
232
374
  },
375
+ required: ['name', 'graphHash'],
233
376
  },
234
- required: ['name', 'graphHash'],
235
- },
236
- needsApproval: true,
237
- approvalDescriptionFn: async (input: unknown) => {
238
- const raw = input as { name: string; graphHash: string }
239
- const fullName = raw.name.startsWith(`ai:${agentName}:`)
240
- ? raw.name
241
- : `ai:${agentName}:${raw.name}`
377
+ needsApproval: true,
378
+ approvalDescriptionFn: async (input: unknown) => {
379
+ const raw = input as { name: string; graphHash: string }
380
+ const fullName = raw.name.startsWith(`ai:${agentName}:`)
381
+ ? raw.name
382
+ : `ai:${agentName}:${raw.name}`
383
+
384
+ const singletonServices = getSingletonServices()
385
+ if (!singletonServices?.workflowService) {
386
+ return `Activate workflow '${fullName}'`
387
+ }
242
388
 
243
- const singletonServices = getSingletonServices()
244
- if (!singletonServices?.workflowService) {
389
+ const version =
390
+ await singletonServices.workflowService.getWorkflowVersion(
391
+ fullName,
392
+ raw.graphHash
393
+ )
394
+ if (version) {
395
+ const graph = version.graph as WorkflowRuntimeMeta
396
+ if (graph.nodes) {
397
+ const desc = graph.description ? `\n${graph.description}` : ''
398
+ return `Activate workflow '${fullName}' (${Object.keys(graph.nodes).length} nodes)${desc}`
399
+ }
400
+ }
245
401
  return `Activate workflow '${fullName}'`
246
- }
247
-
248
- const version =
249
- await singletonServices.workflowService.getWorkflowVersion(
250
- fullName,
251
- raw.graphHash
252
- )
253
- if (version) {
254
- const graph = version.graph as WorkflowRuntimeMeta
255
- if (graph.nodes) {
256
- const desc = graph.description ? `\n${graph.description}` : ''
257
- return `Activate workflow '${fullName}' (${Object.keys(graph.nodes).length} nodes)${desc}`
402
+ },
403
+ execute: async (input: unknown) => {
404
+ const raw = input as { name: string; graphHash: string }
405
+ const fullName = raw.name.startsWith(`ai:${agentName}:`)
406
+ ? raw.name
407
+ : `ai:${agentName}:${raw.name}`
408
+
409
+ const singletonServices = getSingletonServices()
410
+ if (!singletonServices?.workflowService) {
411
+ return { error: 'Workflow service not available' }
258
412
  }
259
- }
260
- return `Activate workflow '${fullName}'`
261
- },
262
- execute: async (input: unknown) => {
263
- const raw = input as { name: string; graphHash: string }
264
- const fullName = raw.name.startsWith(`ai:${agentName}:`)
265
- ? raw.name
266
- : `ai:${agentName}:${raw.name}`
267
413
 
268
- const singletonServices = getSingletonServices()
269
- if (!singletonServices?.workflowService) {
270
- return { error: 'Workflow service not available' }
271
- }
414
+ const version =
415
+ await singletonServices.workflowService.getWorkflowVersion(
416
+ fullName,
417
+ raw.graphHash
418
+ )
419
+ if (!version) {
420
+ return {
421
+ error: `Workflow '${fullName}' with hash '${raw.graphHash}' not found. Use createAgentWorkflow first.`,
422
+ }
423
+ }
272
424
 
273
- const version =
274
- await singletonServices.workflowService.getWorkflowVersion(
425
+ await singletonServices.workflowService.updateWorkflowVersionStatus(
275
426
  fullName,
276
- raw.graphHash
427
+ raw.graphHash,
428
+ 'active'
277
429
  )
278
- if (!version) {
279
- return {
280
- error: `Workflow '${fullName}' with hash '${raw.graphHash}' not found. Use createAgentWorkflow first.`,
281
- }
282
- }
283
430
 
284
- await singletonServices.workflowService.updateWorkflowVersionStatus(
285
- fullName,
286
- raw.graphHash,
287
- 'active'
288
- )
289
-
290
- const graph = version.graph as WorkflowRuntimeMeta
291
- const allMeta = pikkuState(null, 'workflows', 'meta')
292
- allMeta[fullName] = graph
431
+ const graph = version.graph as WorkflowRuntimeMeta
432
+ const allMeta = pikkuState(null, 'workflows', 'meta')
433
+ allMeta[fullName] = graph
434
+
435
+ if (streamContext) {
436
+ streamContext.channel.send({
437
+ type: 'workflow-created',
438
+ workflowName: fullName,
439
+ graph,
440
+ })
441
+ }
293
442
 
294
- if (streamContext) {
295
- streamContext.channel.send({
296
- type: 'workflow-created',
443
+ return {
444
+ success: true,
297
445
  workflowName: fullName,
298
- graph,
299
- })
300
- }
301
-
302
- return {
303
- success: true,
304
- workflowName: fullName,
305
- message: `Workflow '${fullName}' activated and ready to execute.`,
306
- }
307
- },
308
- })
446
+ message: `Workflow '${fullName}' activated and ready to execute.`,
447
+ }
448
+ },
449
+ })
309
450
  } // end if (mode !== 'read')
310
451
 
311
452
  tools.push({