@pikku/core 0.12.1 → 0.12.3

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 (165) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/crypto-utils.d.ts +10 -0
  3. package/dist/crypto-utils.js +62 -0
  4. package/dist/function/function-runner.js +4 -4
  5. package/dist/function/functions.types.d.ts +26 -1
  6. package/dist/function/functions.types.js +16 -0
  7. package/dist/handle-error.d.ts +1 -1
  8. package/dist/handle-error.js +7 -3
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.js +1 -1
  11. package/dist/middleware-runner.js +1 -0
  12. package/dist/permissions.js +1 -0
  13. package/dist/pikku-state.js +7 -1
  14. package/dist/schema.js +1 -1
  15. package/dist/services/gateway-service.d.ts +19 -0
  16. package/dist/services/gateway-service.js +1 -0
  17. package/dist/services/in-memory-workflow-service.d.ts +8 -2
  18. package/dist/services/in-memory-workflow-service.js +28 -1
  19. package/dist/services/index.d.ts +2 -0
  20. package/dist/services/index.js +1 -0
  21. package/dist/services/local-gateway-service.d.ts +22 -0
  22. package/dist/services/local-gateway-service.js +51 -0
  23. package/dist/services/workflow-service.d.ts +8 -2
  24. package/dist/testing/index.d.ts +2 -0
  25. package/dist/testing/index.js +1 -0
  26. package/dist/testing/service-tests.d.ts +31 -0
  27. package/dist/testing/service-tests.js +598 -0
  28. package/dist/types/core.types.d.ts +12 -2
  29. package/dist/types/state.types.d.ts +9 -2
  30. package/dist/utils/hash.d.ts +2 -0
  31. package/dist/utils/hash.js +23 -0
  32. package/dist/utils.js +1 -1
  33. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  34. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  35. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  36. package/dist/wirings/ai-agent/ai-agent-memory.js +19 -7
  37. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  38. package/dist/wirings/ai-agent/ai-agent-prepare.js +141 -48
  39. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  40. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  41. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  42. package/dist/wirings/ai-agent/ai-agent-stream.js +126 -48
  43. package/dist/wirings/ai-agent/ai-agent.types.d.ts +57 -3
  44. package/dist/wirings/ai-agent/index.d.ts +2 -3
  45. package/dist/wirings/ai-agent/index.js +1 -2
  46. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  47. package/dist/wirings/channel/channel-handler.js +16 -2
  48. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  49. package/dist/wirings/channel/channel.types.d.ts +7 -7
  50. package/dist/wirings/channel/index.d.ts +1 -1
  51. package/dist/wirings/channel/local/local-channel-handler.d.ts +8 -1
  52. package/dist/wirings/channel/local/local-channel-handler.js +22 -3
  53. package/dist/wirings/channel/local/local-channel-runner.js +28 -5
  54. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  55. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  56. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  57. package/dist/wirings/cli/cli-runner.js +13 -2
  58. package/dist/wirings/cli/cli.types.d.ts +1 -0
  59. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  60. package/dist/wirings/gateway/gateway-runner.js +325 -0
  61. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  62. package/dist/wirings/gateway/gateway.types.js +1 -0
  63. package/dist/wirings/gateway/index.d.ts +2 -0
  64. package/dist/wirings/gateway/index.js +1 -0
  65. package/dist/wirings/http/http-routes.js +0 -1
  66. package/dist/wirings/http/http-runner.d.ts +1 -1
  67. package/dist/wirings/http/http-runner.js +28 -3
  68. package/dist/wirings/http/http.types.d.ts +3 -11
  69. package/dist/wirings/http/index.d.ts +2 -1
  70. package/dist/wirings/http/index.js +1 -0
  71. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  72. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  73. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  74. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  75. package/dist/wirings/http/web-request.d.ts +12 -0
  76. package/dist/wirings/http/web-request.js +85 -0
  77. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  78. package/dist/wirings/mcp/mcp-runner.js +13 -1
  79. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  80. package/dist/wirings/rpc/rpc-runner.js +36 -14
  81. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  82. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  83. package/dist/wirings/rpc/wire-addon.js +1 -1
  84. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  85. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  86. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  87. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  88. package/dist/wirings/workflow/graph/index.js +1 -0
  89. package/dist/wirings/workflow/index.d.ts +1 -1
  90. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  91. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  92. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  93. package/package.json +6 -3
  94. package/src/crypto-utils.test.ts +99 -1
  95. package/src/crypto-utils.ts +114 -0
  96. package/src/function/function-runner.ts +5 -5
  97. package/src/function/functions.types.ts +38 -1
  98. package/src/handle-error.test.ts +65 -2
  99. package/src/handle-error.ts +8 -4
  100. package/src/index.ts +14 -0
  101. package/src/middleware-runner.ts +1 -0
  102. package/src/permissions.ts +1 -0
  103. package/src/pikku-state.ts +8 -1
  104. package/src/schema.ts +1 -1
  105. package/src/services/gateway-service.ts +20 -0
  106. package/src/services/in-memory-workflow-service.ts +45 -2
  107. package/src/services/index.ts +2 -0
  108. package/src/services/local-gateway-service.ts +62 -0
  109. package/src/services/workflow-service.ts +12 -1
  110. package/src/testing/index.ts +2 -0
  111. package/src/testing/service-tests.ts +873 -0
  112. package/src/types/core.types.ts +12 -1
  113. package/src/types/state.types.ts +12 -2
  114. package/src/utils/hash.test.ts +68 -0
  115. package/src/utils/hash.ts +26 -0
  116. package/src/utils.ts +1 -1
  117. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  118. package/src/wirings/ai-agent/ai-agent-memory.ts +17 -7
  119. package/src/wirings/ai-agent/ai-agent-prepare.ts +194 -62
  120. package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
  121. package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
  122. package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
  123. package/src/wirings/ai-agent/ai-agent-stream.ts +195 -91
  124. package/src/wirings/ai-agent/ai-agent.types.ts +57 -3
  125. package/src/wirings/ai-agent/index.ts +3 -5
  126. package/src/wirings/channel/channel-handler.ts +41 -4
  127. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  128. package/src/wirings/channel/channel.types.ts +12 -7
  129. package/src/wirings/channel/index.ts +1 -0
  130. package/src/wirings/channel/local/local-channel-handler.ts +31 -3
  131. package/src/wirings/channel/local/local-channel-runner.ts +32 -8
  132. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  133. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  134. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  135. package/src/wirings/cli/cli-runner.ts +15 -2
  136. package/src/wirings/cli/cli.types.ts +1 -0
  137. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  138. package/src/wirings/gateway/gateway-runner.ts +411 -0
  139. package/src/wirings/gateway/gateway.types.ts +149 -0
  140. package/src/wirings/gateway/index.ts +13 -0
  141. package/src/wirings/http/http-routes.ts +0 -1
  142. package/src/wirings/http/http-runner.ts +38 -10
  143. package/src/wirings/http/http.types.ts +3 -12
  144. package/src/wirings/http/index.ts +3 -0
  145. package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
  146. package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
  147. package/src/wirings/http/web-request.test.ts +236 -0
  148. package/src/wirings/http/web-request.ts +104 -0
  149. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  150. package/src/wirings/mcp/mcp-runner.ts +30 -12
  151. package/src/wirings/rpc/rpc-runner.ts +45 -24
  152. package/src/wirings/rpc/rpc-types.ts +6 -1
  153. package/src/wirings/rpc/wire-addon.ts +2 -1
  154. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  155. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  156. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  157. package/src/wirings/workflow/graph/index.ts +5 -0
  158. package/src/wirings/workflow/index.ts +1 -0
  159. package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
  160. package/src/wirings/workflow/workflow.types.ts +9 -1
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  163. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -146
  164. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  165. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -214
