@pikku/core 0.12.13 → 0.12.15

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 (150) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/errors/errors.d.ts +18 -0
  3. package/dist/errors/errors.js +33 -0
  4. package/dist/function/function-runner.d.ts +1 -1
  5. package/dist/function/function-runner.js +10 -5
  6. package/dist/function/functions.types.js +1 -0
  7. package/dist/function/index.d.ts +2 -1
  8. package/dist/function/index.js +1 -0
  9. package/dist/handle-error.d.ts +2 -2
  10. package/dist/handle-error.js +8 -8
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.js +1 -2
  13. package/dist/middleware/index.d.ts +2 -0
  14. package/dist/middleware/index.js +2 -0
  15. package/dist/middleware/remote-auth.js +5 -2
  16. package/dist/permissions.d.ts +13 -1
  17. package/dist/permissions.js +51 -0
  18. package/dist/pikku-state.d.ts +0 -1
  19. package/dist/pikku-state.js +2 -4
  20. package/dist/remote.d.ts +10 -0
  21. package/dist/remote.js +32 -0
  22. package/dist/schema.js +2 -0
  23. package/dist/services/credential-service.d.ts +11 -0
  24. package/dist/services/credential-wire-service.d.ts +14 -3
  25. package/dist/services/credential-wire-service.js +49 -6
  26. package/dist/services/deployment-service.d.ts +12 -1
  27. package/dist/services/in-memory-queue-service.d.ts +7 -0
  28. package/dist/services/in-memory-queue-service.js +28 -0
  29. package/dist/services/index.d.ts +4 -1
  30. package/dist/services/index.js +2 -1
  31. package/dist/services/local-credential-service.d.ts +2 -0
  32. package/dist/services/local-credential-service.js +20 -0
  33. package/dist/services/logger-console.d.ts +26 -40
  34. package/dist/services/logger-console.js +102 -46
  35. package/dist/services/logger.d.ts +5 -0
  36. package/dist/services/meta-service.d.ts +175 -0
  37. package/dist/services/meta-service.js +310 -0
  38. package/dist/services/pikku-user-id.d.ts +3 -0
  39. package/dist/services/pikku-user-id.js +16 -0
  40. package/dist/services/typed-credential-service.d.ts +2 -0
  41. package/dist/services/typed-credential-service.js +6 -0
  42. package/dist/services/workflow-service.d.ts +6 -1
  43. package/dist/testing/service-tests.js +0 -8
  44. package/dist/types/core.types.d.ts +14 -2
  45. package/dist/types/state.types.d.ts +9 -2
  46. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +21 -0
  47. package/dist/wirings/ai-agent/ai-agent-prepare.js +90 -21
  48. package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
  49. package/dist/wirings/ai-agent/ai-agent-runner.js +24 -2
  50. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
  51. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -3
  52. package/dist/wirings/ai-agent/ai-agent.types.d.ts +25 -4
  53. package/dist/wirings/ai-agent/index.d.ts +1 -1
  54. package/dist/wirings/ai-agent/index.js +1 -1
  55. package/dist/wirings/channel/channel-runner.js +3 -3
  56. package/dist/wirings/cli/cli-runner.js +4 -3
  57. package/dist/wirings/cli/command-parser.js +7 -3
  58. package/dist/wirings/http/http-runner.d.ts +1 -1
  59. package/dist/wirings/http/http-runner.js +23 -15
  60. package/dist/wirings/http/http.types.d.ts +2 -0
  61. package/dist/wirings/mcp/mcp-runner.js +5 -3
  62. package/dist/wirings/queue/queue-runner.d.ts +3 -1
  63. package/dist/wirings/queue/queue-runner.js +8 -3
  64. package/dist/wirings/queue/queue.types.d.ts +6 -0
  65. package/dist/wirings/rpc/rpc-runner.js +57 -90
  66. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
  67. package/dist/wirings/scheduler/scheduler-runner.js +9 -5
  68. package/dist/wirings/trigger/trigger-runner.js +4 -2
  69. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +2 -0
  70. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
  71. package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
  72. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  73. package/dist/wirings/workflow/graph/graph-runner.js +18 -4
  74. package/dist/wirings/workflow/index.d.ts +4 -2
  75. package/dist/wirings/workflow/index.js +4 -2
  76. package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
  77. package/dist/wirings/workflow/pikku-workflow-service.js +218 -24
  78. package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
  79. package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
  80. package/dist/wirings/workflow/workflow.types.d.ts +19 -1
  81. package/package.json +4 -1
  82. package/src/errors/errors.ts +44 -0
  83. package/src/function/function-runner.ts +24 -13
  84. package/src/function/functions.types.ts +1 -0
  85. package/src/function/index.ts +10 -0
  86. package/src/handle-error.test.ts +2 -2
  87. package/src/handle-error.ts +8 -8
  88. package/src/index.ts +1 -2
  89. package/src/middleware/index.ts +2 -0
  90. package/src/middleware/remote-auth.test.ts +4 -4
  91. package/src/middleware/remote-auth.ts +7 -2
  92. package/src/permissions.ts +70 -0
  93. package/src/pikku-state.test.ts +0 -26
  94. package/src/pikku-state.ts +2 -5
  95. package/src/remote.ts +46 -0
  96. package/src/schema.ts +1 -0
  97. package/src/services/credential-service.ts +13 -0
  98. package/src/services/credential-wire-service.test.ts +174 -0
  99. package/src/services/credential-wire-service.ts +52 -8
  100. package/src/services/deployment-service.ts +17 -1
  101. package/src/services/in-memory-queue-service.ts +46 -0
  102. package/src/services/index.ts +21 -1
  103. package/src/services/local-credential-service.ts +22 -0
  104. package/src/services/logger-console.ts +127 -47
  105. package/src/services/logger.ts +6 -0
  106. package/src/services/meta-service.ts +523 -0
  107. package/src/services/pikku-user-id.test.ts +62 -0
  108. package/src/services/pikku-user-id.ts +17 -0
  109. package/src/services/typed-credential-service.ts +8 -0
  110. package/src/services/workflow-service.ts +8 -0
  111. package/src/testing/service-tests.ts +0 -10
  112. package/src/types/core.types.ts +16 -2
  113. package/src/types/state.types.ts +7 -2
  114. package/src/wirings/ai-agent/ai-agent-prepare.ts +122 -41
  115. package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
  116. package/src/wirings/ai-agent/ai-agent-runner.ts +22 -3
  117. package/src/wirings/ai-agent/ai-agent-stream.ts +115 -3
  118. package/src/wirings/ai-agent/ai-agent.types.ts +27 -4
  119. package/src/wirings/ai-agent/index.ts +1 -0
  120. package/src/wirings/channel/channel-handler.ts +0 -1
  121. package/src/wirings/channel/channel-runner.ts +4 -5
  122. package/src/wirings/cli/cli-runner.test.ts +8 -7
  123. package/src/wirings/cli/cli-runner.ts +5 -4
  124. package/src/wirings/cli/command-parser.ts +8 -3
  125. package/src/wirings/http/http-runner.ts +27 -18
  126. package/src/wirings/http/http.types.ts +2 -0
  127. package/src/wirings/mcp/mcp-runner.ts +7 -10
  128. package/src/wirings/queue/queue-runner.test.ts +5 -11
  129. package/src/wirings/queue/queue-runner.ts +12 -4
  130. package/src/wirings/queue/queue.types.ts +6 -0
  131. package/src/wirings/rpc/rpc-runner.ts +91 -118
  132. package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
  133. package/src/wirings/scheduler/scheduler-runner.ts +13 -5
  134. package/src/wirings/trigger/trigger-runner.test.ts +10 -11
  135. package/src/wirings/trigger/trigger-runner.ts +6 -4
  136. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +2 -0
  137. package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
  138. package/src/wirings/workflow/graph/graph-runner.ts +19 -4
  139. package/src/wirings/workflow/index.ts +17 -6
  140. package/src/wirings/workflow/pikku-workflow-service.ts +286 -24
  141. package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
  142. package/src/wirings/workflow/workflow.types.ts +18 -1
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
  145. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
  146. package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
  147. package/dist/wirings/workflow/workflow-helpers.js +0 -38
  148. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
  149. package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
  150. package/src/wirings/workflow/workflow-helpers.ts +0 -79
