@pikku/core 0.12.4 → 0.12.5
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 +11 -1
- package/dist/dev/hot-reload.d.ts +10 -0
- package/dist/dev/hot-reload.js +158 -0
- package/dist/middleware-runner.d.ts +1 -0
- package/dist/middleware-runner.js +5 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +5 -0
- package/dist/services/in-memory-workflow-service.js +7 -11
- package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
- package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
- package/dist/wirings/channel/channel-middleware-runner.js +5 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
- package/package.json +3 -2
- package/src/crypto-utils.test.ts +214 -0
- package/src/crypto-utils.ts +213 -0
- package/src/dev/hot-reload.test.ts +484 -0
- package/src/dev/hot-reload.ts +212 -0
- package/src/errors/error-handler.ts +62 -0
- package/src/errors/error.test.ts +195 -0
- package/src/errors/errors.ts +374 -0
- package/src/errors/index.ts +2 -0
- package/src/factory-functions.test.ts +109 -0
- package/src/function/function-runner.test.ts +536 -0
- package/src/function/function-runner.ts +365 -0
- package/src/function/functions.types.ts +304 -0
- package/src/function/index.ts +5 -0
- package/src/handle-error.test.ts +424 -0
- package/src/handle-error.ts +69 -0
- package/src/index.ts +118 -0
- package/src/internal.ts +6 -0
- package/src/middleware/auth-apikey.test.ts +363 -0
- package/src/middleware/auth-apikey.ts +47 -0
- package/src/middleware/auth-bearer.test.ts +450 -0
- package/src/middleware/auth-bearer.ts +72 -0
- package/src/middleware/auth-cookie.test.ts +528 -0
- package/src/middleware/auth-cookie.ts +80 -0
- package/src/middleware/cors.test.ts +424 -0
- package/src/middleware/cors.ts +104 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/remote-auth.test.ts +488 -0
- package/src/middleware/remote-auth.ts +68 -0
- package/src/middleware/timeout.ts +15 -0
- package/src/middleware-runner.test.ts +418 -0
- package/src/middleware-runner.ts +240 -0
- package/src/permissions.test.ts +434 -0
- package/src/permissions.ts +327 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.test.ts +224 -0
- package/src/pikku-state.ts +216 -0
- package/src/run-tests-script.test.ts +49 -0
- package/src/schema.test.ts +249 -0
- package/src/schema.ts +151 -0
- package/src/services/ai-agent-runner-service.ts +41 -0
- package/src/services/ai-run-state-service.ts +20 -0
- package/src/services/ai-storage-service.ts +39 -0
- package/src/services/content-service.ts +76 -0
- package/src/services/deployment-service.ts +22 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/gopass-secrets.ts +98 -0
- package/src/services/in-memory-ai-run-state-service.ts +73 -0
- package/src/services/in-memory-trigger-service.ts +64 -0
- package/src/services/in-memory-workflow-service.test.ts +351 -0
- package/src/services/in-memory-workflow-service.ts +502 -0
- package/src/services/index.ts +56 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +120 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/services/local-secrets.test.ts +80 -0
- package/src/services/local-secrets.ts +94 -0
- package/src/services/local-variables.test.ts +61 -0
- package/src/services/local-variables.ts +39 -0
- package/src/services/logger-console.test.ts +118 -0
- package/src/services/logger-console.ts +98 -0
- package/src/services/logger.ts +57 -0
- package/src/services/scheduler-service.ts +86 -0
- package/src/services/schema-service.ts +33 -0
- package/src/services/scoped-secret-service.test.ts +74 -0
- package/src/services/scoped-secret-service.ts +41 -0
- package/src/services/secret-service.ts +38 -0
- package/src/services/trigger-service.ts +17 -0
- package/src/services/typed-secret-service.test.ts +93 -0
- package/src/services/typed-secret-service.ts +70 -0
- package/src/services/typed-variables-service.test.ts +73 -0
- package/src/services/typed-variables-service.ts +81 -0
- package/src/services/user-session-service.test.ts +113 -0
- package/src/services/user-session-service.ts +86 -0
- package/src/services/variables-service.ts +11 -0
- package/src/services/workflow-service.ts +95 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +107 -0
- package/src/types/core.types.ts +478 -0
- package/src/types/state.types.ts +188 -0
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/utils.test.ts +350 -0
- package/src/utils.ts +129 -0
- package/src/version.test.ts +80 -0
- package/src/version.ts +25 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
- package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
- package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
- package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
- package/src/wirings/ai-agent/index.ts +37 -0
- package/src/wirings/channel/channel-common.ts +90 -0
- package/src/wirings/channel/channel-handler.ts +250 -0
- package/src/wirings/channel/channel-middleware-runner.ts +126 -0
- package/src/wirings/channel/channel-runner.ts +166 -0
- package/src/wirings/channel/channel-store.ts +29 -0
- package/src/wirings/channel/channel.types.ts +172 -0
- package/src/wirings/channel/define-channel-routes.ts +25 -0
- package/src/wirings/channel/eventhub-service.ts +34 -0
- package/src/wirings/channel/eventhub-store.ts +13 -0
- package/src/wirings/channel/index.ts +24 -0
- package/src/wirings/channel/local/index.ts +3 -0
- package/src/wirings/channel/local/local-channel-handler.ts +81 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
- package/src/wirings/channel/local/local-channel-runner.ts +210 -0
- package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
- package/src/wirings/channel/log-channels.ts +20 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
- package/src/wirings/channel/serverless/index.ts +5 -0
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
- package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
- package/src/wirings/cli/channel/index.ts +1 -0
- package/src/wirings/cli/cli-runner.test.ts +403 -0
- package/src/wirings/cli/cli-runner.ts +529 -0
- package/src/wirings/cli/cli.types.ts +358 -0
- package/src/wirings/cli/command-parser.test.ts +445 -0
- package/src/wirings/cli/command-parser.ts +504 -0
- package/src/wirings/cli/define-cli-commands.ts +24 -0
- package/src/wirings/cli/index.ts +19 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-routes.test.ts +322 -0
- package/src/wirings/http/http-routes.ts +197 -0
- package/src/wirings/http/http-runner.test.ts +144 -0
- package/src/wirings/http/http-runner.ts +588 -0
- package/src/wirings/http/http.types.ts +369 -0
- package/src/wirings/http/index.ts +28 -0
- package/src/wirings/http/log-http-routes.ts +22 -0
- package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
- package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
- package/src/wirings/http/routers/http-router.ts +13 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
- package/src/wirings/http/routers/path-to-regex.ts +126 -0
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/index.ts +27 -0
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
- package/src/wirings/mcp/mcp-runner.ts +323 -0
- package/src/wirings/mcp/mcp.types.ts +216 -0
- package/src/wirings/node/index.ts +2 -0
- package/src/wirings/node/node.types.ts +23 -0
- package/src/wirings/oauth2/index.ts +3 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
- package/src/wirings/oauth2/oauth2-client.ts +335 -0
- package/src/wirings/oauth2/oauth2.types.ts +69 -0
- package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
- package/src/wirings/queue/index.ts +31 -0
- package/src/wirings/queue/queue-runner.test.ts +710 -0
- package/src/wirings/queue/queue-runner.ts +192 -0
- package/src/wirings/queue/queue.types.ts +189 -0
- package/src/wirings/queue/register-queue-helper.ts +60 -0
- package/src/wirings/queue/validate-worker-config.test.ts +108 -0
- package/src/wirings/queue/validate-worker-config.ts +116 -0
- package/src/wirings/rpc/index.ts +4 -0
- package/src/wirings/rpc/rpc-runner.ts +460 -0
- package/src/wirings/rpc/rpc-types.ts +56 -0
- package/src/wirings/rpc/wire-addon.ts +21 -0
- package/src/wirings/scheduler/index.ts +11 -0
- package/src/wirings/scheduler/log-schedulers.ts +20 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
- package/src/wirings/scheduler/scheduler-runner.ts +133 -0
- package/src/wirings/scheduler/scheduler.types.ts +53 -0
- package/src/wirings/secret/index.ts +9 -0
- package/src/wirings/secret/secret.types.ts +32 -0
- package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
- package/src/wirings/secret/validate-secret-definitions.ts +82 -0
- package/src/wirings/trigger/index.ts +10 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
- package/src/wirings/trigger/trigger-runner.test.ts +79 -0
- package/src/wirings/trigger/trigger-runner.ts +135 -0
- package/src/wirings/trigger/trigger.types.ts +178 -0
- package/src/wirings/variable/index.ts +8 -0
- package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
- package/src/wirings/variable/validate-variable-definitions.ts +69 -0
- package/src/wirings/variable/variable.types.ts +22 -0
- package/src/wirings/workflow/dsl/index.ts +31 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
- package/src/wirings/workflow/graph/graph-node.ts +222 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
- package/src/wirings/workflow/graph/graph-runner.ts +816 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +19 -0
- package/src/wirings/workflow/graph/template.test.ts +49 -0
- package/src/wirings/workflow/graph/template.ts +42 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
- package/src/wirings/workflow/index.ts +82 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
- package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
- package/src/wirings/workflow/workflow-helpers.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +274 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import type { PikkuWire, CoreUserSession } from '../../types/core.types.js'
|
|
2
|
+
import type {
|
|
3
|
+
CoreAIAgent,
|
|
4
|
+
AIAgentInput,
|
|
5
|
+
AIAgentToolDef,
|
|
6
|
+
AIContentPart,
|
|
7
|
+
AIMessage,
|
|
8
|
+
AIStreamChannel,
|
|
9
|
+
AIStreamEvent,
|
|
10
|
+
PikkuAIMiddlewareHooks,
|
|
11
|
+
} from './ai-agent.types.js'
|
|
12
|
+
import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js'
|
|
13
|
+
import { PikkuError } from '../../errors/error-handler.js'
|
|
14
|
+
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
15
|
+
import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
|
|
16
|
+
import type { SessionService } from '../../services/user-session-service.js'
|
|
17
|
+
import { randomUUID } from 'crypto'
|
|
18
|
+
import { streamAIAgent } from './ai-agent-stream.js'
|
|
19
|
+
import { runAIAgent } from './ai-agent-runner.js'
|
|
20
|
+
import {
|
|
21
|
+
resolveNamespace,
|
|
22
|
+
ContextAwareRPCService,
|
|
23
|
+
} from '../../wirings/rpc/rpc-runner.js'
|
|
24
|
+
import {
|
|
25
|
+
buildDynamicWorkflowInstructions,
|
|
26
|
+
buildWorkflowTools,
|
|
27
|
+
} from './agent-dynamic-workflow.js'
|
|
28
|
+
import {
|
|
29
|
+
resolveMemoryServices,
|
|
30
|
+
loadContextMessages,
|
|
31
|
+
trimMessages,
|
|
32
|
+
} from './ai-agent-memory.js'
|
|
33
|
+
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
34
|
+
|
|
35
|
+
export type RunAIAgentParams = {
|
|
36
|
+
sessionService?: SessionService<CoreUserSession>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type StreamAIAgentOptions = {
|
|
40
|
+
requiresToolApproval?: 'all' | 'explicit' | false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ToolApprovalRequired extends PikkuError {
|
|
44
|
+
public readonly toolCallId: string
|
|
45
|
+
public readonly toolName: string
|
|
46
|
+
public readonly args: unknown
|
|
47
|
+
public reason?: string
|
|
48
|
+
public readonly displayToolName?: string
|
|
49
|
+
public readonly displayArgs?: unknown
|
|
50
|
+
public readonly agentRunId?: string
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
toolCallId: string,
|
|
54
|
+
toolName: string,
|
|
55
|
+
args: unknown,
|
|
56
|
+
reason?: string,
|
|
57
|
+
displayToolName?: string,
|
|
58
|
+
displayArgs?: unknown,
|
|
59
|
+
agentRunId?: string
|
|
60
|
+
) {
|
|
61
|
+
super(`Tool '${displayToolName ?? toolName}' requires approval`)
|
|
62
|
+
this.toolCallId = toolCallId
|
|
63
|
+
this.toolName = toolName
|
|
64
|
+
this.args = args
|
|
65
|
+
this.reason = reason
|
|
66
|
+
this.displayToolName = displayToolName
|
|
67
|
+
this.displayArgs = displayArgs
|
|
68
|
+
this.agentRunId = agentRunId
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type StreamContext = {
|
|
73
|
+
channel: AIStreamChannel
|
|
74
|
+
options?: StreamAIAgentOptions
|
|
75
|
+
delegateState?: { delegated: boolean }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const resolveAgent = (
|
|
79
|
+
agentName: string
|
|
80
|
+
): { agent: CoreAIAgent; packageName: string | null; resolvedName: string } => {
|
|
81
|
+
const mainAgent = pikkuState(null, 'agent', 'agents').get(agentName)
|
|
82
|
+
if (mainAgent) {
|
|
83
|
+
return { agent: mainAgent, packageName: null, resolvedName: agentName }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const colonIndex = agentName.indexOf(':')
|
|
87
|
+
if (colonIndex !== -1) {
|
|
88
|
+
const namespace = agentName.substring(0, colonIndex)
|
|
89
|
+
const localName = agentName.substring(colonIndex + 1)
|
|
90
|
+
const addons = pikkuState(null, 'addons', 'packages')
|
|
91
|
+
const pkgConfig = addons.get(namespace)
|
|
92
|
+
if (pkgConfig) {
|
|
93
|
+
const extAgent = pikkuState(pkgConfig.package, 'agent', 'agents').get(
|
|
94
|
+
localName
|
|
95
|
+
)
|
|
96
|
+
if (extAgent) {
|
|
97
|
+
return {
|
|
98
|
+
agent: extAgent,
|
|
99
|
+
packageName: pkgConfig.package,
|
|
100
|
+
resolvedName: localName,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`AI agent not found: ${agentName}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function buildInstructions(
|
|
110
|
+
agentName: string,
|
|
111
|
+
packageName: string | null
|
|
112
|
+
): Promise<string> {
|
|
113
|
+
const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
|
|
114
|
+
const rawInstructions = meta?.instructions ?? ''
|
|
115
|
+
let instructions = Array.isArray(rawInstructions)
|
|
116
|
+
? rawInstructions.join('\n')
|
|
117
|
+
: rawInstructions
|
|
118
|
+
|
|
119
|
+
if (meta?.agents?.length) {
|
|
120
|
+
instructions +=
|
|
121
|
+
'\n\nWhen calling a sub-agent, provide a short session name that describes the task. ' +
|
|
122
|
+
'Use the same session name to continue a previous conversation with that agent. ' +
|
|
123
|
+
'Use a new session name for a new independent task. ' +
|
|
124
|
+
'When a request involves multiple actions for the same domain, combine them into a single sub-agent call rather than making separate calls.'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (meta?.dynamicWorkflows && meta.tools?.length) {
|
|
128
|
+
instructions += buildDynamicWorkflowInstructions(
|
|
129
|
+
meta.tools,
|
|
130
|
+
meta.dynamicWorkflows
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return instructions
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type ScopedChannel = AIStreamChannel & {
|
|
138
|
+
approvals: Array<{
|
|
139
|
+
toolCallId: string
|
|
140
|
+
toolName: string
|
|
141
|
+
args: unknown
|
|
142
|
+
runId: string
|
|
143
|
+
}>
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createScopedChannel(
|
|
147
|
+
parent: AIStreamChannel,
|
|
148
|
+
agentName: string,
|
|
149
|
+
session: string
|
|
150
|
+
): ScopedChannel {
|
|
151
|
+
const capturedApprovals: ScopedChannel['approvals'] = []
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
channelId: `${parent.channelId}:${agentName}:${session}`,
|
|
155
|
+
openingData: parent.openingData,
|
|
156
|
+
get state() {
|
|
157
|
+
return parent.state
|
|
158
|
+
},
|
|
159
|
+
get approvals() {
|
|
160
|
+
return capturedApprovals
|
|
161
|
+
},
|
|
162
|
+
close: () => {},
|
|
163
|
+
sendBinary: (data) => parent.sendBinary(data),
|
|
164
|
+
send: (event: AIStreamEvent) => {
|
|
165
|
+
if (event.type === 'done') return
|
|
166
|
+
if (event.type === 'approval-request') {
|
|
167
|
+
capturedApprovals.push({
|
|
168
|
+
toolCallId: event.toolCallId,
|
|
169
|
+
toolName: event.toolName,
|
|
170
|
+
args: event.args,
|
|
171
|
+
runId: (event as any).runId,
|
|
172
|
+
})
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
event.type === 'step-start' ||
|
|
177
|
+
event.type === 'text-delta' ||
|
|
178
|
+
event.type === 'reasoning-delta' ||
|
|
179
|
+
event.type === 'tool-call' ||
|
|
180
|
+
event.type === 'tool-result' ||
|
|
181
|
+
event.type === 'usage' ||
|
|
182
|
+
event.type === 'error' ||
|
|
183
|
+
event.type === 'workflow-created'
|
|
184
|
+
) {
|
|
185
|
+
parent.send({ ...event, agent: agentName, session } as AIStreamEvent)
|
|
186
|
+
} else {
|
|
187
|
+
parent.send(event)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function buildToolDefs(
|
|
194
|
+
params: RunAIAgentParams,
|
|
195
|
+
agentSessionMap: Map<string, string>,
|
|
196
|
+
resourceId: string,
|
|
197
|
+
agentName: string,
|
|
198
|
+
packageName: string | null,
|
|
199
|
+
streamContext?: StreamContext,
|
|
200
|
+
aiMiddlewares?: PikkuAIMiddlewareHooks[],
|
|
201
|
+
agentMode?: 'delegate' | 'supervise'
|
|
202
|
+
): Promise<{ tools: AIAgentToolDef[]; missingRpcs: string[] }> {
|
|
203
|
+
const singletonServices = getSingletonServices()
|
|
204
|
+
const tools: AIAgentToolDef[] = []
|
|
205
|
+
const missingRpcs: string[] = []
|
|
206
|
+
const approvalPolicy =
|
|
207
|
+
streamContext?.options?.requiresToolApproval ?? 'explicit'
|
|
208
|
+
|
|
209
|
+
const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName]
|
|
210
|
+
if (!meta) return { tools, missingRpcs }
|
|
211
|
+
|
|
212
|
+
const metaTools = meta.tools
|
|
213
|
+
const metaAgents = meta.agents
|
|
214
|
+
|
|
215
|
+
if (metaTools?.length) {
|
|
216
|
+
for (const toolName of metaTools) {
|
|
217
|
+
let fnMeta: any
|
|
218
|
+
let resolvedPkg: string | null = null
|
|
219
|
+
let schemas: Map<string, any>
|
|
220
|
+
|
|
221
|
+
const resolved = toolName.includes(':')
|
|
222
|
+
? resolveNamespace(toolName)
|
|
223
|
+
: null
|
|
224
|
+
|
|
225
|
+
let pikkuFuncId: string | undefined
|
|
226
|
+
|
|
227
|
+
if (resolved) {
|
|
228
|
+
resolvedPkg = resolved.package
|
|
229
|
+
pikkuFuncId = resolved.function
|
|
230
|
+
fnMeta = pikkuState(resolvedPkg, 'function', 'meta')[pikkuFuncId]
|
|
231
|
+
schemas = pikkuState(resolvedPkg, 'misc', 'schemas')
|
|
232
|
+
} else {
|
|
233
|
+
const rpcMeta = pikkuState(null, 'rpc', 'meta')
|
|
234
|
+
pikkuFuncId = rpcMeta[toolName]
|
|
235
|
+
if (!pikkuFuncId) {
|
|
236
|
+
missingRpcs.push(toolName)
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
|
|
240
|
+
schemas = pikkuState(null, 'misc', 'schemas')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!fnMeta) {
|
|
244
|
+
missingRpcs.push(toolName)
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const inputSchemaName = fnMeta?.inputSchemaName
|
|
249
|
+
let inputSchema = inputSchemaName
|
|
250
|
+
? schemas.get(inputSchemaName)
|
|
251
|
+
: undefined
|
|
252
|
+
if (
|
|
253
|
+
!inputSchema ||
|
|
254
|
+
(typeof inputSchema === 'object' &&
|
|
255
|
+
inputSchema.type === 'object' &&
|
|
256
|
+
!inputSchema.properties)
|
|
257
|
+
) {
|
|
258
|
+
inputSchema = { type: 'object', properties: {} }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const needsApproval =
|
|
262
|
+
approvalPolicy === 'all' ||
|
|
263
|
+
(approvalPolicy === 'explicit' && fnMeta?.approvalRequired)
|
|
264
|
+
|
|
265
|
+
// Build approvalDescriptionFn if the function has an approvalDescription configured
|
|
266
|
+
let approvalDescriptionFn:
|
|
267
|
+
| ((input: unknown) => Promise<string>)
|
|
268
|
+
| undefined
|
|
269
|
+
if (needsApproval && pikkuFuncId) {
|
|
270
|
+
const funcConfig = pikkuState(resolvedPkg, 'function', 'functions').get(
|
|
271
|
+
pikkuFuncId
|
|
272
|
+
)
|
|
273
|
+
if (funcConfig?.approvalDescription) {
|
|
274
|
+
const descFn = funcConfig.approvalDescription
|
|
275
|
+
const capturedPkg = resolvedPkg
|
|
276
|
+
approvalDescriptionFn = async (input: unknown) => {
|
|
277
|
+
let services = singletonServices
|
|
278
|
+
if (capturedPkg) {
|
|
279
|
+
const pkgServices = pikkuState(
|
|
280
|
+
capturedPkg,
|
|
281
|
+
'package',
|
|
282
|
+
'singletonServices'
|
|
283
|
+
)
|
|
284
|
+
if (pkgServices) {
|
|
285
|
+
services = pkgServices
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return descFn(services, input)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
tools.push({
|
|
294
|
+
name: toolName.replaceAll(':', '__'),
|
|
295
|
+
description: fnMeta?.description || fnMeta?.title || toolName,
|
|
296
|
+
inputSchema,
|
|
297
|
+
needsApproval: needsApproval || undefined,
|
|
298
|
+
approvalDescriptionFn,
|
|
299
|
+
execute: async (toolInput: unknown) => {
|
|
300
|
+
const wire: PikkuWire = params.sessionService
|
|
301
|
+
? { ...createMiddlewareSessionWireProps(params.sessionService) }
|
|
302
|
+
: {}
|
|
303
|
+
const rpcService = new ContextAwareRPCService(
|
|
304
|
+
singletonServices,
|
|
305
|
+
wire,
|
|
306
|
+
{ sessionService: params.sessionService }
|
|
307
|
+
)
|
|
308
|
+
return rpcService.rpc(toolName, toolInput)
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (metaAgents?.length) {
|
|
315
|
+
const allAgentsMeta = pikkuState(null, 'agent', 'agentsMeta')
|
|
316
|
+
|
|
317
|
+
for (const subAgentName of metaAgents) {
|
|
318
|
+
const subMeta = allAgentsMeta[subAgentName]
|
|
319
|
+
if (!subMeta) {
|
|
320
|
+
singletonServices.logger.warn(
|
|
321
|
+
`Sub-agent '${subAgentName}' not found in agent registry`
|
|
322
|
+
)
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
tools.push({
|
|
327
|
+
name: subAgentName,
|
|
328
|
+
description: subMeta.description,
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
message: { type: 'string' },
|
|
333
|
+
session: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description: 'Short session label for thread continuity',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
required: ['message', 'session'],
|
|
339
|
+
},
|
|
340
|
+
execute: async (toolInput: unknown) => {
|
|
341
|
+
const { message, session } = toolInput as {
|
|
342
|
+
message: string
|
|
343
|
+
session: string
|
|
344
|
+
}
|
|
345
|
+
const sessionKey = `${subAgentName}::${session}`
|
|
346
|
+
let threadId = agentSessionMap.get(sessionKey)
|
|
347
|
+
if (!threadId) {
|
|
348
|
+
threadId = randomUUID()
|
|
349
|
+
agentSessionMap.set(sessionKey, threadId)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (streamContext) {
|
|
353
|
+
const isDelegate = agentMode !== 'supervise'
|
|
354
|
+
if (isDelegate && streamContext.delegateState) {
|
|
355
|
+
streamContext.delegateState.delegated = true
|
|
356
|
+
}
|
|
357
|
+
const { channel } = streamContext
|
|
358
|
+
channel.send({
|
|
359
|
+
type: 'agent-call',
|
|
360
|
+
agentName: subAgentName,
|
|
361
|
+
session,
|
|
362
|
+
input: message,
|
|
363
|
+
})
|
|
364
|
+
const subChannel = createScopedChannel(
|
|
365
|
+
channel,
|
|
366
|
+
subAgentName,
|
|
367
|
+
session
|
|
368
|
+
)
|
|
369
|
+
// In supervise mode, suppress sub-agent text from reaching the client.
|
|
370
|
+
// Approvals still flow through normally.
|
|
371
|
+
const effectiveChannel = isDelegate
|
|
372
|
+
? subChannel
|
|
373
|
+
: {
|
|
374
|
+
...subChannel,
|
|
375
|
+
send: (event: AIStreamEvent) => {
|
|
376
|
+
if (
|
|
377
|
+
event.type === 'text-delta' ||
|
|
378
|
+
event.type === 'reasoning-delta'
|
|
379
|
+
)
|
|
380
|
+
return
|
|
381
|
+
subChannel.send(event)
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
const resultText = await streamAIAgent(
|
|
385
|
+
subAgentName,
|
|
386
|
+
{ message, threadId, resourceId },
|
|
387
|
+
effectiveChannel,
|
|
388
|
+
params,
|
|
389
|
+
agentSessionMap,
|
|
390
|
+
streamContext.options
|
|
391
|
+
)
|
|
392
|
+
if (subChannel.approvals.length > 0) {
|
|
393
|
+
return {
|
|
394
|
+
__approvalRequired: true,
|
|
395
|
+
toolName: subAgentName,
|
|
396
|
+
args: toolInput,
|
|
397
|
+
agentRunId: subChannel.approvals[0].runId,
|
|
398
|
+
subApprovals: subChannel.approvals,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
channel.send({
|
|
402
|
+
type: 'agent-result',
|
|
403
|
+
agentName: subAgentName,
|
|
404
|
+
session,
|
|
405
|
+
result: resultText,
|
|
406
|
+
})
|
|
407
|
+
return resultText
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// No stream context: sub-agent runs non-streaming
|
|
411
|
+
const result = await runAIAgent(
|
|
412
|
+
subAgentName,
|
|
413
|
+
{ message, threadId, resourceId },
|
|
414
|
+
params,
|
|
415
|
+
agentSessionMap
|
|
416
|
+
)
|
|
417
|
+
if (
|
|
418
|
+
result.status === 'suspended' &&
|
|
419
|
+
result.pendingApprovals?.length
|
|
420
|
+
) {
|
|
421
|
+
return {
|
|
422
|
+
__approvalRequired: true,
|
|
423
|
+
toolName: subAgentName,
|
|
424
|
+
args: toolInput,
|
|
425
|
+
agentRunId: result.runId,
|
|
426
|
+
subApprovals: result.pendingApprovals.map((a) => ({
|
|
427
|
+
toolCallId: a.toolCallId,
|
|
428
|
+
toolName: a.toolName,
|
|
429
|
+
args: a.args,
|
|
430
|
+
runId: a.runId,
|
|
431
|
+
})),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return result.object ?? result.text
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (meta.dynamicWorkflows) {
|
|
441
|
+
const workflowTools = buildWorkflowTools(
|
|
442
|
+
agentName,
|
|
443
|
+
packageName,
|
|
444
|
+
meta.tools ?? [],
|
|
445
|
+
meta.dynamicWorkflows,
|
|
446
|
+
streamContext,
|
|
447
|
+
params.sessionService
|
|
448
|
+
)
|
|
449
|
+
tools.push(...workflowTools)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const hasToolHooks = aiMiddlewares?.some(
|
|
453
|
+
(mw) => mw.beforeToolCall || mw.afterToolCall
|
|
454
|
+
)
|
|
455
|
+
if (hasToolHooks) {
|
|
456
|
+
for (const tool of tools) {
|
|
457
|
+
const originalExecute = tool.execute
|
|
458
|
+
tool.execute = async (toolInput: unknown) => {
|
|
459
|
+
const toolCallId = randomUUID()
|
|
460
|
+
let args = (toolInput ?? {}) as Record<string, unknown>
|
|
461
|
+
|
|
462
|
+
for (const mw of aiMiddlewares!) {
|
|
463
|
+
if (mw.beforeToolCall) {
|
|
464
|
+
const beforeResult = await mw.beforeToolCall(singletonServices, {
|
|
465
|
+
toolName: tool.name,
|
|
466
|
+
toolCallId,
|
|
467
|
+
args,
|
|
468
|
+
})
|
|
469
|
+
if (beforeResult && 'args' in beforeResult) {
|
|
470
|
+
args = beforeResult.args
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const startTime = Date.now()
|
|
476
|
+
let result: unknown
|
|
477
|
+
let execError: unknown
|
|
478
|
+
try {
|
|
479
|
+
result = await originalExecute(args)
|
|
480
|
+
} catch (err) {
|
|
481
|
+
execError = err
|
|
482
|
+
result = err instanceof Error ? err.message : String(err)
|
|
483
|
+
}
|
|
484
|
+
const durationMs = Date.now() - startTime
|
|
485
|
+
|
|
486
|
+
for (let i = aiMiddlewares!.length - 1; i >= 0; i--) {
|
|
487
|
+
const mw = aiMiddlewares![i]
|
|
488
|
+
if (mw.afterToolCall) {
|
|
489
|
+
const afterResult = await mw.afterToolCall(singletonServices, {
|
|
490
|
+
toolName: tool.name,
|
|
491
|
+
toolCallId,
|
|
492
|
+
args,
|
|
493
|
+
result,
|
|
494
|
+
durationMs,
|
|
495
|
+
})
|
|
496
|
+
if (afterResult && 'result' in afterResult) {
|
|
497
|
+
result = afterResult.result
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (execError) throw execError
|
|
503
|
+
return result
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return { tools, missingRpcs }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export async function prepareAgentRun(
|
|
512
|
+
agentName: string,
|
|
513
|
+
input: AIAgentInput,
|
|
514
|
+
params: RunAIAgentParams,
|
|
515
|
+
agentSessionMap: Map<string, string>,
|
|
516
|
+
streamContext?: StreamContext
|
|
517
|
+
) {
|
|
518
|
+
const singletonServices = getSingletonServices()
|
|
519
|
+
const { agent, packageName, resolvedName } = resolveAgent(agentName)
|
|
520
|
+
|
|
521
|
+
const agentRunner = singletonServices.aiAgentRunner
|
|
522
|
+
if (!agentRunner) {
|
|
523
|
+
throw new Error('AIAgentRunnerService not available in singletonServices')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (agent.dynamicWorkflows && singletonServices.workflowService) {
|
|
527
|
+
const persisted =
|
|
528
|
+
await singletonServices.workflowService.getAIGeneratedWorkflows(
|
|
529
|
+
resolvedName
|
|
530
|
+
)
|
|
531
|
+
const allMeta = pikkuState(null, 'workflows', 'meta')
|
|
532
|
+
for (const wf of persisted) {
|
|
533
|
+
if (!allMeta[wf.workflowName]) {
|
|
534
|
+
allMeta[wf.workflowName] = wf.graph
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const { storage } = resolveMemoryServices(agent, singletonServices)
|
|
540
|
+
const memoryConfig = agent.memory
|
|
541
|
+
const threadId = input.threadId
|
|
542
|
+
|
|
543
|
+
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
|
|
544
|
+
const meta = agentsMeta[resolvedName]
|
|
545
|
+
const outputSchemaName = meta?.outputSchema
|
|
546
|
+
const outputSchema = outputSchemaName
|
|
547
|
+
? pikkuState(packageName, 'misc', 'schemas').get(outputSchemaName)
|
|
548
|
+
: undefined
|
|
549
|
+
|
|
550
|
+
const workingMemorySchemaName = meta?.workingMemorySchema ?? null
|
|
551
|
+
const workingMemoryJsonSchema = workingMemorySchemaName
|
|
552
|
+
? pikkuState(packageName, 'misc', 'schemas').get(workingMemorySchemaName)
|
|
553
|
+
: undefined
|
|
554
|
+
|
|
555
|
+
if (storage) {
|
|
556
|
+
try {
|
|
557
|
+
await storage.getThread(threadId)
|
|
558
|
+
} catch {
|
|
559
|
+
await storage.createThread(input.resourceId, { threadId })
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let messages: AIMessage[] = []
|
|
564
|
+
if (storage) {
|
|
565
|
+
messages = await storage.getMessages(threadId, {
|
|
566
|
+
lastN: memoryConfig?.lastMessages ?? 20,
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const contextMessages = await loadContextMessages(
|
|
571
|
+
memoryConfig,
|
|
572
|
+
storage,
|
|
573
|
+
input,
|
|
574
|
+
workingMemoryJsonSchema
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
const userContent: AIMessage['content'] = input.attachments?.length
|
|
578
|
+
? [
|
|
579
|
+
{ type: 'text' as const, text: input.message },
|
|
580
|
+
...input.attachments.map(
|
|
581
|
+
(a) =>
|
|
582
|
+
({
|
|
583
|
+
type: a.type,
|
|
584
|
+
data: a.data,
|
|
585
|
+
url: a.url,
|
|
586
|
+
mediaType: a.mediaType,
|
|
587
|
+
...(a.filename ? { filename: a.filename } : {}),
|
|
588
|
+
}) as AIContentPart
|
|
589
|
+
),
|
|
590
|
+
]
|
|
591
|
+
: input.message
|
|
592
|
+
|
|
593
|
+
const userMessage: AIMessage = {
|
|
594
|
+
id: randomUUID(),
|
|
595
|
+
role: 'user',
|
|
596
|
+
content: userContent,
|
|
597
|
+
createdAt: new Date(),
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const allMessages = [...contextMessages, ...messages, userMessage]
|
|
601
|
+
const trimmedMessages = trimMessages(allMessages)
|
|
602
|
+
|
|
603
|
+
const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
|
|
604
|
+
|
|
605
|
+
const { tools, missingRpcs } = await buildToolDefs(
|
|
606
|
+
params,
|
|
607
|
+
agentSessionMap,
|
|
608
|
+
input.resourceId,
|
|
609
|
+
resolvedName,
|
|
610
|
+
packageName,
|
|
611
|
+
streamContext,
|
|
612
|
+
aiMiddlewares,
|
|
613
|
+
agent.agentMode
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
const instructions = await buildInstructions(resolvedName, packageName)
|
|
617
|
+
|
|
618
|
+
const resolved = resolveModelConfig(resolvedName, agent)
|
|
619
|
+
|
|
620
|
+
// Per-request overrides
|
|
621
|
+
if (input.model) {
|
|
622
|
+
resolved.model = resolveModelConfig(resolvedName, {
|
|
623
|
+
...agent,
|
|
624
|
+
model: input.model,
|
|
625
|
+
}).model
|
|
626
|
+
}
|
|
627
|
+
if (input.temperature !== undefined) {
|
|
628
|
+
resolved.temperature = input.temperature
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const maxSteps = resolved.maxSteps ?? 10
|
|
632
|
+
|
|
633
|
+
const runnerParams: AIAgentRunnerParams = {
|
|
634
|
+
model: resolved.model,
|
|
635
|
+
temperature: resolved.temperature,
|
|
636
|
+
instructions,
|
|
637
|
+
messages: trimmedMessages,
|
|
638
|
+
tools,
|
|
639
|
+
maxSteps: 1,
|
|
640
|
+
toolChoice: agent.toolChoice ?? 'auto',
|
|
641
|
+
outputSchema,
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
agent,
|
|
646
|
+
packageName,
|
|
647
|
+
resolvedName,
|
|
648
|
+
agentRunner,
|
|
649
|
+
storage,
|
|
650
|
+
memoryConfig,
|
|
651
|
+
threadId,
|
|
652
|
+
userMessage,
|
|
653
|
+
runnerParams,
|
|
654
|
+
maxSteps,
|
|
655
|
+
missingRpcs,
|
|
656
|
+
workingMemoryJsonSchema,
|
|
657
|
+
workingMemorySchemaName,
|
|
658
|
+
}
|
|
659
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { approveAIAgent } from './ai-agent-registry.js'
|
|
5
|
+
|
|
6
|
+
describe('approveAIAgent', () => {
|
|
7
|
+
test('rejects approval when run agent does not match expected agent', async () => {
|
|
8
|
+
const aiRunState = {
|
|
9
|
+
getRun: async () => ({
|
|
10
|
+
runId: 'run-1',
|
|
11
|
+
agentName: 'internal-agent',
|
|
12
|
+
threadId: 'thread-1',
|
|
13
|
+
resourceId: 'resource-1',
|
|
14
|
+
status: 'suspended',
|
|
15
|
+
pendingApprovals: [],
|
|
16
|
+
usage: { inputTokens: 0, outputTokens: 0, model: 'test-model' },
|
|
17
|
+
createdAt: new Date(),
|
|
18
|
+
updatedAt: new Date(),
|
|
19
|
+
}),
|
|
20
|
+
updateRun: async () => {},
|
|
21
|
+
} as any
|
|
22
|
+
|
|
23
|
+
await assert.rejects(
|
|
24
|
+
() =>
|
|
25
|
+
approveAIAgent(
|
|
26
|
+
aiRunState,
|
|
27
|
+
'run-1',
|
|
28
|
+
[{ toolCallId: 'call-1', approved: true }],
|
|
29
|
+
'public-agent'
|
|
30
|
+
),
|
|
31
|
+
{
|
|
32
|
+
message:
|
|
33
|
+
"Run run-1 belongs to agent 'internal-agent', not 'public-agent'",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
})
|