@@ -22,6 +22,7 @@ import type {
22
22
  } from '../wirings/workflow/workflow.types.js'
23
23
  import type { PikkuGraphWire } from '../wirings/workflow/graph/workflow-graph.types.js'
24
24
  import type { PikkuTrigger } from '../wirings/trigger/trigger.types.js'
25
+ import type { PikkuGateway } from '../wirings/gateway/gateway.types.js'
25
26
  import type { SchedulerService } from '../services/scheduler-service.js'
26
27
  import type { DeploymentService } from '../services/deployment-service.js'
27
28
  import type { AIStorageService } from '../services/ai-storage-service.js'
@@ -29,7 +30,9 @@ import type { AIStorageService } from '../services/ai-storage-service.js'
29
30
  import type { ContentService } from '../services/content-service.js'
30
31
  import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.js'
31
32
  import type { AIRunStateService } from '../services/ai-run-state-service.js'
33
+ import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
32
34
  import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
35
+ import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
33
36
 
34
37
  export type PikkuWiringTypes =
35
38
  | 'http'
@@ -42,6 +45,7 @@ export type PikkuWiringTypes =
42
45
  | 'cli'
43
46
  | 'workflow'
44
47
  | 'agent'
48
+ | 'gateway'
45
49
 
46
50
  export interface FunctionServicesMeta {
47
51
  optimized: boolean
@@ -105,7 +109,8 @@ export type FunctionRuntimeMeta = {
105
109
  readonly?: boolean
106
110
  sessionless?: boolean
107
111
  version?: number
108
- requiresApproval?: boolean
112
+ approvalRequired?: boolean
113
+ approvalDescription?: string
109
114
  contractHash?: string
110
115
  inputHash?: string
111
116
  outputHash?: string
@@ -218,6 +223,10 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
218
223
  aiAgentRunner?: AIAgentRunnerService
219
224
  /** AI run state service (run lifecycle + approval persistence) */
220
225
  aiRunState?: AIRunStateService
226
+ /** Agent run service (listing threads, runs, steps) */
227
+ agentRunService?: AgentRunService
228
+ /** Workflow run service (listing workflow runs) */
229
+ workflowRunService?: WorkflowRunService
221
230
  }
222
231
 
223
232
  /**
@@ -249,6 +258,7 @@ export type PikkuWire<
249
258
  workflowStep: WorkflowStepWire
250
259
  graph: PikkuGraphWire
251
260
  trigger: PikkuTrigger<TriggerOutput>
261
+ gateway: PikkuGateway
252
262
  session: HasInitialSession extends true
253
263
  ? UserSession
254
264
  : UserSession | undefined
@@ -445,6 +455,7 @@ export type CreateConfig<
445
455
  */
446
456
  export type CommonWireMeta = {
447
457
  pikkuFuncId: string
458
+ packageName?: string
448
459
 
449
460
  title?: string
450
461
  tags?: string[]
@@ -26,6 +26,10 @@ import type {
26
26
  CoreAIAgent,
27
27
  AIAgentMeta,
28
28
  } from '../wirings/ai-agent/ai-agent.types.js'
29
+ import type {
30
+ CoreGateway,
31
+ GatewaysMeta,
32
+ } from '../wirings/gateway/gateway.types.js'
29
33
  import type {
30
34
  CoreQueueWorker,
31
35
  QueueWorkersMeta,
@@ -75,8 +79,10 @@ export interface PikkuPackageState {
75
79
  path: string
76
80
  }
77
81
  >
78
- /** Maps namespace aliases to package config (e.g., 'ext' -> { package: '@pikku/...', rpcEndpoint: '...' }) */
79
- addons: Map<
82
+ }
83
+ /** Maps namespace aliases to package config (e.g., 'console' -> { package: '@pikku/console', ... }) */
84
+ addons: {
85
+ packages: Map<
80
86
  string,
81
87
  { package: string; rpcEndpoint?: string; auth?: boolean; tags?: string[] }
82
88
  >
@@ -121,6 +127,10 @@ export interface PikkuPackageState {
121
127
  agents: Map<string, CoreAIAgent>
122
128
  agentsMeta: AIAgentMeta
123
129
  }
130
+ gateway: {
131
+ gateways: Map<string, CoreGateway>
132
+ meta: GatewaysMeta
133
+ }
124
134
  cli: {
125
135
  meta: CLIMeta | Record<string, any> // Backward compatible with old published CLI format
126
136
  programs: Record<string, CLIProgramState>
@@ -0,0 +1,68 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { canonicalJSON, hashString } from './hash.js'
5
+
6
+ describe('canonicalJSON', () => {
7
+ it('sorts object keys alphabetically', () => {
8
+ const result = canonicalJSON({ b: 1, a: 2, c: 3 })
9
+ assert.equal(result, '{"a":2,"b":1,"c":3}')
10
+ })
11
+
12
+ it('sorts nested object keys', () => {
13
+ const result = canonicalJSON({ z: { b: 1, a: 2 }, a: 1 })
14
+ assert.equal(result, '{"a":1,"z":{"a":2,"b":1}}')
15
+ })
16
+
17
+ it('preserves array order', () => {
18
+ const result = canonicalJSON([3, 1, 2])
19
+ assert.equal(result, '[3,1,2]')
20
+ })
21
+
22
+ it('sorts objects inside arrays', () => {
23
+ const result = canonicalJSON([{ b: 1, a: 2 }])
24
+ assert.equal(result, '[{"a":2,"b":1}]')
25
+ })
26
+
27
+ it('handles null and undefined', () => {
28
+ assert.equal(canonicalJSON(null), 'null')
29
+ assert.equal(canonicalJSON(undefined), undefined)
30
+ })
31
+
32
+ it('handles primitives', () => {
33
+ assert.equal(canonicalJSON('hello'), '"hello"')
34
+ assert.equal(canonicalJSON(42), '42')
35
+ assert.equal(canonicalJSON(true), 'true')
36
+ })
37
+
38
+ it('produces deterministic output for same input regardless of key order', () => {
39
+ const a = canonicalJSON({ x: 1, y: 2, z: 3 })
40
+ const b = canonicalJSON({ z: 3, x: 1, y: 2 })
41
+ assert.equal(a, b)
42
+ })
43
+ })
44
+
45
+ describe('hashString', () => {
46
+ it('returns a hex string of default length 16', () => {
47
+ const result = hashString('test')
48
+ assert.equal(result.length, 16)
49
+ assert.match(result, /^[0-9a-f]+$/)
50
+ })
51
+
52
+ it('returns consistent hash for same input', () => {
53
+ const a = hashString('hello world')
54
+ const b = hashString('hello world')
55
+ assert.equal(a, b)
56
+ })
57
+
58
+ it('returns different hashes for different inputs', () => {
59
+ const a = hashString('hello')
60
+ const b = hashString('world')
61
+ assert.notEqual(a, b)
62
+ })
63
+
64
+ it('respects custom length parameter', () => {
65
+ const result = hashString('test', 8)
66
+ assert.equal(result.length, 8)
67
+ })
68
+ })
@@ -0,0 +1,26 @@
1
+ import { createHash } from 'crypto'
2
+
3
+ export function canonicalJSON(obj: unknown): string {
4
+ return JSON.stringify(sortDeep(obj))
5
+ }
6
+
7
+ function sortDeep(value: unknown): unknown {
8
+ if (value === null || value === undefined) {
9
+ return value
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return value.map(sortDeep)
13
+ }
14
+ if (typeof value === 'object') {
15
+ const sorted: Record<string, unknown> = {}
16
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
17
+ sorted[key] = sortDeep((value as Record<string, unknown>)[key])
18
+ }
19
+ return sorted
20
+ }
21
+ return value
22
+ }
23
+
24
+ export function hashString(input: string, length: number = 16): string {
25
+ return createHash('sha256').update(input).digest('hex').slice(0, length)
26
+ }
package/src/utils.ts CHANGED
@@ -103,7 +103,7 @@ export const stopSingletonServices = async (): Promise<void> => {
103
103
  const singletonServices = getSingletonServices()
104
104
  const logger = singletonServices.logger
105
105
 
106
- // First, stop all addon package singleton services
106
+ // Stop all addon package singleton services
107
107
  const stateMap = getAllPackageStates()
108
108
  if (stateMap.size > 0) {
109
109
  for (const [packageName, packageState] of stateMap) {
@@ -0,0 +1,469 @@
1
+ import type { AIAgentToolDef } from './ai-agent.types.js'
2
+ import { pikkuState, getSingletonServices } from '../../pikku-state.js'
3
+ import { canonicalJSON, hashString } from '../../utils/hash.js'
4
+ import type { WorkflowRuntimeMeta } from '../workflow/workflow.types.js'
5
+ import { runWorkflowGraph } from '../workflow/graph/graph-runner.js'
6
+ import type { StreamContext } from './ai-agent-prepare.js'
7
+ import type { PikkuWire, CoreUserSession } from '../../types/core.types.js'
8
+ import { ContextAwareRPCService, resolveNamespace } from '../rpc/rpc-runner.js'
9
+ import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
10
+ import type { SessionService } from '../../services/user-session-service.js'
11
+ import {
12
+ validateWorkflowWiring,
13
+ computeEntryNodeIds,
14
+ } from '../workflow/graph/graph-validation.js'
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
+ )
23
+ }
24
+
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>
40
+
41
+ const resolved = toolName.includes(':') ? resolveNamespace(toolName) : null
42
+
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
+ }
55
+
56
+ if (!fnMeta) continue
57
+
58
+ const inputSchema = fnMeta.inputSchemaName
59
+ ? schemas.get(fnMeta.inputSchemaName)
60
+ : null
61
+ const outputSchema = fnMeta.outputSchemaName
62
+ ? schemas.get(fnMeta.outputSchemaName)
63
+ : null
64
+
65
+ const toolDescription = fnMeta.description || fnMeta.title || ''
66
+ const inputProps = inputSchema?.properties
67
+ ? Object.entries(inputSchema.properties)
68
+ .map(
69
+ ([k, v]: [string, any]) =>
70
+ `${k}${inputSchema.required?.includes(k) ? '' : '?'}: ${v.type || 'any'}`
71
+ )
72
+ .join(', ')
73
+ : ''
74
+ const outputProps = outputSchema?.properties
75
+ ? Object.entries(outputSchema.properties)
76
+ .map(([k, v]: [string, any]) => `${k}: ${v.type || 'any'}`)
77
+ .join(', ')
78
+ : 'any'
79
+
80
+ toolSchemaLines.push(
81
+ `- ${toolName}(input: {${inputProps}}) → {${outputProps}}${toolDescription ? ` — ${toolDescription}` : ''}`
82
+ )
83
+ }
84
+
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
+ )
102
+ }
103
+
104
+ export function buildWorkflowTools(
105
+ agentName: string,
106
+ packageName: string | null,
107
+ toolNames: string[],
108
+ mode: 'read' | 'always' | 'ask',
109
+ streamContext?: StreamContext,
110
+ sessionService?: SessionService<CoreUserSession>
111
+ ): AIAgentToolDef[] {
112
+ const tools: AIAgentToolDef[] = []
113
+
114
+ 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":{}}}',
135
+ },
136
+ },
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' }
152
+ }
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.',
161
+ }
162
+ }
163
+
164
+ const validationErrors = validateWorkflowWiring(nodes, toolNames)
165
+ if (validationErrors.length > 0) {
166
+ return {
167
+ error: 'Workflow validation failed',
168
+ errors: validationErrors,
169
+ }
170
+ }
171
+
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.',
177
+ }
178
+ }
179
+
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
+ }
192
+
193
+ const singletonServices = getSingletonServices()
194
+ if (!singletonServices?.workflowService) {
195
+ return { error: 'Workflow service not available' }
196
+ }
197
+
198
+ await singletonServices.workflowService.upsertWorkflowVersion(
199
+ fullName,
200
+ graphHash,
201
+ graph,
202
+ 'ai-agent',
203
+ 'draft'
204
+ )
205
+
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
+ })
216
+
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',
232
+ },
233
+ },
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}`
242
+
243
+ const singletonServices = getSingletonServices()
244
+ if (!singletonServices?.workflowService) {
245
+ 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}`
258
+ }
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
+
268
+ const singletonServices = getSingletonServices()
269
+ if (!singletonServices?.workflowService) {
270
+ return { error: 'Workflow service not available' }
271
+ }
272
+
273
+ const version =
274
+ await singletonServices.workflowService.getWorkflowVersion(
275
+ fullName,
276
+ raw.graphHash
277
+ )
278
+ if (!version) {
279
+ return {
280
+ error: `Workflow '${fullName}' with hash '${raw.graphHash}' not found. Use createAgentWorkflow first.`,
281
+ }
282
+ }
283
+
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
293
+
294
+ if (streamContext) {
295
+ streamContext.channel.send({
296
+ type: 'workflow-created',
297
+ 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
+ })
309
+ } // end if (mode !== 'read')
310
+
311
+ tools.push({
312
+ name: 'listAgentWorkflows',
313
+ description:
314
+ 'List previously saved workflows for this agent that can be executed.',
315
+ inputSchema: {
316
+ type: 'object',
317
+ properties: {},
318
+ },
319
+ execute: async () => {
320
+ const singletonServices = getSingletonServices()
321
+ if (!singletonServices?.workflowService) {
322
+ return { error: 'Workflow service not available' }
323
+ }
324
+
325
+ const results: Array<{
326
+ name: string
327
+ description?: string
328
+ graphHash?: string
329
+ }> = []
330
+
331
+ const allMeta = pikkuState(null, 'workflows', 'meta')
332
+ const prefix = `ai:${agentName}:`
333
+ for (const [name, meta] of Object.entries(allMeta)) {
334
+ if (name.startsWith(prefix) && meta.source === 'ai-agent') {
335
+ results.push({
336
+ name,
337
+ description: meta.description,
338
+ graphHash: meta.graphHash,
339
+ })
340
+ }
341
+ }
342
+
343
+ if (results.length === 0) {
344
+ const persisted =
345
+ await singletonServices.workflowService.getAIGeneratedWorkflows(
346
+ agentName
347
+ )
348
+ for (const wf of persisted) {
349
+ results.push({
350
+ name: wf.workflowName,
351
+ graphHash: wf.graphHash,
352
+ })
353
+ }
354
+ }
355
+
356
+ return { workflows: results }
357
+ },
358
+ })
359
+
360
+ tools.push({
361
+ name: 'executeAgentWorkflow',
362
+ description: 'Execute a previously saved workflow with the given input.',
363
+ inputSchema: {
364
+ type: 'object',
365
+ properties: {
366
+ name: {
367
+ type: 'string',
368
+ description:
369
+ 'Workflow name (will be auto-prefixed with ai:{agentName}: if needed)',
370
+ },
371
+ input: {
372
+ type: 'object',
373
+ description: 'Input data for the workflow trigger',
374
+ },
375
+ },
376
+ required: ['name'],
377
+ },
378
+ needsApproval: true,
379
+ approvalDescriptionFn: async (input: unknown) => {
380
+ const { name, input: workflowInput } = input as {
381
+ name: string
382
+ input?: Record<string, any>
383
+ }
384
+ const fullName = name.startsWith(`ai:${agentName}:`)
385
+ ? name
386
+ : `ai:${agentName}:${name}`
387
+ const inputStr = workflowInput
388
+ ? `\nInput: ${JSON.stringify(workflowInput)}`
389
+ : ''
390
+ return `Execute workflow '${fullName}'${inputStr}`
391
+ },
392
+ execute: async (toolInput: unknown) => {
393
+ const { name, input: workflowInput } = toolInput as {
394
+ name: string
395
+ input?: Record<string, any>
396
+ }
397
+
398
+ const fullName = name.startsWith(`ai:${agentName}:`)
399
+ ? name
400
+ : `ai:${agentName}:${name}`
401
+
402
+ const singletonServices = getSingletonServices()
403
+ if (!singletonServices?.workflowService) {
404
+ return { error: 'Workflow service not available' }
405
+ }
406
+
407
+ const allMeta = pikkuState(null, 'workflows', 'meta')
408
+ if (!allMeta[fullName]) {
409
+ return {
410
+ error: `Workflow '${fullName}' not found. Use listAgentWorkflows to see available workflows, or createAgentWorkflow to make a new one.`,
411
+ }
412
+ }
413
+
414
+ const workflowService = singletonServices.workflowService as any
415
+
416
+ const wire: PikkuWire = sessionService
417
+ ? { ...createMiddlewareSessionWireProps(sessionService) }
418
+ : {}
419
+ const rpcService = new ContextAwareRPCService(singletonServices, wire, {
420
+ sessionService,
421
+ })
422
+
423
+ const { runId } = await runWorkflowGraph(
424
+ workflowService,
425
+ fullName,
426
+ workflowInput ?? {},
427
+ rpcService,
428
+ true,
429
+ undefined,
430
+ { type: 'ai-agent' }
431
+ )
432
+
433
+ const maxWait = 45000
434
+ const startTime = Date.now()
435
+ let pollInterval = 100
436
+ while (Date.now() - startTime < maxWait) {
437
+ const run = await workflowService.getRun(runId)
438
+ if (!run) {
439
+ return { error: `Workflow run '${runId}' not found` }
440
+ }
441
+ if (
442
+ run.status === 'completed' ||
443
+ run.status === 'failed' ||
444
+ run.status === 'cancelled' ||
445
+ run.status === 'suspended'
446
+ ) {
447
+ if (run.status === 'failed') {
448
+ return {
449
+ error: `Workflow failed: ${run.error?.message || 'Unknown error'}`,
450
+ runId,
451
+ }
452
+ }
453
+ return { result: run.output, runId, status: run.status }
454
+ }
455
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
456
+ pollInterval = Math.min(pollInterval * 1.5, 2000)
457
+ }
458
+
459
+ return {
460
+ runId,
461
+ status: 'running',
462
+ message:
463
+ 'Workflow is still running after timeout. It will continue in the background.',
464
+ }
465
+ },
466
+ })
467
+
468
+ return tools
469
+ }