@@ -34,6 +34,7 @@ import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
34
34
  import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
35
35
  import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
36
36
  import type { CredentialService } from '../services/credential-service.js'
37
+ import type { MetaService } from '../services/meta-service.js'
37
38
 
38
39
  export type PikkuWiringTypes =
39
40
  | 'http'
@@ -108,6 +109,7 @@ export type FunctionRuntimeMeta = {
108
109
  remote?: boolean
109
110
  mcp?: boolean
110
111
  readonly?: boolean
112
+ deploy?: 'serverless' | 'server' | 'auto'
111
113
  sessionless?: boolean
112
114
  version?: number
113
115
  approvalRequired?: boolean
@@ -130,6 +132,8 @@ export type FunctionMeta = FunctionRuntimeMeta &
130
132
  middleware: MiddlewareMetadata[]
131
133
  permissions: PermissionMetadata[]
132
134
  isDirectFunction: boolean
135
+ sourceFile: string
136
+ exportedName: string
133
137
  } & CommonWireMeta
134
138
  >
135
139
 
@@ -230,6 +234,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
230
234
  workflowRunService?: WorkflowRunService
231
235
  /** Credential service for dynamic/managed credentials (OAuth tokens, per-user API keys) */
232
236
  credentialService?: CredentialService
237
+ /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
238
+ metaService?: MetaService
233
239
  }
