@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.
- package/CHANGELOG.md +46 -0
- package/dist/errors/errors.d.ts +18 -0
- package/dist/errors/errors.js +33 -0
- package/dist/function/function-runner.d.ts +1 -1
- package/dist/function/function-runner.js +10 -5
- package/dist/function/functions.types.js +1 -0
- package/dist/function/index.d.ts +2 -1
- package/dist/function/index.js +1 -0
- package/dist/handle-error.d.ts +2 -2
- package/dist/handle-error.js +8 -8
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/remote-auth.js +5 -2
- package/dist/permissions.d.ts +13 -1
- package/dist/permissions.js +51 -0
- package/dist/pikku-state.d.ts +0 -1
- package/dist/pikku-state.js +2 -4
- package/dist/remote.d.ts +10 -0
- package/dist/remote.js +32 -0
- package/dist/schema.js +2 -0
- package/dist/services/credential-service.d.ts +11 -0
- package/dist/services/credential-wire-service.d.ts +14 -3
- package/dist/services/credential-wire-service.js +49 -6
- package/dist/services/deployment-service.d.ts +12 -1
- package/dist/services/in-memory-queue-service.d.ts +7 -0
- package/dist/services/in-memory-queue-service.js +28 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +2 -1
- package/dist/services/local-credential-service.d.ts +2 -0
- package/dist/services/local-credential-service.js +20 -0
- package/dist/services/logger-console.d.ts +26 -40
- package/dist/services/logger-console.js +102 -46
- package/dist/services/logger.d.ts +5 -0
- package/dist/services/meta-service.d.ts +175 -0
- package/dist/services/meta-service.js +310 -0
- package/dist/services/pikku-user-id.d.ts +3 -0
- package/dist/services/pikku-user-id.js +16 -0
- package/dist/services/typed-credential-service.d.ts +2 -0
- package/dist/services/typed-credential-service.js +6 -0
- package/dist/services/workflow-service.d.ts +6 -1
- package/dist/testing/service-tests.js +0 -8
- package/dist/types/core.types.d.ts +14 -2
- package/dist/types/state.types.d.ts +9 -2
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +21 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +90 -21
- package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
- package/dist/wirings/ai-agent/ai-agent-runner.js +24 -2
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +61 -3
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +25 -4
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/channel/channel-runner.js +3 -3
- package/dist/wirings/cli/cli-runner.js +4 -3
- package/dist/wirings/cli/command-parser.js +7 -3
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +23 -15
- package/dist/wirings/http/http.types.d.ts +2 -0
- package/dist/wirings/mcp/mcp-runner.js +5 -3
- package/dist/wirings/queue/queue-runner.d.ts +3 -1
- package/dist/wirings/queue/queue-runner.js +8 -3
- package/dist/wirings/queue/queue.types.d.ts +6 -0
- package/dist/wirings/rpc/rpc-runner.js +57 -90
- package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
- package/dist/wirings/scheduler/scheduler-runner.js +9 -5
- package/dist/wirings/trigger/trigger-runner.js +4 -2
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +2 -0
- package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +18 -4
- package/dist/wirings/workflow/index.d.ts +4 -2
- package/dist/wirings/workflow/index.js +4 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
- package/dist/wirings/workflow/pikku-workflow-service.js +218 -24
- package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
- package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
- package/dist/wirings/workflow/workflow.types.d.ts +19 -1
- package/package.json +4 -1
- package/src/errors/errors.ts +44 -0
- package/src/function/function-runner.ts +24 -13
- package/src/function/functions.types.ts +1 -0
- package/src/function/index.ts +10 -0
- package/src/handle-error.test.ts +2 -2
- package/src/handle-error.ts +8 -8
- package/src/index.ts +1 -2
- package/src/middleware/index.ts +2 -0
- package/src/middleware/remote-auth.test.ts +4 -4
- package/src/middleware/remote-auth.ts +7 -2
- package/src/permissions.ts +70 -0
- package/src/pikku-state.test.ts +0 -26
- package/src/pikku-state.ts +2 -5
- package/src/remote.ts +46 -0
- package/src/schema.ts +1 -0
- package/src/services/credential-service.ts +13 -0
- package/src/services/credential-wire-service.test.ts +174 -0
- package/src/services/credential-wire-service.ts +52 -8
- package/src/services/deployment-service.ts +17 -1
- package/src/services/in-memory-queue-service.ts +46 -0
- package/src/services/index.ts +21 -1
- package/src/services/local-credential-service.ts +22 -0
- package/src/services/logger-console.ts +127 -47
- package/src/services/logger.ts +6 -0
- package/src/services/meta-service.ts +523 -0
- package/src/services/pikku-user-id.test.ts +62 -0
- package/src/services/pikku-user-id.ts +17 -0
- package/src/services/typed-credential-service.ts +8 -0
- package/src/services/workflow-service.ts +8 -0
- package/src/testing/service-tests.ts +0 -10
- package/src/types/core.types.ts +16 -2
- package/src/types/state.types.ts +7 -2
- package/src/wirings/ai-agent/ai-agent-prepare.ts +122 -41
- package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
- package/src/wirings/ai-agent/ai-agent-runner.ts +22 -3
- package/src/wirings/ai-agent/ai-agent-stream.ts +115 -3
- package/src/wirings/ai-agent/ai-agent.types.ts +27 -4
- package/src/wirings/ai-agent/index.ts +1 -0
- package/src/wirings/channel/channel-handler.ts +0 -1
- package/src/wirings/channel/channel-runner.ts +4 -5
- package/src/wirings/cli/cli-runner.test.ts +8 -7
- package/src/wirings/cli/cli-runner.ts +5 -4
- package/src/wirings/cli/command-parser.ts +8 -3
- package/src/wirings/http/http-runner.ts +27 -18
- package/src/wirings/http/http.types.ts +2 -0
- package/src/wirings/mcp/mcp-runner.ts +7 -10
- package/src/wirings/queue/queue-runner.test.ts +5 -11
- package/src/wirings/queue/queue-runner.ts +12 -4
- package/src/wirings/queue/queue.types.ts +6 -0
- package/src/wirings/rpc/rpc-runner.ts +91 -118
- package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
- package/src/wirings/scheduler/scheduler-runner.ts +13 -5
- package/src/wirings/trigger/trigger-runner.test.ts +10 -11
- package/src/wirings/trigger/trigger-runner.ts +6 -4
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +2 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
- package/src/wirings/workflow/graph/graph-runner.ts +19 -4
- package/src/wirings/workflow/index.ts +17 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +286 -24
- package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
- package/src/wirings/workflow/workflow.types.ts +18 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
- package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
- package/dist/wirings/workflow/workflow-helpers.js +0 -38
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
- package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
- package/src/wirings/workflow/workflow-helpers.ts +0 -79
package/src/types/core.types.ts
CHANGED
|
@@ -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
|
|
279
|
-
|
|
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
|
/**
|
package/src/types/state.types.ts
CHANGED
|
@@ -182,7 +182,12 @@ export interface PikkuPackageState {
|
|
|
182
182
|
} | null
|
|
183
183
|
/** Cached singleton services for this package */
|
|
184
184
|
singletonServices: CoreSingletonServices | null
|
|
185
|
-
/**
|
|
186
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
15
|
-
`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
+
sourceFile?: string
|
|
403
|
+
exportedName?: string
|
|
381
404
|
}
|
|
382
405
|
>
|
|
@@ -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
|
|
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
|
-
|
|
40
|
-
`
|
|
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)
|