@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
@@ -1,4 +1,5 @@
1
1
  import { PikkuError } from '../../errors/error-handler.js';
2
+ import { checkAuthPermissions } from '../../permissions.js';
2
3
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
3
4
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
4
5
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
@@ -6,7 +7,6 @@ import { randomUUID } from 'crypto';
6
7
  import { streamAIAgent } from './ai-agent-stream.js';
7
8
  import { runAIAgent } from './ai-agent-runner.js';
8
9
  import { resolveNamespace, ContextAwareRPCService, } from '../../wirings/rpc/rpc-runner.js';
9
- import { buildDynamicWorkflowInstructions, buildWorkflowTools, } from './agent-dynamic-workflow.js';
10
10
  import { resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
11
11
  import { resolveModelConfig } from './ai-agent-model-config.js';
12
12
  export class ToolApprovalRequired extends PikkuError {
@@ -28,7 +28,57 @@ export class ToolApprovalRequired extends PikkuError {
28
28
  this.agentRunId = agentRunId;
29
29
  }
30
30
  }
31
+ export class ToolCredentialRequired extends PikkuError {
32
+ toolCallId;
33
+ toolName;
34
+ args;
35
+ credentialName;
36
+ credentialType;
37
+ connectUrl;
38
+ constructor(toolCallId, toolName, args, credentialName, credentialType, connectUrl) {
39
+ super(`Tool '${toolName}' requires credential '${credentialName}'`);
40
+ this.toolCallId = toolCallId;
41
+ this.toolName = toolName;
42
+ this.args = args;
43
+ this.credentialName = credentialName;
44
+ this.credentialType = credentialType;
45
+ this.connectUrl = connectUrl;
46
+ }
47
+ }
48
+ /**
49
+ * Given a list of tool names (e.g. ["oauth-api:getProfile"]),
50
+ * returns the wire OAuth credentials required by their addons.
51
+ */
52
+ export function getAddonCredentialRequirements(toolNames) {
53
+ const requirements = new Map();
54
+ for (const toolName of toolNames) {
55
+ if (!toolName.includes(':'))
56
+ continue;
57
+ const resolved = resolveNamespace(toolName);
58
+ if (!resolved)
59
+ continue;
60
+ const credsMeta = pikkuState(resolved.package, 'package', 'credentialsMeta');
61
+ if (!credsMeta)
62
+ continue;
63
+ for (const [name, meta] of Object.entries(credsMeta)) {
64
+ if (meta.type === 'wire' && meta.oauth2 && !requirements.has(name)) {
65
+ requirements.set(name, {
66
+ credentialName: name,
67
+ displayName: meta.displayName ?? name,
68
+ addonNamespace: toolName.split(':')[0],
69
+ type: 'wire',
70
+ oauth2: true,
71
+ });
72
+ }
73
+ }
74
+ }
75
+ return [...requirements.values()];
76
+ }
31
77
  export const resolveAgent = (agentName) => {
78
+ if (!agentName) {
79
+ console.error('[resolveAgent] agentName is undefined/null! Stack:', new Error().stack);
80
+ throw new Error('resolveAgent called with undefined agentName');
81
+ }
32
82
  const mainAgent = pikkuState(null, 'agent', 'agents').get(agentName);
33
83
  if (mainAgent) {
34
84
  return { agent: mainAgent, packageName: null, resolvedName: agentName };
@@ -54,10 +104,23 @@ export const resolveAgent = (agentName) => {
54
104
  };
55
105
  export async function buildInstructions(agentName, packageName) {
56
106
  const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName];
57
- const rawInstructions = meta?.instructions ?? '';
58
- let instructions = Array.isArray(rawInstructions)
59
- ? rawInstructions.join('\n')
60
- : rawInstructions;
107
+ const parts = [];
108
+ if (meta?.role)
109
+ parts.push(meta.role);
110
+ if (meta?.personality)
111
+ parts.push(meta.personality);
112
+ if (meta?.goal)
113
+ parts.push(meta.goal);
114
+ let instructions = parts.join('\n\n');
115
+ if (meta?.tools?.length) {
116
+ instructions +=
117
+ '\n\nTool usage rules:\n' +
118
+ '- Act immediately with the information given. Do NOT ask clarifying questions unless a required field is truly missing.\n' +
119
+ '- Only use fields defined in your tool schemas. Never mention or ask for fields that do not exist.\n' +
120
+ '- Never fill optional fields with placeholder or zero values. Omit them entirely unless the user provides a real value.\n' +
121
+ '- Never stuff unrelated information into the wrong field.\n' +
122
+ '- Keep responses concise.';
123
+ }
61
124
  if (meta?.agents?.length) {
62
125
  instructions +=
63
126
  '\n\nWhen calling a sub-agent, provide a short session name that describes the task. ' +
@@ -65,9 +128,6 @@ export async function buildInstructions(agentName, packageName) {
65
128
  'Use a new session name for a new independent task. ' +
66
129
  'When a request involves multiple actions for the same domain, combine them into a single sub-agent call rather than making separate calls.';
67
130
  }
68
- if (meta?.dynamicWorkflows && meta.tools?.length) {
69
- instructions += buildDynamicWorkflowInstructions(meta.tools, meta.dynamicWorkflows);
70
- }
71
131
  return instructions;
72
132
  }
73
133
  export function createScopedChannel(parent, agentName, session) {
@@ -119,6 +179,10 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
119
179
  const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName];
120
180
  if (!meta)
121
181
  return { tools, missingRpcs };
182
+ // Get session for permission filtering
183
+ const session = params.sessionService
184
+ ? await params.sessionService.get()
185
+ : null;
122
186
  const metaTools = meta.tools;
123
187
  const metaAgents = meta.agents;
124
188
  if (metaTools?.length) {
@@ -150,6 +214,14 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
150
214
  missingRpcs.push(toolName);
151
215
  continue;
152
216
  }
217
+ // Filter out tools the user doesn't have auth for
218
+ if (fnMeta.permissions?.length) {
219
+ if (!session)
220
+ continue;
221
+ const allowed = await checkAuthPermissions(fnMeta.permissions, session, singletonServices, resolvedPkg);
222
+ if (!allowed)
223
+ continue;
224
+ }
153
225
  const inputSchemaName = fnMeta?.inputSchemaName;
154
226
  let inputSchema = inputSchemaName
155
227
  ? schemas.get(inputSchemaName)
@@ -205,6 +277,14 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
205
277
  singletonServices.logger.warn(`Sub-agent '${subAgentName}' not found in agent registry`);
206
278
  continue;
207
279
  }
280
+ // Filter out sub-agents the user doesn't have auth for
281
+ if (subMeta.permissions?.length) {
282
+ if (!session)
283
+ continue;
284
+ const allowed = await checkAuthPermissions(subMeta.permissions, session, singletonServices);
285
+ if (!allowed)
286
+ continue;
287
+ }
208
288
  tools.push({
209
289
  name: subAgentName,
210
290
  description: subMeta.description,
@@ -293,10 +373,6 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
293
373
  });
294
374
  }
295
375
  }