234
240
 
235
241
  /**
@@ -248,6 +254,8 @@ export type PikkuWire<
248
254
  > = Partial<{
249
255
  wireType: PikkuWiringTypes
250
256
  wireId: string
257
+ /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
258
+ traceId: string
251
259
  http: PikkuHTTP<In>
252
260
  mcp: PikkuMCP<MCPTools>
253
261
  rpc: TypedRPC
@@ -273,10 +281,16 @@ export type PikkuWire<
273
281
  getSession: () => Promise<UserSession> | UserSession | undefined
274
282
  /** Whether the session was modified during this run */
275
283
  hasSessionChanged: () => boolean
284
+ /** The resolved user identity for credential lookups */
285
+ pikkuUserId: string
276
286
  /** Set a credential value (available in middleware) */
277
287
  setCredential: (name: string, value: unknown) => void
278
- /** Get all resolved credentials (only available in pikkuWireServices) */
279
- getCredentials: () => Record<string, unknown>
288
+ /** Get a single credential by name lazy-loads from CredentialService on first call, sync thereafter */
289
+ getCredential: <T = unknown>(name: string) => T | null | Promise<T | null>
290
+ /** Get all resolved credentials — lazy-loads from CredentialService on first call, sync thereafter */
291
+ getCredentials: () =>
292
+ | Record<string, unknown>
293
+ | Promise<Record<string, unknown>>
280
294
  }>
281
295
 
282
296
  /**
@@ -182,7 +182,12 @@ export interface PikkuPackageState {
182
182
  } | null
183
183
  /** Cached singleton services for this package */
184
184
  singletonServices: CoreSingletonServices | null
185
- /** Absolute path to this package's .pikku directory */
186
- metaDir: string | null
185
+ /** Credential metadata for this addon package */
186
+ credentialsMeta: Record<
187
+ string,
188
+ { name: string; displayName: string; type: string; oauth2?: boolean }
189
+ > | null
190
+ /** Services this addon needs from the parent project */
191
+ requiredParentServices: string[] | null
187
192
  }
188
193
  }
@@ -11,6 +11,7 @@ import type {
11
11
  } from './ai-agent.types.js'
12
12
  import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js'
13
13
  import { PikkuError } from '../../errors/error-handler.js'
14
+ import { checkAuthPermissions } from '../../permissions.js'
14
15
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
15
16
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
16
17
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
@@ -22,10 +23,6 @@ import {
22
23
  resolveNamespace,
23
24
  ContextAwareRPCService,
24
25
  } from '../../wirings/rpc/rpc-runner.js'
25
- import {
26
- buildDynamicWorkflowInstructions,
27
- buildWorkflowTools,
28
- } from './agent-dynamic-workflow.js'
29
26
  import {
30
27
  resolveMemoryServices,
31
28
  loadContextMessages,
@@ -70,6 +67,75 @@ export class ToolApprovalRequired extends PikkuError {
70
67
  }
71
68
  }
72
69
 
70
+ export class ToolCredentialRequired extends PikkuError {
71
+ public readonly toolCallId: string
72
+ public readonly toolName: string
73
+ public readonly args: unknown
74
+ public readonly credentialName: string
75
+ public readonly credentialType: 'oauth2' | 'apikey'
76
+ public readonly connectUrl?: string
77
+
78
+ constructor(
79
+ toolCallId: string,
80
+ toolName: string,
81
+ args: unknown,
82
+ credentialName: string,
83
+ credentialType: 'oauth2' | 'apikey',
84
+ connectUrl?: string
85
+ ) {
86
+ super(`Tool '${toolName}' requires credential '${credentialName}'`)
87
+ this.toolCallId = toolCallId
88
+ this.toolName = toolName
89
+ this.args = args
90
+ this.credentialName = credentialName
91
+ this.credentialType = credentialType
92
+ this.connectUrl = connectUrl
93
+ }
94
+ }
95
+
96
+ export interface AddonCredentialRequirement {
97
+ credentialName: string
98
+ displayName: string
99
+ addonNamespace: string
100
+ type: 'wire'
101
+ oauth2: boolean
102
+ }
103
+
104
+ /**
105
+ * Given a list of tool names (e.g. ["oauth-api:getProfile"]),
106
+ * returns the wire OAuth credentials required by their addons.
107
+ */
108
+ export function getAddonCredentialRequirements(
109
+ toolNames: string[]
110
+ ): AddonCredentialRequirement[] {
111
+ const requirements = new Map<string, AddonCredentialRequirement>()
112
+
113
+ for (const toolName of toolNames) {
114
+ if (!toolName.includes(':')) continue
115
+ const resolved = resolveNamespace(toolName)
116
+ if (!resolved) continue
117
+
118
+ const credsMeta = pikkuState(resolved.package, 'package', 'credentialsMeta')
119
+ if (!credsMeta) continue
120
+
121
+ for (const [name, meta] of Object.entries(
122
+ credsMeta as Record<string, any>
123
+ )) {
124
+ if (meta.type === 'wire' && meta.oauth2 && !requirements.has(name)) {
125
+ requirements.set(name, {
126
+ credentialName: name,
127
+ displayName: meta.displayName ?? name,
128
+ addonNamespace: toolName.split(':')[0],
129
+ type: 'wire',
130
+ oauth2: true,
131
+ })
132
+ }
133
+ }
134
+ }
135
+
136
+ return [...requirements.values()]
137
+ }
138
+
73
139
  export type StreamContext = {
74
140
  channel: AIStreamChannel
75
141
  options?: StreamAIAgentOptions
@@ -79,6 +145,13 @@ export type StreamContext = {
79
145
  export const resolveAgent = (
80
146
  agentName: string
81
147
  ): { agent: CoreAIAgent; packageName: string | null; resolvedName: string } => {
148
+ if (!agentName) {
149
+ console.error(
150
+ '[resolveAgent] agentName is undefined/null! Stack:',
151
+ new Error().stack
152
+ )
153
+ throw new Error('resolveAgent called with undefined agentName')
154
+ }
82
155
  const mainAgent = pikkuState(null, 'agent', 'agents').get(agentName)
83
156
  if (mainAgent) {
84
157
  return { agent: mainAgent, packageName: null, resolvedName: agentName }
@@ -112,10 +185,21 @@ export async function buildInstructions(
112
185
  packageName: string | null
113
186
  ): Promise<string> {
114
187
  const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
115
- const rawInstructions = meta?.instructions ?? ''
116
- let instructions = Array.isArray(rawInstructions)
117
- ? rawInstructions.join('\n')
118
- : rawInstructions
188
+ const parts: string[] = []
189
+ if (meta?.role) parts.push(meta.role)
190
+ if (meta?.personality) parts.push(meta.personality)
191
+ if (meta?.goal) parts.push(meta.goal)
192
+ let instructions = parts.join('\n\n')
193
+
194
+ if (meta?.tools?.length) {
195
+ instructions +=
196
+ '\n\nTool usage rules:\n' +
197
+ '- Act immediately with the information given. Do NOT ask clarifying questions unless a required field is truly missing.\n' +
198
+ '- Only use fields defined in your tool schemas. Never mention or ask for fields that do not exist.\n' +
199
+ '- Never fill optional fields with placeholder or zero values. Omit them entirely unless the user provides a real value.\n' +
200
+ '- Never stuff unrelated information into the wrong field.\n' +
201
+ '- Keep responses concise.'
202
+ }
119
203
 
120
204
  if (meta?.agents?.length) {
121
205
  instructions +=
@@ -125,13 +209,6 @@ export async function buildInstructions(
125
209
  'When a request involves multiple actions for the same domain, combine them into a single sub-agent call rather than making separate calls.'
126
210
  }
127
211
 
128
- if (meta?.dynamicWorkflows && meta.tools?.length) {
129
- instructions += buildDynamicWorkflowInstructions(
130
- meta.tools,
131
- meta.dynamicWorkflows
132
- )
133
- }
134
-
135
212
  return instructions
136
213
  }
137
214
 
@@ -210,6 +287,11 @@ export async function buildToolDefs(
210
287
  const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
211
288
  if (!meta) return { tools, missingRpcs }
212
289
 
290
+ // Get session for permission filtering
291
+ const session = params.sessionService
292
+ ? await params.sessionService.get()
293
+ : null
294
+
213
295
  const metaTools = meta.tools
214
296
  const metaAgents = meta.agents
215
297
 
@@ -246,6 +328,18 @@ export async function buildToolDefs(
246
328
  continue
247
329
  }
248
330
 
331
+ // Filter out tools the user doesn't have auth for
332
+ if (fnMeta.permissions?.length) {
333
+ if (!session) continue
334
+ const allowed = await checkAuthPermissions(
335
+ fnMeta.permissions,
336
+ session,
337
+ singletonServices,
338
+ resolvedPkg
339
+ )
340
+ if (!allowed) continue
341
+ }
342
+
249
343
  const inputSchemaName = fnMeta?.inputSchemaName
250
344
  let inputSchema = inputSchemaName
251
345
  ? schemas.get(inputSchemaName)
@@ -324,6 +418,17 @@ export async function buildToolDefs(
324
418
  continue
325
419
  }
326
420
 
421
+ // Filter out sub-agents the user doesn't have auth for
422
+ if (subMeta.permissions?.length) {
423
+ if (!session) continue
424
+ const allowed = await checkAuthPermissions(
425
+ subMeta.permissions,
426
+ session,
427
+ singletonServices
428
+ )
429
+ if (!allowed) continue
430
+ }
431
+
327
432
  tools.push({
328
433
  name: subAgentName,
329
434
  description: subMeta.description,
@@ -438,18 +543,6 @@ export async function buildToolDefs(
438
543
  }
439
544
  }
440
545
 
441
- if (meta.dynamicWorkflows) {
442
- const workflowTools = buildWorkflowTools(
443
- agentName,
444
- packageName,
445
- meta.tools ?? [],
446
- meta.dynamicWorkflows,
447
- streamContext,
448
- params.sessionService
449
- )
450
- tools.push(...workflowTools)
451
- }
452
-
453
546
  const hasToolHooks = aiMiddlewares?.some(
454
547
  (mw) => mw.beforeToolCall || mw.afterToolCall
455
548
  )
@@ -478,8 +571,9 @@ export async function buildToolDefs(
478
571
  let execError: unknown
479
572
  try {
480
573
  result = await originalExecute(args)
481
- } catch (err) {
574
+ } catch (err: any) {
482
575
  execError = err
576
+ if (err?.payload?.error === 'missing_credential') throw err
483
577
  result = err instanceof Error ? err.message : String(err)
484
578
  }
485
579
  const durationMs = Date.now() - startTime
@@ -524,19 +618,6 @@ export async function prepareAgentRun(
524
618
  throw new AIProviderNotConfiguredError()
525
619
  }
526
620
 
527
- if (agent.dynamicWorkflows && singletonServices.workflowService) {
528
- const persisted =
529
- await singletonServices.workflowService.getAIGeneratedWorkflows(
530
- resolvedName
531
- )
532
- const allMeta = pikkuState(null, 'workflows', 'meta')
533
- for (const wf of persisted) {
534
- if (!allMeta[wf.workflowName]) {
535
- allMeta[wf.workflowName] = wf.graph
536
- }
537
- }
538
- }
539
-
540
621
  const { storage } = resolveMemoryServices(agent, singletonServices)
541
622
  const memoryConfig = agent.memory
542
623
  const threadId = input.threadId
@@ -1,7 +1,6 @@
1
1
  import type { CoreAIAgent, AgentRunState } from './ai-agent.types.js'
2
2
  import { pikkuState } from '../../pikku-state.js'
3
3
  import type { AIRunStateService } from '../../services/ai-run-state-service.js'
4
- import { PikkuMissingMetaError } from '../../errors/errors.js'
5
4
 
6
5
  export const addAIAgent = (
7
6
  agentName: string,
@@ -11,9 +10,10 @@ export const addAIAgent = (
11
10
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
12
11
  const agentMeta = agentsMeta[agentName]
13
12
  if (!agentMeta) {
14
- throw new PikkuMissingMetaError(
15
- `Missing generated metadata for AI agent '${agentName}'`
13
+ console.warn(
14
+ `[pikku] Skipping AI agent '${agentName}' — metadata not found. Consider moving this wiring to its own file.`
16
15
  )
16
+ return
17
17
  }
18
18
  const agents = pikkuState(packageName, 'agent', 'agents')
19
19
  if (agents.has(agentName)) {
@@ -32,6 +32,19 @@ import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js'
32
32
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
33
33
  import { resolveModelConfig } from './ai-agent-model-config.js'
34
34
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
35
+
36
+ function stripNulls(obj: unknown): unknown {
37
+ if (obj === null) return undefined
38
+ if (Array.isArray(obj)) return obj.map(stripNulls)
39
+ if (typeof obj !== 'object') return obj
40
+ const result: Record<string, unknown> = {}
41
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
42
+ if (value !== null) {
43
+ result[key] = stripNulls(value)
44
+ }
45
+ }
46
+ return result
47
+ }
35
48
  import { randomUUID } from 'crypto'
36
49
 
37
50
  export async function runAIAgent(
@@ -420,18 +433,24 @@ export async function resumeAIAgentSync(
420
433
  `Tool "${pending.toolName}" not found in agent definition`
421
434
  )
422
435
  }
423
- const toolArgs =
436
+ const rawArgs =
424
437
  typeof pending.args === 'string'
425
438
  ? JSON.parse(pending.args)
426
439
  : pending.args
440
+ // Strip null values recursively — LLMs send null for optional fields but Zod expects undefined
441
+ const toolArgs = stripNulls(rawArgs) ?? {}
427
442
  try {
428
443
  const toolResult = await matchingTool.execute(toolArgs)
429
444
  resultStr =
430
445
  typeof toolResult === 'string'
431
446
  ? toolResult
432
447
  : JSON.stringify(toolResult)
433
- } catch (err) {
434
- resultStr = `Error: ${err instanceof Error ? err.message : String(err)}`
448
+ } catch (err: any) {
449
+ if (err?.payload?.error === 'missing_credential') {
450
+ resultStr = JSON.stringify(err.payload)
451
+ } else {
452
+ resultStr = `Error: ${err instanceof Error ? err.message : String(err)}`
453
+ }
435
454
  }
436
455
  } else {
437
456
  continue
@@ -36,6 +36,7 @@ import {
36
36
  buildToolDefs,
37
37
  createScopedChannel,
38
38
  ToolApprovalRequired,
39
+ ToolCredentialRequired,
39
40
  type RunAIAgentParams,
40
41
  type StreamAIAgentOptions,
41
42
  type StreamContext,
@@ -236,6 +237,7 @@ type StepLoopParams = {
236
237
  type StepLoopResult =
237
238
  | { outcome: 'done' }
238
239
  | { outcome: 'approval'; approvals: ToolApprovalRequired[] }
240
+ | { outcome: 'credential'; credentialRequests: ToolCredentialRequired[] }
239
241
 
240
242
  async function runStreamStepLoop(
241
243
  params: StepLoopParams
@@ -310,6 +312,13 @@ async function runStreamStepLoop(
310
312
  return { outcome: 'approval', approvals: approvalsNeeded }
311
313
  }
312
314
 
315
+ const credentialRequests = checkForCredentialRequests(stepResult, runId)
316
+ if (credentialRequests.length > 0) {
317
+ // Append step messages so the tool result is preserved for resume
318
+ appendStepMessages(runnerParams, stepResult)
319
+ return { outcome: 'credential', credentialRequests }
320
+ }
321
+
313
322
  appendStepMessages(runnerParams, stepResult)
314
323
  }
315
324
 
@@ -386,6 +395,40 @@ export function checkForApprovals(
386
395
  return approvals
387
396
  }
388
397
 
398
+ export function checkForCredentialRequests(
399
+ stepResult: AIAgentStepResult,
400
+ runId: string
401
+ ): ToolCredentialRequired[] {
402
+ const requests: ToolCredentialRequired[] = []
403
+ for (const tr of stepResult.toolResults) {
404
+ if (
405
+ tr.result &&
406
+ typeof tr.result === 'object' &&
407
+ '__credentialRequired' in (tr.result as object)
408
+ ) {
409
+ const r = tr.result as {
410
+ credentialName: string
411
+ credentialType: 'oauth2' | 'apikey'
412
+ connectUrl?: string
413
+ }
414
+ const tc = stepResult.toolCalls.find(
415
+ (t) => t.toolCallId === tr.toolCallId
416
+ )
417
+ requests.push(
418
+ new ToolCredentialRequired(
419
+ tr.toolCallId,
420
+ tc?.toolName ?? 'unknown',
421
+ tc?.args ?? {},
422
+ r.credentialName,
423
+ r.credentialType,
424
+ r.connectUrl
425
+ )
426
+ )
427
+ }
428
+ }
429
+ return requests
430
+ }
431
+
389
432
  export function appendStepMessages(
390
433
  runnerParams: AIAgentRunnerParams,
391
434
  stepResult: AIAgentStepResult
@@ -472,6 +515,40 @@ function handleApprovals(
472
515
  })()
473
516
  }
474
517
 
518
+ function handleCredentialRequests(
519
+ requests: ToolCredentialRequired[],
520
+ runId: string,
521
+ channel: AIStreamChannel,
522
+ aiRunState: AIRunStateService,
523
+ persistingChannel: PersistingChannel
524
+ ): Promise<void> {
525
+ return (async () => {
526
+ await persistingChannel.flush()
527
+
528
+ const pendingApprovals = requests.map((req) => ({
529
+ type: 'credential-request' as const,
530
+ toolCallId: req.toolCallId,
531
+ toolName: req.toolName,
532
+ args: req.args,
533
+ credentialName: req.credentialName,
534
+ credentialType: req.credentialType,
535
+ connectUrl: req.connectUrl,
536
+ }))
537
+
538
+ await aiRunState.updateRun(runId, {
539
+ status: 'suspended',
540
+ suspendReason: 'credential',
541
+ pendingApprovals,
542
+ })
543
+
544
+ // Don't send credential-request SSE events — the tool result with
545
+ // __credentialRequired was already streamed. The frontend detects it
546
+ // from the tool result and shows Connect/Ignore buttons.
547
+ channel.send({ type: 'done' })
548
+ channel.close()
549
+ })()
550
+ }
551
+
475
552
  export async function streamAIAgent(
476
553
  agentName: string,
477
554
  input: {
@@ -665,6 +742,17 @@ export async function streamAIAgent(
665
742
  return persistingChannel.fullText
666
743
  }
667
744
 
745
+ if (loopResult.outcome === 'credential') {
746
+ await handleCredentialRequests(
747
+ loopResult.credentialRequests,
748
+ runId,
749
+ channel,
750
+ aiRunState,
751
+ persistingChannel
752
+ )
753
+ return persistingChannel.fullText
754
+ }
755
+
668
756
  await postStreamCleanup(
669
757
  persistingChannel,
670
758
  aiMiddlewares,
@@ -769,7 +857,8 @@ export async function resumeAIAgent(
769
857
  {
770
858
  id: input.toolCallId,
771
859
  name:
772
- pending.type === 'tool-call'
860
+ pending.type === 'tool-call' ||
861
+ pending.type === 'credential-request'
773
862
  ? pending.toolName
774
863
  : pending.agentName,
775
864
  result: denialResult,
@@ -896,8 +985,12 @@ export async function resumeAIAgent(
896
985
  let isError = false
897
986
  try {
898
987
  toolResult = await matchingTool.execute(toolArgs)
899
- } catch (execErr) {
900
- toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
988
+ } catch (execErr: any) {
989
+ if (execErr?.payload?.error === 'missing_credential') {
990
+ toolResult = execErr.payload
991
+ } else {
992
+ toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
993
+ }
901
994
  isError = true
902
995
  }
903
996
 
@@ -1073,6 +1166,14 @@ async function continueAfterToolResult(
1073
1166
  ).tools
1074
1167
 
1075
1168
  const resolved = resolveModelConfig(resolvedName, agent)
1169
+ console.log(
1170
+ '[DEBUG resume] resolvedName:',
1171
+ resolvedName,
1172
+ 'agent.model:',
1173
+ agent.model,
1174
+ 'resolved.model:',
1175
+ resolved.model
1176
+ )
1076
1177
  const maxSteps = resolved.maxSteps ?? 10
1077
1178
 
1078
1179
  const runnerParams: AIAgentRunnerParams = {
@@ -1111,6 +1212,17 @@ async function continueAfterToolResult(
1111
1212
  return
1112
1213
  }
1113
1214
 
1215
+ if (loopResult.outcome === 'credential') {
1216
+ await handleCredentialRequests(
1217
+ loopResult.credentialRequests,
1218
+ run.runId,
1219
+ channel,
1220
+ aiRunState,
1221
+ persistingChannel
1222
+ )
1223
+ return
1224
+ }
1225
+
1114
1226
  await postStreamCleanup(
1115
1227
  persistingChannel,
1116
1228
  aiMiddlewares,
@@ -195,7 +195,9 @@ export type CoreAIAgent<
195
195
  description: string
196
196
  summary?: string
197
197
  errors?: string[]
198
- instructions: string | string[]
198
+ role?: string
199
+ personality?: string
200
+ goal: string
199
201
  model: string
200
202
  temperature?: number
201
203
  tools?: unknown[]
@@ -204,7 +206,6 @@ export type CoreAIAgent<
204
206
  memory?: AIAgentMemoryConfig
205
207
  maxSteps?: number
206
208
  toolChoice?: 'auto' | 'required' | 'none'
207
- dynamicWorkflows?: 'read' | 'always' | 'ask'
208
209
  input?: unknown
209
210
  output?: unknown
210
211
  tags?: string[]
@@ -263,6 +264,18 @@ export type AIStreamEvent =
263
264
  agent?: string
264
265
  session?: string
265
266
  }
267
+ | {
268
+ type: 'credential-request'
269
+ toolCallId: string
270
+ toolName: string
271
+ args: unknown
272
+ credentialName: string
273
+ credentialType: 'oauth2' | 'apikey'
274
+ connectUrl?: string
275
+ runId: string
276
+ agent?: string
277
+ session?: string
278
+ }
266
279
  | {
267
280
  type: 'usage'
268
281
  tokens: { input: number; output: number }
@@ -310,6 +323,15 @@ export type PendingApproval =
310
323
  displayToolName: string
311
324
  displayArgs: unknown
312
325
  }
326
+ | {
327
+ type: 'credential-request'
328
+ toolCallId: string
329
+ toolName: string
330
+ args: unknown
331
+ credentialName: string
332
+ credentialType: 'oauth2' | 'apikey'
333
+ connectUrl?: string
334
+ }
313
335
 
314
336
  export interface AgentRunState {
315
337
  runId: string
@@ -318,7 +340,7 @@ export interface AgentRunState {
318
340
  resourceId: string
319
341
  status: 'running' | 'suspended' | 'completed' | 'failed'
320
342
  errorMessage?: string
321
- suspendReason?: 'approval' | 'rpc-missing'
343
+ suspendReason?: 'approval' | 'credential' | 'rpc-missing'
322
344
  missingRpcs?: string[]
323
345
  pendingApprovals?: PendingApproval[]
324
346
  usage: { inputTokens: number; outputTokens: number; model: string }
@@ -377,6 +399,7 @@ export type AIAgentMeta = Record<
377
399
  channelMiddleware?: MiddlewareMetadata[]
378
400
  aiMiddleware?: MiddlewareMetadata[]
379
401
  permissions?: PermissionMetadata[]
380
- dynamicWorkflows?: 'read' | 'always' | 'ask'
402
+ sourceFile?: string
403
+ exportedName?: string
381
404
  }
382
405
  >
@@ -10,6 +10,7 @@ export {
10
10
  type RunAIAgentParams,
11
11
  type StreamAIAgentOptions,
12
12
  ToolApprovalRequired,
13
+ ToolCredentialRequired,
13
14
  } from './ai-agent-prepare.js'
14
15
  export {
15
16
  addAIAgent,
@@ -13,7 +13,6 @@ import { pikkuState } from '../../pikku-state.js'
13
13
  import { runPikkuFunc } from '../../function/function-runner.js'
14
14
  import type { SessionService } from '../../services/user-session-service.js'
15
15
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
16
-
17
16
  const getChannelMeta = (channelName: string) => {
18
17
  return pikkuState(null, 'channel', 'meta')[channelName]
19
18
  }
@@ -1,4 +1,4 @@
1
- import { NotFoundError, PikkuMissingMetaError } from '../../errors/errors.js'
1
+ import { NotFoundError } from '../../errors/errors.js'
2
2
  import { addFunction } from '../../function/function-runner.js'
3
3
  import type { CorePikkuPermission } from '../../function/functions.types.js'
4
4
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
@@ -36,13 +36,12 @@ export const wireChannel = <
36
36
  const channelsMeta = pikkuState(null, 'channel', 'meta')
37
37
  const channelMeta = channelsMeta[channel.name]
38
38
  if (!channelMeta) {
39
- throw new PikkuMissingMetaError(
40
- `Missing generated metadata for channel '${channel.name}'`
39
+ console.warn(
40
+ `[pikku] Skipping channel '${channel.name}' — metadata not found. Consider moving this wiring to its own file.`
41
41
  )
42
+ return
42
43
  }
43
44
 
44
- pikkuState(null, 'channel', 'channels').set(channel.name, channel as any)
45
-
46
45
  // Register onConnect function if provided
47
46
  if (channel.onConnect && channelMeta.connect) {
48
47
  addFunction(channelMeta.connect.pikkuFuncId, channel.onConnect as any)