296
- if (meta.dynamicWorkflows) {
297
- const workflowTools = buildWorkflowTools(agentName, packageName, meta.tools ?? [], meta.dynamicWorkflows, streamContext, params.sessionService);
298
- tools.push(...workflowTools);
299
- }
300
376
  const hasToolHooks = aiMiddlewares?.some((mw) => mw.beforeToolCall || mw.afterToolCall);
301
377
  if (hasToolHooks) {
302
378
  for (const tool of tools) {
@@ -324,6 +400,8 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
324
400
  }
325
401
  catch (err) {
326
402
  execError = err;
403
+ if (err?.payload?.error === 'missing_credential')
404
+ throw err;
327
405
  result = err instanceof Error ? err.message : String(err);
328
406
  }
329
407
  const durationMs = Date.now() - startTime;
@@ -357,15 +435,6 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
357
435
  if (!agentRunner) {
358
436
  throw new AIProviderNotConfiguredError();
359
437
  }
360
- if (agent.dynamicWorkflows && singletonServices.workflowService) {
361
- const persisted = await singletonServices.workflowService.getAIGeneratedWorkflows(resolvedName);
362
- const allMeta = pikkuState(null, 'workflows', 'meta');
363
- for (const wf of persisted) {
364
- if (!allMeta[wf.workflowName]) {
365
- allMeta[wf.workflowName] = wf.graph;
366
- }
367
- }
368
- }
369
438
  const { storage } = resolveMemoryServices(agent, singletonServices);
370
439
  const memoryConfig = agent.memory;
371
440
  const threadId = input.threadId;
@@ -1,10 +1,10 @@
1
1
  import { pikkuState } from '../../pikku-state.js';
2
- import { PikkuMissingMetaError } from '../../errors/errors.js';
3
2
  export const addAIAgent = (agentName, agent, packageName = null) => {
4
3
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta');
5
4
  const agentMeta = agentsMeta[agentName];
6
5
  if (!agentMeta) {
7
- throw new PikkuMissingMetaError(`Missing generated metadata for AI agent '${agentName}'`);
6
+ console.warn(`[pikku] Skipping AI agent '${agentName}' — metadata not found. Consider moving this wiring to its own file.`);
7
+ return;
8
8
  }
9
9
  const agents = pikkuState(packageName, 'agent', 'agents');
10
10
  if (agents.has(agentName)) {
@@ -4,6 +4,21 @@ import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
4
4
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
5
5
  import { resolveModelConfig } from './ai-agent-model-config.js';
6
6
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
7
+ function stripNulls(obj) {
8
+ if (obj === null)
9
+ return undefined;
10
+ if (Array.isArray(obj))
11
+ return obj.map(stripNulls);
12
+ if (typeof obj !== 'object')
13
+ return obj;
14
+ const result = {};
15
+ for (const [key, value] of Object.entries(obj)) {
16
+ if (value !== null) {
17
+ result[key] = stripNulls(value);
18
+ }
19
+ }
20
+ return result;
21
+ }
7
22
  import { randomUUID } from 'crypto';
8
23
  export async function runAIAgent(agentName, input, params, agentSessionMap) {
9
24
  const sessionMap = agentSessionMap ?? new Map();
@@ -287,9 +302,11 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
287
302
  if (!matchingTool) {
288
303
  throw new Error(`Tool "${pending.toolName}" not found in agent definition`);
289
304
  }
290
- const toolArgs = typeof pending.args === 'string'
305
+ const rawArgs = typeof pending.args === 'string'
291
306
  ? JSON.parse(pending.args)
292
307
  : pending.args;
308
+ // Strip null values recursively — LLMs send null for optional fields but Zod expects undefined
309
+ const toolArgs = stripNulls(rawArgs) ?? {};
293
310
  try {
294
311
  const toolResult = await matchingTool.execute(toolArgs);
295
312
  resultStr =
@@ -298,7 +315,12 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
298
315
  : JSON.stringify(toolResult);
299
316
  }
300
317
  catch (err) {
301
- resultStr = `Error: ${err instanceof Error ? err.message : String(err)}`;
318
+ if (err?.payload?.error === 'missing_credential') {
319
+ resultStr = JSON.stringify(err.payload);
320
+ }
321
+ else {
322
+ resultStr = `Error: ${err instanceof Error ? err.message : String(err)}`;
323
+ }
302
324
  }
303
325
  }
304
326
  else {
@@ -1,7 +1,8 @@
1
1
  import type { AIStreamChannel } from './ai-agent.types.js';
2
2
  import type { AIAgentRunnerParams, AIAgentStepResult } from '../../services/ai-agent-runner-service.js';
3
- import { ToolApprovalRequired, type RunAIAgentParams, type StreamAIAgentOptions } from './ai-agent-prepare.js';
3
+ import { ToolApprovalRequired, ToolCredentialRequired, type RunAIAgentParams, type StreamAIAgentOptions } from './ai-agent-prepare.js';
4
4
  export declare function checkForApprovals(stepResult: AIAgentStepResult, tools: AIAgentRunnerParams['tools'], runId: string): ToolApprovalRequired[];
5
+ export declare function checkForCredentialRequests(stepResult: AIAgentStepResult, runId: string): ToolCredentialRequired[];
5
6
  export declare function appendStepMessages(runnerParams: AIAgentRunnerParams, stepResult: AIAgentStepResult): void;
6
7
  export declare function streamAIAgent(agentName: string, input: {
7
8
  message: string;
@@ -3,7 +3,7 @@ import { AIProviderNotConfiguredError } from '../../errors/errors.js';
3
3
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { parseWorkingMemory, deepMergeWorkingMemory, resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
6
- import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, } from './ai-agent-prepare.js';
6
+ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
7
7
  import { resolveModelConfig } from './ai-agent-model-config.js';
8
8
  function createPersistingChannel(parent, storage, threadId) {
9
9
  let fullText = '';
@@ -197,6 +197,12 @@ async function runStreamStepLoop(params) {
197
197
  }
198
198
  return { outcome: 'approval', approvals: approvalsNeeded };
199
199
  }
200
+ const credentialRequests = checkForCredentialRequests(stepResult, runId);
201
+ if (credentialRequests.length > 0) {
202
+ // Append step messages so the tool result is preserved for resume
203
+ appendStepMessages(runnerParams, stepResult);
204
+ return { outcome: 'credential', credentialRequests };
205
+ }
200
206
  appendStepMessages(runnerParams, stepResult);
201
207
  }
202
208
  return { outcome: 'done' };
@@ -226,6 +232,19 @@ export function checkForApprovals(stepResult, tools, runId) {
226
232
  }
227
233
  return approvals;
228
234
  }
235
+ export function checkForCredentialRequests(stepResult, runId) {
236
+ const requests = [];
237
+ for (const tr of stepResult.toolResults) {
238
+ if (tr.result &&
239
+ typeof tr.result === 'object' &&
240
+ '__credentialRequired' in tr.result) {
241
+ const r = tr.result;
242
+ const tc = stepResult.toolCalls.find((t) => t.toolCallId === tr.toolCallId);
243
+ requests.push(new ToolCredentialRequired(tr.toolCallId, tc?.toolName ?? 'unknown', tc?.args ?? {}, r.credentialName, r.credentialType, r.connectUrl));
244
+ }
245
+ }
246
+ return requests;
247
+ }
229
248
  export function appendStepMessages(runnerParams, stepResult) {
230
249
  const assistantMsg = {
231
250
  id: randomUUID(),
@@ -293,6 +312,30 @@ function handleApprovals(approvals, runId, channel, aiRunState, persistingChanne
293
312
  channel.close();
294
313
  })();
295
314
  }
315
+ function handleCredentialRequests(requests, runId, channel, aiRunState, persistingChannel) {
316
+ return (async () => {
317
+ await persistingChannel.flush();
318
+ const pendingApprovals = requests.map((req) => ({
319
+ type: 'credential-request',
320
+ toolCallId: req.toolCallId,
321
+ toolName: req.toolName,
322
+ args: req.args,
323
+ credentialName: req.credentialName,
324
+ credentialType: req.credentialType,
325
+ connectUrl: req.connectUrl,
326
+ }));
327
+ await aiRunState.updateRun(runId, {
328
+ status: 'suspended',
329
+ suspendReason: 'credential',
330
+ pendingApprovals,
331
+ });
332
+ // Don't send credential-request SSE events — the tool result with
333
+ // __credentialRequired was already streamed. The frontend detects it
334
+ // from the tool result and shows Connect/Ignore buttons.
335
+ channel.send({ type: 'done' });
336
+ channel.close();
337
+ })();
338
+ }
296
339
  export async function streamAIAgent(agentName, input, channel, params, agentSessionMap, options) {
297
340
  const sessionMap = agentSessionMap ?? new Map();
298
341
  const normalizedInput = input;
@@ -419,6 +462,10 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
419
462
  await handleApprovals(loopResult.approvals, runId, channel, aiRunState, persistingChannel);
420
463
  return persistingChannel.fullText;
421
464
  }
465
+ if (loopResult.outcome === 'credential') {
466
+ await handleCredentialRequests(loopResult.credentialRequests, runId, channel, aiRunState, persistingChannel);
467
+ return persistingChannel.fullText;
468
+ }
422
469
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, runId);
423
470
  channel.send({ type: 'done' });
424
471
  channel.close();
@@ -487,7 +534,8 @@ export async function resumeAIAgent(input, channel, params, options) {
487
534
  toolResults: [
488
535
  {
489
536
  id: input.toolCallId,
490
- name: pending.type === 'tool-call'
537
+ name: pending.type === 'tool-call' ||
538
+ pending.type === 'credential-request'
491
539
  ? pending.toolName
492
540
  : pending.agentName,
493
541
  result: denialResult,
@@ -573,7 +621,12 @@ export async function resumeAIAgent(input, channel, params, options) {
573
621
  toolResult = await matchingTool.execute(toolArgs);
574
622
  }
575
623
  catch (execErr) {
576
- toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`;
624
+ if (execErr?.payload?.error === 'missing_credential') {
625
+ toolResult = execErr.payload;
626
+ }
627
+ else {
628
+ toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`;
629
+ }
577
630
  isError = true;
578
631
  }
579
632
  const resultStr = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
@@ -680,6 +733,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
680
733
  const streamContext = { channel, options };
681
734
  const resumeTools = (await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, aiMiddlewares)).tools;
682
735
  const resolved = resolveModelConfig(resolvedName, agent);
736
+ console.log('[DEBUG resume] resolvedName:', resolvedName, 'agent.model:', agent.model, 'resolved.model:', resolved.model);
683
737
  const maxSteps = resolved.maxSteps ?? 10;
684
738
  const runnerParams = {
685
739
  model: resolved.model,
@@ -708,6 +762,10 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
708
762
  await handleApprovals(loopResult.approvals, run.runId, channel, aiRunState, persistingChannel);
709
763
  return;
710
764
  }
765
+ if (loopResult.outcome === 'credential') {
766
+ await handleCredentialRequests(loopResult.credentialRequests, run.runId, channel, aiRunState, persistingChannel);
767
+ return;
768
+ }
711
769
  await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, run.threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, run.runId);
712
770
  channel.send({ type: 'done' });
713
771
  channel.close();
@@ -183,7 +183,9 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
183
183
  description: string;
184
184
  summary?: string;
185
185
  errors?: string[];
186
- instructions: string | string[];
186
+ role?: string;
187
+ personality?: string;
188
+ goal: string;
187
189
  model: string;
188
190
  temperature?: number;
189
191
  tools?: unknown[];
@@ -192,7 +194,6 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
192
194
  memory?: AIAgentMemoryConfig;
193
195
  maxSteps?: number;
194
196
  toolChoice?: 'auto' | 'required' | 'none';
195
- dynamicWorkflows?: 'read' | 'always' | 'ask';
196
197
  input?: unknown;
197
198
  output?: unknown;
198
199
  tags?: string[];
@@ -256,6 +257,17 @@ export type AIStreamEvent = {
256
257
  reason?: string;
257
258
  agent?: string;
258
259
  session?: string;
260
+ } | {
261
+ type: 'credential-request';
262
+ toolCallId: string;
263
+ toolName: string;
264
+ args: unknown;
265
+ credentialName: string;
266
+ credentialType: 'oauth2' | 'apikey';
267
+ connectUrl?: string;
268
+ runId: string;
269
+ agent?: string;
270
+ session?: string;
259
271
  } | {
260
272
  type: 'usage';
261
273
  tokens: {
@@ -307,6 +319,14 @@ export type PendingApproval = {
307
319
  agentRunId: string;
308
320
  displayToolName: string;
309
321
  displayArgs: unknown;
322
+ } | {
323
+ type: 'credential-request';
324
+ toolCallId: string;
325
+ toolName: string;
326
+ args: unknown;
327
+ credentialName: string;
328
+ credentialType: 'oauth2' | 'apikey';
329
+ connectUrl?: string;
310
330
  };
311
331
  export interface AgentRunState {
312
332
  runId: string;
@@ -315,7 +335,7 @@ export interface AgentRunState {
315
335
  resourceId: string;
316
336
  status: 'running' | 'suspended' | 'completed' | 'failed';
317
337
  errorMessage?: string;
318
- suspendReason?: 'approval' | 'rpc-missing';
338
+ suspendReason?: 'approval' | 'credential' | 'rpc-missing';
319
339
  missingRpcs?: string[];
320
340
  pendingApprovals?: PendingApproval[];
321
341
  usage: {
@@ -363,5 +383,6 @@ export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' |
363
383
  channelMiddleware?: MiddlewareMetadata[];
364
384
  aiMiddleware?: MiddlewareMetadata[];
365
385
  permissions?: PermissionMetadata[];
366
- dynamicWorkflows?: 'read' | 'always' | 'ask';
386
+ sourceFile?: string;
387
+ exportedName?: string;
367
388
  }>;
@@ -1,6 +1,6 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
2
2
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
3
3
  export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
- export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, } from './ai-agent-prepare.js';
4
+ export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
5
5
  export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
6
6
  export type { AIAgentInput, AIAgentInputAttachment, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
@@ -1,5 +1,5 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
2
2
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
3
3
  export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
- export { ToolApprovalRequired, } from './ai-agent-prepare.js';
4
+ export { ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
5
5
  export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
@@ -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 { pikkuState, getSingletonServices } from '../../pikku-state.js';
4
4
  import { coerceTopLevelDataFromSchema, validateSchema } from '../../schema.js';
@@ -12,9 +12,9 @@ export const wireChannel = (channel) => {
12
12
  const channelsMeta = pikkuState(null, 'channel', 'meta');
13
13
  const channelMeta = channelsMeta[channel.name];
14
14
  if (!channelMeta) {
15
- throw new PikkuMissingMetaError(`Missing generated metadata for channel '${channel.name}'`);
15
+ console.warn(`[pikku] Skipping channel '${channel.name}' — metadata not found. Consider moving this wiring to its own file.`);
16
+ return;
16
17
  }
17
- pikkuState(null, 'channel', 'channels').set(channel.name, channel);
18
18
  // Register onConnect function if provided
19
19
  if (channel.onConnect && channelMeta.connect) {
20
20
  addFunction(channelMeta.connect.pikkuFuncId, channel.onConnect);
@@ -1,4 +1,4 @@
1
- import { NotFoundError, PikkuMissingMetaError } from '../../errors/errors.js';
1
+ import { NotFoundError } from '../../errors/errors.js';
2
2
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
3
3
  import { pikkuState } from '../../pikku-state.js';
4
4
  import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
@@ -29,7 +29,8 @@ export const wireCLI = (cli) => {
29
29
  // Get the existing metadata that was generated during inspection
30
30
  const cliMeta = pikkuState(null, 'cli', 'meta') || {};
31
31
  if (!cliMeta.programs?.[cli.program]) {
32
- throw new PikkuMissingMetaError(`CLI metadata not found for program '${cli.program}'`);
32
+ console.warn(`[pikku] Skipping CLI program '${cli.program}' — metadata not found. Consider moving this wiring to its own file.`);
33
+ return;
33
34
  }
34
35
  // Get existing programs state and add this program
35
36
  const programs = pikkuState(null, 'cli', 'programs') || {};
@@ -110,7 +111,7 @@ function registerCLICommands(commands, path = [], inheritedOptions = {}, program
110
111
  programs[program].commandMiddleware[commandId] = command.middleware;
111
112
  }
112
113
  }
113
- addFunction(funcName, unwrapFunc(command));
114
+ addFunction(funcName, unwrapFunc(command), currentMeta?.packageName);
114
115
  // Register renderer if provided
115
116
  if (typeof command === 'object' && command.render) {
116
117
  if (programs[program]) {
@@ -1,3 +1,7 @@
1
+ /** Convert kebab-case to camelCase: "from-plan" → "fromPlan" */
2
+ function toCamelCase(str) {
3
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4
+ }
1
5
  /**
2
6
  * Parses raw CLI arguments into structured data for a specific program
3
7
  */
@@ -81,11 +85,11 @@ export function parseCLIArguments(args, programName, allMeta) {
81
85
  while (currentIndex < args.length) {
82
86
  const arg = args[currentIndex];
83
87
  if (arg.startsWith('--')) {
84
- // Long option
88
+ // Long option (--from-plan → fromPlan)
85
89
  const equalIndex = arg.indexOf('=');
86
90
  if (equalIndex > 0) {
87
91
  // --option=value format
88
- const key = arg.slice(2, equalIndex);
92
+ const key = toCamelCase(arg.slice(2, equalIndex));
89
93
  const optionDef = availableOptions[key];
90
94
  // Unknown options are allowed for forward compatibility
91
95
  const value = arg.slice(equalIndex + 1);
@@ -93,7 +97,7 @@ export function parseCLIArguments(args, programName, allMeta) {
93
97
  }
94
98
  else {
95
99
  // --option value format
96
- const key = arg.slice(2);
100
+ const key = toCamelCase(arg.slice(2));
97
101
  const optionDef = availableOptions[key];
98
102
  // Unknown options are allowed for forward compatibility
99
103
  if (optionDef && optionDef.array) {
@@ -136,4 +136,4 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
136
136
  * @param {RunHTTPWiringOptions} options - Options such as singleton services, session handling, and error configuration.
137
137
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
138
138
  */
139
- export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, }?: RunHTTPWiringOptions) => Promise<Out | void>;
139
+ export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, traceId: externalTraceId, }?: RunHTTPWiringOptions) => Promise<Out | void>;