@pikku/core 0.12.4 → 0.12.6
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 +21 -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/content-service.d.ts +2 -0
- package/dist/services/in-memory-workflow-service.d.ts +1 -0
- package/dist/services/in-memory-workflow-service.js +16 -11
- package/dist/services/workflow-service.d.ts +1 -0
- 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/http/http-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +6 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +51 -16
- package/dist/wirings/workflow/workflow.types.d.ts +2 -0
- package/package.json +5 -3
- 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 +81 -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 +512 -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 +96 -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 +1229 -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 +276 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIStreamChannel,
|
|
3
|
+
AIStreamEvent,
|
|
4
|
+
AIMessage,
|
|
5
|
+
AIToolCall,
|
|
6
|
+
AIToolResult,
|
|
7
|
+
PikkuAIMiddlewareHooks,
|
|
8
|
+
AgentRunState,
|
|
9
|
+
CoreAIAgent,
|
|
10
|
+
AIAgentMemoryConfig,
|
|
11
|
+
} from './ai-agent.types.js'
|
|
12
|
+
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
13
|
+
import {
|
|
14
|
+
combineChannelMiddleware,
|
|
15
|
+
wrapChannelWithMiddleware,
|
|
16
|
+
} from '../channel/channel-middleware-runner.js'
|
|
17
|
+
import { randomUUID } from 'crypto'
|
|
18
|
+
import type { AIStorageService } from '../../services/ai-storage-service.js'
|
|
19
|
+
import type {
|
|
20
|
+
AIAgentRunnerParams,
|
|
21
|
+
AIAgentStepResult,
|
|
22
|
+
} from '../../services/ai-agent-runner-service.js'
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
parseWorkingMemory,
|
|
26
|
+
deepMergeWorkingMemory,
|
|
27
|
+
resolveMemoryServices,
|
|
28
|
+
loadContextMessages,
|
|
29
|
+
trimMessages,
|
|
30
|
+
} from './ai-agent-memory.js'
|
|
31
|
+
import {
|
|
32
|
+
prepareAgentRun,
|
|
33
|
+
resolveAgent,
|
|
34
|
+
buildInstructions,
|
|
35
|
+
buildToolDefs,
|
|
36
|
+
createScopedChannel,
|
|
37
|
+
ToolApprovalRequired,
|
|
38
|
+
type RunAIAgentParams,
|
|
39
|
+
type StreamAIAgentOptions,
|
|
40
|
+
type StreamContext,
|
|
41
|
+
} from './ai-agent-prepare.js'
|
|
42
|
+
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
43
|
+
import type { AIRunStateService } from '../../services/ai-run-state-service.js'
|
|
44
|
+
import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js'
|
|
45
|
+
|
|
46
|
+
type PersistingChannel = AIStreamChannel & {
|
|
47
|
+
fullText: string
|
|
48
|
+
flush: () => Promise<void>
|
|
49
|
+
totalUsage: { inputTokens: number; outputTokens: number; model?: string }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createPersistingChannel(
|
|
53
|
+
parent: AIStreamChannel,
|
|
54
|
+
storage: AIStorageService | undefined,
|
|
55
|
+
threadId: string
|
|
56
|
+
): PersistingChannel {
|
|
57
|
+
let fullText = ''
|
|
58
|
+
let stepText = ''
|
|
59
|
+
let stepToolCalls: AIToolCall[] = []
|
|
60
|
+
let stepToolResults: AIToolResult[] = []
|
|
61
|
+
const totalUsage: {
|
|
62
|
+
inputTokens: number
|
|
63
|
+
outputTokens: number
|
|
64
|
+
model?: string
|
|
65
|
+
} = {
|
|
66
|
+
inputTokens: 0,
|
|
67
|
+
outputTokens: 0,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const flushStep = async () => {
|
|
71
|
+
if (!storage) return
|
|
72
|
+
const text = stepText
|
|
73
|
+
const calls = stepToolCalls
|
|
74
|
+
const results = stepToolResults
|
|
75
|
+
stepText = ''
|
|
76
|
+
stepToolCalls = []
|
|
77
|
+
stepToolResults = []
|
|
78
|
+
if (text || calls.length > 0) {
|
|
79
|
+
const assistantMsg: AIMessage = {
|
|
80
|
+
id: randomUUID(),
|
|
81
|
+
role: 'assistant',
|
|
82
|
+
content: text || undefined,
|
|
83
|
+
toolCalls: calls.length > 0 ? calls : undefined,
|
|
84
|
+
createdAt: new Date(),
|
|
85
|
+
}
|
|
86
|
+
const messages: AIMessage[] = [assistantMsg]
|
|
87
|
+
if (results.length > 0) {
|
|
88
|
+
messages.push({
|
|
89
|
+
id: randomUUID(),
|
|
90
|
+
role: 'tool',
|
|
91
|
+
toolResults: results,
|
|
92
|
+
createdAt: new Date(),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
await storage.saveMessages(threadId, messages)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const channel: PersistingChannel = {
|
|
100
|
+
channelId: parent.channelId,
|
|
101
|
+
openingData: parent.openingData,
|
|
102
|
+
get state() {
|
|
103
|
+
return parent.state
|
|
104
|
+
},
|
|
105
|
+
get fullText() {
|
|
106
|
+
return fullText
|
|
107
|
+
},
|
|
108
|
+
get totalUsage() {
|
|
109
|
+
return totalUsage
|
|
110
|
+
},
|
|
111
|
+
flush: flushStep,
|
|
112
|
+
close: () => parent.close(),
|
|
113
|
+
sendBinary: (data) => parent.sendBinary(data),
|
|
114
|
+
send: (event: AIStreamEvent) => {
|
|
115
|
+
if (storage) {
|
|
116
|
+
switch (event.type) {
|
|
117
|
+
case 'text-delta':
|
|
118
|
+
stepText += event.text
|
|
119
|
+
fullText += event.text
|
|
120
|
+
break
|
|
121
|
+
case 'tool-call':
|
|
122
|
+
stepToolCalls.push({
|
|
123
|
+
id: event.toolCallId,
|
|
124
|
+
name: event.toolName,
|
|
125
|
+
args: event.args as Record<string, unknown>,
|
|
126
|
+
})
|
|
127
|
+
break
|
|
128
|
+
case 'tool-result':
|
|
129
|
+
stepToolResults.push({
|
|
130
|
+
id: event.toolCallId,
|
|
131
|
+
name: event.toolName,
|
|
132
|
+
result:
|
|
133
|
+
typeof event.result === 'string'
|
|
134
|
+
? event.result
|
|
135
|
+
: JSON.stringify(event.result),
|
|
136
|
+
})
|
|
137
|
+
break
|
|
138
|
+
case 'usage':
|
|
139
|
+
totalUsage.inputTokens += event.tokens.input
|
|
140
|
+
totalUsage.outputTokens += event.tokens.output
|
|
141
|
+
if (event.model) totalUsage.model = event.model
|
|
142
|
+
flushStep()
|
|
143
|
+
break
|
|
144
|
+
case 'done':
|
|
145
|
+
flushStep()
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
parent.send(event)
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
return channel
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function postStreamCleanup(
|
|
156
|
+
persistingChannel: PersistingChannel,
|
|
157
|
+
aiMiddlewares: PikkuAIMiddlewareHooks[],
|
|
158
|
+
singletonServices: any,
|
|
159
|
+
storage: AIStorageService | undefined,
|
|
160
|
+
memoryConfig: any,
|
|
161
|
+
threadId: string,
|
|
162
|
+
workingMemorySchemaName: string | null,
|
|
163
|
+
messages: AIMessage[],
|
|
164
|
+
aiRunState: AIRunStateService,
|
|
165
|
+
runId: string
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
const usage = persistingChannel.totalUsage
|
|
168
|
+
let outputText = persistingChannel.fullText
|
|
169
|
+
let outputMessages = messages
|
|
170
|
+
for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
|
|
171
|
+
const mw = aiMiddlewares[i]
|
|
172
|
+
if (mw.modifyOutput) {
|
|
173
|
+
const result = await mw.modifyOutput(singletonServices, {
|
|
174
|
+
text: outputText,
|
|
175
|
+
messages: outputMessages,
|
|
176
|
+
usage: {
|
|
177
|
+
inputTokens: usage.inputTokens,
|
|
178
|
+
outputTokens: usage.outputTokens,
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
outputText = result.text
|
|
182
|
+
outputMessages = result.messages
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (storage && memoryConfig?.workingMemory && outputText) {
|
|
187
|
+
const parsed = parseWorkingMemory(outputText)
|
|
188
|
+
if (parsed) {
|
|
189
|
+
const existing =
|
|
190
|
+
(await storage.getWorkingMemory(threadId, 'thread')) ?? {}
|
|
191
|
+
const merged = deepMergeWorkingMemory(existing, parsed)
|
|
192
|
+
|
|
193
|
+
if (singletonServices.schema && workingMemorySchemaName) {
|
|
194
|
+
try {
|
|
195
|
+
await singletonServices.schema.validateSchema(
|
|
196
|
+
workingMemorySchemaName,
|
|
197
|
+
merged
|
|
198
|
+
)
|
|
199
|
+
} catch (err) {
|
|
200
|
+
singletonServices.logger.warn(
|
|
201
|
+
`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await storage.saveWorkingMemory(threadId, 'thread', merged)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await aiRunState.updateRun(runId, {
|
|
211
|
+
status: 'completed',
|
|
212
|
+
...(usage.model
|
|
213
|
+
? {
|
|
214
|
+
usage: {
|
|
215
|
+
inputTokens: usage.inputTokens,
|
|
216
|
+
outputTokens: usage.outputTokens,
|
|
217
|
+
model: usage.model,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type StepLoopParams = {
|
|
225
|
+
agent: CoreAIAgent
|
|
226
|
+
runnerParams: AIAgentRunnerParams
|
|
227
|
+
maxSteps: number
|
|
228
|
+
agentRunner: AIAgentRunnerService
|
|
229
|
+
persistingChannel: PersistingChannel
|
|
230
|
+
channel: AIStreamChannel
|
|
231
|
+
runId: string
|
|
232
|
+
aiMiddlewares: PikkuAIMiddlewareHooks[]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
type StepLoopResult =
|
|
236
|
+
| { outcome: 'done' }
|
|
237
|
+
| { outcome: 'approval'; approvals: ToolApprovalRequired[] }
|
|
238
|
+
|
|
239
|
+
async function runStreamStepLoop(
|
|
240
|
+
params: StepLoopParams
|
|
241
|
+
): Promise<StepLoopResult> {
|
|
242
|
+
const {
|
|
243
|
+
agent,
|
|
244
|
+
runnerParams,
|
|
245
|
+
maxSteps,
|
|
246
|
+
agentRunner,
|
|
247
|
+
persistingChannel,
|
|
248
|
+
channel,
|
|
249
|
+
runId,
|
|
250
|
+
aiMiddlewares,
|
|
251
|
+
} = params
|
|
252
|
+
|
|
253
|
+
const singletonServices = getSingletonServices()
|
|
254
|
+
|
|
255
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
256
|
+
if (agent.prepareStep) {
|
|
257
|
+
let stopped = false
|
|
258
|
+
await agent.prepareStep({
|
|
259
|
+
stepNumber: step,
|
|
260
|
+
messages: runnerParams.messages,
|
|
261
|
+
tools: runnerParams.tools,
|
|
262
|
+
toolChoice: runnerParams.toolChoice,
|
|
263
|
+
model: runnerParams.model,
|
|
264
|
+
stop: () => {
|
|
265
|
+
stopped = true
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
if (stopped) break
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
channel.send({ type: 'step-start', stepNumber: step })
|
|
272
|
+
|
|
273
|
+
const stepResult = await agentRunner.stream(runnerParams, persistingChannel)
|
|
274
|
+
|
|
275
|
+
for (const mw of aiMiddlewares) {
|
|
276
|
+
if (mw.afterStep) {
|
|
277
|
+
await mw.afterStep(singletonServices, {
|
|
278
|
+
stepNumber: step,
|
|
279
|
+
text: stepResult.text,
|
|
280
|
+
toolCalls: stepResult.toolCalls,
|
|
281
|
+
toolResults: stepResult.toolResults,
|
|
282
|
+
usage: stepResult.usage,
|
|
283
|
+
finishReason: stepResult.finishReason,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (stepResult.toolCalls.length === 0) break
|
|
289
|
+
|
|
290
|
+
const approvalsNeeded = checkForApprovals(
|
|
291
|
+
stepResult,
|
|
292
|
+
runnerParams.tools,
|
|
293
|
+
runId
|
|
294
|
+
)
|
|
295
|
+
if (approvalsNeeded.length > 0) {
|
|
296
|
+
// For each approval, call approvalDescriptionFn if available
|
|
297
|
+
for (const approval of approvalsNeeded) {
|
|
298
|
+
const toolDef = runnerParams.tools.find(
|
|
299
|
+
(t) => t.name === approval.toolName
|
|
300
|
+
)
|
|
301
|
+
if (toolDef?.approvalDescriptionFn && !approval.reason) {
|
|
302
|
+
try {
|
|
303
|
+
approval.reason = await toolDef.approvalDescriptionFn(approval.args)
|
|
304
|
+
} catch {
|
|
305
|
+
// If description generation fails, continue without it
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { outcome: 'approval', approvals: approvalsNeeded }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
appendStepMessages(runnerParams, stepResult)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { outcome: 'done' }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function checkForApprovals(
|
|
319
|
+
stepResult: AIAgentStepResult,
|
|
320
|
+
tools: AIAgentRunnerParams['tools'],
|
|
321
|
+
runId: string
|
|
322
|
+
): ToolApprovalRequired[] {
|
|
323
|
+
const approvals: ToolApprovalRequired[] = []
|
|
324
|
+
for (const tc of stepResult.toolCalls) {
|
|
325
|
+
const toolDef = tools.find((t) => t.name === tc.toolName)
|
|
326
|
+
|
|
327
|
+
if (toolDef?.needsApproval) {
|
|
328
|
+
approvals.push(
|
|
329
|
+
new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args)
|
|
330
|
+
)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const tr = stepResult.toolResults.find(
|
|
335
|
+
(r) => r.toolCallId === tc.toolCallId
|
|
336
|
+
)
|
|
337
|
+
if (
|
|
338
|
+
tr?.result &&
|
|
339
|
+
typeof tr.result === 'object' &&
|
|
340
|
+
'__approvalRequired' in (tr.result as object)
|
|
341
|
+
) {
|
|
342
|
+
const r = tr.result as {
|
|
343
|
+
toolName: string
|
|
344
|
+
args: unknown
|
|
345
|
+
reason?: string
|
|
346
|
+
displayToolName?: string
|
|
347
|
+
displayArgs?: unknown
|
|
348
|
+
agentRunId?: string
|
|
349
|
+
subApprovals?: Array<{
|
|
350
|
+
toolCallId: string
|
|
351
|
+
toolName: string
|
|
352
|
+
args: unknown
|
|
353
|
+
runId: string
|
|
354
|
+
}>
|
|
355
|
+
}
|
|
356
|
+
if (r.subApprovals?.length) {
|
|
357
|
+
for (const sub of r.subApprovals) {
|
|
358
|
+
approvals.push(
|
|
359
|
+
new ToolApprovalRequired(
|
|
360
|
+
sub.toolCallId,
|
|
361
|
+
r.toolName,
|
|
362
|
+
r.args,
|
|
363
|
+
undefined,
|
|
364
|
+
sub.toolName,
|
|
365
|
+
sub.args,
|
|
366
|
+
r.agentRunId
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
approvals.push(
|
|
372
|
+
new ToolApprovalRequired(
|
|
373
|
+
tc.toolCallId,
|
|
374
|
+
r.toolName,
|
|
375
|
+
r.args,
|
|
376
|
+
r.reason,
|
|
377
|
+
r.displayToolName,
|
|
378
|
+
r.displayArgs,
|
|
379
|
+
r.agentRunId
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return approvals
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function appendStepMessages(
|
|
389
|
+
runnerParams: AIAgentRunnerParams,
|
|
390
|
+
stepResult: AIAgentStepResult
|
|
391
|
+
): void {
|
|
392
|
+
const assistantMsg: AIMessage = {
|
|
393
|
+
id: randomUUID(),
|
|
394
|
+
role: 'assistant',
|
|
395
|
+
content: stepResult.text || undefined,
|
|
396
|
+
toolCalls:
|
|
397
|
+
stepResult.toolCalls.length > 0
|
|
398
|
+
? stepResult.toolCalls.map((tc) => ({
|
|
399
|
+
id: tc.toolCallId,
|
|
400
|
+
name: tc.toolName,
|
|
401
|
+
args: tc.args as Record<string, unknown>,
|
|
402
|
+
}))
|
|
403
|
+
: undefined,
|
|
404
|
+
createdAt: new Date(),
|
|
405
|
+
}
|
|
406
|
+
runnerParams.messages.push(assistantMsg)
|
|
407
|
+
|
|
408
|
+
if (stepResult.toolResults.length > 0) {
|
|
409
|
+
const toolMsg: AIMessage = {
|
|
410
|
+
id: randomUUID(),
|
|
411
|
+
role: 'tool',
|
|
412
|
+
toolResults: stepResult.toolResults.map((tr) => ({
|
|
413
|
+
id: tr.toolCallId,
|
|
414
|
+
name: tr.toolName,
|
|
415
|
+
result:
|
|
416
|
+
typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result),
|
|
417
|
+
})),
|
|
418
|
+
createdAt: new Date(),
|
|
419
|
+
}
|
|
420
|
+
runnerParams.messages.push(toolMsg)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function handleApprovals(
|
|
425
|
+
approvals: ToolApprovalRequired[],
|
|
426
|
+
runId: string,
|
|
427
|
+
channel: AIStreamChannel,
|
|
428
|
+
aiRunState: AIRunStateService,
|
|
429
|
+
persistingChannel: PersistingChannel
|
|
430
|
+
): Promise<void> {
|
|
431
|
+
return (async () => {
|
|
432
|
+
await persistingChannel.flush()
|
|
433
|
+
|
|
434
|
+
const pendingApprovals = approvals.map((err) =>
|
|
435
|
+
err.agentRunId
|
|
436
|
+
? {
|
|
437
|
+
type: 'agent-call' as const,
|
|
438
|
+
toolCallId: err.toolCallId,
|
|
439
|
+
agentName: err.toolName,
|
|
440
|
+
agentRunId: err.agentRunId,
|
|
441
|
+
displayToolName: err.displayToolName ?? err.toolName,
|
|
442
|
+
displayArgs: err.displayArgs ?? err.args,
|
|
443
|
+
}
|
|
444
|
+
: {
|
|
445
|
+
type: 'tool-call' as const,
|
|
446
|
+
toolCallId: err.toolCallId,
|
|
447
|
+
toolName: err.toolName,
|
|
448
|
+
args: err.args,
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
await aiRunState.updateRun(runId, {
|
|
453
|
+
status: 'suspended',
|
|
454
|
+
suspendReason: 'approval',
|
|
455
|
+
pendingApprovals,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
for (const err of approvals) {
|
|
459
|
+
const approvalEvent = {
|
|
460
|
+
type: 'approval-request' as const,
|
|
461
|
+
toolCallId: err.toolCallId,
|
|
462
|
+
toolName: err.displayToolName ?? err.toolName,
|
|
463
|
+
args: err.displayArgs ?? err.args,
|
|
464
|
+
reason: err.reason,
|
|
465
|
+
runId,
|
|
466
|
+
}
|
|
467
|
+
channel.send(approvalEvent as any)
|
|
468
|
+
}
|
|
469
|
+
channel.send({ type: 'done' })
|
|
470
|
+
channel.close()
|
|
471
|
+
})()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function streamAIAgent(
|
|
475
|
+
agentName: string,
|
|
476
|
+
input: {
|
|
477
|
+
message: string
|
|
478
|
+
threadId: string
|
|
479
|
+
resourceId: string
|
|
480
|
+
model?: string
|
|
481
|
+
temperature?: number
|
|
482
|
+
},
|
|
483
|
+
channel: AIStreamChannel,
|
|
484
|
+
params: RunAIAgentParams,
|
|
485
|
+
agentSessionMap?: Map<string, string>,
|
|
486
|
+
options?: StreamAIAgentOptions
|
|
487
|
+
): Promise<string> {
|
|
488
|
+
const sessionMap = agentSessionMap ?? new Map<string, string>()
|
|
489
|
+
|
|
490
|
+
const normalizedInput = input
|
|
491
|
+
|
|
492
|
+
const streamContext: StreamContext = { channel, options }
|
|
493
|
+
// delegateState is attached after prepareAgentRun resolves the agent config
|
|
494
|
+
|
|
495
|
+
const {
|
|
496
|
+
agent,
|
|
497
|
+
packageName,
|
|
498
|
+
resolvedName,
|
|
499
|
+
agentRunner,
|
|
500
|
+
storage,
|
|
501
|
+
memoryConfig,
|
|
502
|
+
threadId,
|
|
503
|
+
userMessage,
|
|
504
|
+
runnerParams,
|
|
505
|
+
maxSteps,
|
|
506
|
+
missingRpcs,
|
|
507
|
+
workingMemorySchemaName,
|
|
508
|
+
} = await prepareAgentRun(
|
|
509
|
+
agentName,
|
|
510
|
+
normalizedInput,
|
|
511
|
+
params,
|
|
512
|
+
sessionMap,
|
|
513
|
+
streamContext
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
const singletonServices = getSingletonServices()
|
|
517
|
+
const { aiRunState } = singletonServices
|
|
518
|
+
if (!aiRunState) {
|
|
519
|
+
throw new Error('AIRunStateService not available in singletonServices')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (missingRpcs.length > 0) {
|
|
523
|
+
await aiRunState.createRun({
|
|
524
|
+
agentName,
|
|
525
|
+
threadId,
|
|
526
|
+
resourceId: normalizedInput.resourceId,
|
|
527
|
+
status: 'suspended',
|
|
528
|
+
suspendReason: 'rpc-missing',
|
|
529
|
+
missingRpcs,
|
|
530
|
+
usage: { inputTokens: 0, outputTokens: 0, model: agent.model },
|
|
531
|
+
createdAt: new Date(),
|
|
532
|
+
updatedAt: new Date(),
|
|
533
|
+
})
|
|
534
|
+
channel.send({ type: 'suspended', reason: 'rpc-missing', missingRpcs })
|
|
535
|
+
channel.send({ type: 'done' })
|
|
536
|
+
return ''
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
|
|
540
|
+
|
|
541
|
+
let modifiedMessages = runnerParams.messages
|
|
542
|
+
let modifiedInstructions = runnerParams.instructions
|
|
543
|
+
for (const mw of aiMiddlewares) {
|
|
544
|
+
if (mw.modifyInput) {
|
|
545
|
+
const result = await mw.modifyInput(singletonServices, {
|
|
546
|
+
messages: modifiedMessages,
|
|
547
|
+
instructions: modifiedInstructions,
|
|
548
|
+
})
|
|
549
|
+
modifiedMessages = result.messages
|
|
550
|
+
modifiedInstructions = result.instructions
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
runnerParams.messages = modifiedMessages
|
|
554
|
+
runnerParams.instructions = modifiedInstructions
|
|
555
|
+
|
|
556
|
+
const runId = await aiRunState.createRun({
|
|
557
|
+
agentName,
|
|
558
|
+
threadId,
|
|
559
|
+
resourceId: normalizedInput.resourceId,
|
|
560
|
+
status: 'running',
|
|
561
|
+
usage: { inputTokens: 0, outputTokens: 0, model: agent.model },
|
|
562
|
+
createdAt: new Date(),
|
|
563
|
+
updatedAt: new Date(),
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
if (storage) {
|
|
567
|
+
await storage.saveMessages(threadId, [userMessage])
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const streamMiddleware = aiMiddlewares
|
|
571
|
+
.filter((mw) => mw.modifyOutputStream)
|
|
572
|
+
.map((mw) => {
|
|
573
|
+
const state: Record<string, unknown> = {}
|
|
574
|
+
const allEvents: AIStreamEvent[] = []
|
|
575
|
+
return async (services: any, event: any, next: any) => {
|
|
576
|
+
allEvents.push(event)
|
|
577
|
+
const result = await mw.modifyOutputStream!(services, {
|
|
578
|
+
event,
|
|
579
|
+
allEvents,
|
|
580
|
+
state,
|
|
581
|
+
})
|
|
582
|
+
if (result == null) return
|
|
583
|
+
if (Array.isArray(result)) {
|
|
584
|
+
for (const r of result) await next(r)
|
|
585
|
+
} else {
|
|
586
|
+
await next(result)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
|
|
592
|
+
const meta = agentsMeta[resolvedName]
|
|
593
|
+
const allChannelMiddleware = combineChannelMiddleware(
|
|
594
|
+
'agent',
|
|
595
|
+
`stream:${agentName}`,
|
|
596
|
+
{
|
|
597
|
+
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
598
|
+
wireChannelMiddleware: [
|
|
599
|
+
...((agent.channelMiddleware as any[]) ?? []),
|
|
600
|
+
...streamMiddleware,
|
|
601
|
+
],
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
const wrappedChannel =
|
|
606
|
+
allChannelMiddleware.length > 0
|
|
607
|
+
? (wrapChannelWithMiddleware(
|
|
608
|
+
{ channel },
|
|
609
|
+
singletonServices,
|
|
610
|
+
allChannelMiddleware
|
|
611
|
+
).channel as AIStreamChannel)
|
|
612
|
+
: channel
|
|
613
|
+
|
|
614
|
+
// In delegate mode (default), suppress parent's text from reaching the client
|
|
615
|
+
// AFTER a sub-agent has been called. If the parent responds directly (no delegation),
|
|
616
|
+
// its text goes through normally. Sub-agent text bypasses this path entirely
|
|
617
|
+
// (goes through subChannel → channel directly).
|
|
618
|
+
const isDelegateMode = agent.agentMode !== 'supervise' && meta.agents?.length
|
|
619
|
+
const delegateState = { delegated: false }
|
|
620
|
+
if (isDelegateMode) {
|
|
621
|
+
streamContext.delegateState = delegateState
|
|
622
|
+
}
|
|
623
|
+
const outputChannel = isDelegateMode
|
|
624
|
+
? {
|
|
625
|
+
...wrappedChannel,
|
|
626
|
+
send: (event: AIStreamEvent) => {
|
|
627
|
+
if (
|
|
628
|
+
delegateState.delegated &&
|
|
629
|
+
(event.type === 'text-delta' || event.type === 'reasoning-delta')
|
|
630
|
+
)
|
|
631
|
+
return
|
|
632
|
+
wrappedChannel.send(event)
|
|
633
|
+
},
|
|
634
|
+
delegateState,
|
|
635
|
+
}
|
|
636
|
+
: wrappedChannel
|
|
637
|
+
|
|
638
|
+
const persistingChannel = createPersistingChannel(
|
|
639
|
+
outputChannel,
|
|
640
|
+
storage,
|
|
641
|
+
threadId
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const loopResult = await runStreamStepLoop({
|
|
646
|
+
agent,
|
|
647
|
+
runnerParams,
|
|
648
|
+
maxSteps,
|
|
649
|
+
agentRunner,
|
|
650
|
+
persistingChannel,
|
|
651
|
+
channel,
|
|
652
|
+
runId,
|
|
653
|
+
aiMiddlewares,
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
if (loopResult.outcome === 'approval') {
|
|
657
|
+
await handleApprovals(
|
|
658
|
+
loopResult.approvals,
|
|
659
|
+
runId,
|
|
660
|
+
channel,
|
|
661
|
+
aiRunState,
|
|
662
|
+
persistingChannel
|
|
663
|
+
)
|
|
664
|
+
return persistingChannel.fullText
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
await postStreamCleanup(
|
|
668
|
+
persistingChannel,
|
|
669
|
+
aiMiddlewares,
|
|
670
|
+
singletonServices,
|
|
671
|
+
storage,
|
|
672
|
+
memoryConfig,
|
|
673
|
+
threadId,
|
|
674
|
+
workingMemorySchemaName,
|
|
675
|
+
runnerParams.messages,
|
|
676
|
+
aiRunState,
|
|
677
|
+
runId
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
channel.send({ type: 'done' })
|
|
681
|
+
channel.close()
|
|
682
|
+
return persistingChannel.fullText
|
|
683
|
+
} catch (err) {
|
|
684
|
+
for (const mw of aiMiddlewares) {
|
|
685
|
+
if (mw.onError) {
|
|
686
|
+
try {
|
|
687
|
+
await mw.onError(singletonServices, {
|
|
688
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
689
|
+
stepNumber: -1,
|
|
690
|
+
messages: runnerParams.messages,
|
|
691
|
+
})
|
|
692
|
+
} catch {
|
|
693
|
+
// onError hooks must not affect error flow
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
await aiRunState.updateRun(runId, {
|
|
698
|
+
status: 'failed',
|
|
699
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
700
|
+
})
|
|
701
|
+
channel.send({
|
|
702
|
+
type: 'error',
|
|
703
|
+
message: err instanceof Error ? err.message : String(err),
|
|
704
|
+
})
|
|
705
|
+
channel.send({ type: 'done' })
|
|
706
|
+
channel.close()
|
|
707
|
+
return persistingChannel.fullText
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export async function resumeAIAgent(
|
|
712
|
+
input: {
|
|
713
|
+
runId: string
|
|
714
|
+
toolCallId: string
|
|
715
|
+
approved: boolean
|
|
716
|
+
},
|
|
717
|
+
channel: AIStreamChannel,
|
|
718
|
+
params: RunAIAgentParams,
|
|
719
|
+
options?: StreamAIAgentOptions
|
|
720
|
+
): Promise<void> {
|
|
721
|
+
const singletonServices = getSingletonServices()
|
|
722
|
+
const { aiRunState } = singletonServices
|
|
723
|
+
if (!aiRunState) {
|
|
724
|
+
throw new Error('AIRunStateService not available in singletonServices')
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const run = await aiRunState.getRun(input.runId)
|
|
728
|
+
if (!run) {
|
|
729
|
+
throw new Error(`No run found for runId ${input.runId}`)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const pending = run.pendingApprovals?.find(
|
|
733
|
+
(p) => p.toolCallId === input.toolCallId
|
|
734
|
+
)
|
|
735
|
+
if (!pending) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`No pending approval for toolCallId ${input.toolCallId} on run ${input.runId}`
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
await aiRunState.resolveApproval(
|
|
742
|
+
input.toolCallId,
|
|
743
|
+
input.approved ? 'approved' : 'denied'
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
const { agent, packageName, resolvedName } = resolveAgent(run.agentName)
|
|
747
|
+
const { storage } = resolveMemoryServices(agent, singletonServices)
|
|
748
|
+
const memoryConfig = agent.memory
|
|
749
|
+
const agentRunner = singletonServices.aiAgentRunner
|
|
750
|
+
if (!agentRunner) {
|
|
751
|
+
throw new Error('AIAgentRunnerService not available in singletonServices')
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!input.approved) {
|
|
755
|
+
if (pending.type === 'agent-call') {
|
|
756
|
+
await aiRunState.updateRun(pending.agentRunId, { status: 'failed' })
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const denialResult =
|
|
760
|
+
'The user explicitly declined this action. Inform them that it was declined and do not retry.'
|
|
761
|
+
|
|
762
|
+
if (storage) {
|
|
763
|
+
await storage.saveMessages(run.threadId, [
|
|
764
|
+
{
|
|
765
|
+
id: randomUUID(),
|
|
766
|
+
role: 'tool',
|
|
767
|
+
toolResults: [
|
|
768
|
+
{
|
|
769
|
+
id: input.toolCallId,
|
|
770
|
+
name:
|
|
771
|
+
pending.type === 'tool-call'
|
|
772
|
+
? pending.toolName
|
|
773
|
+
: pending.agentName,
|
|
774
|
+
result: denialResult,
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
createdAt: new Date(),
|
|
778
|
+
},
|
|
779
|
+
])
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Check remaining pending approvals
|
|
783
|
+
const updatedRun = await aiRunState.getRun(run.runId)
|
|
784
|
+
const remaining = updatedRun?.pendingApprovals ?? []
|
|
785
|
+
|
|
786
|
+
if (remaining.length > 0) {
|
|
787
|
+
// Still waiting for more approvals - don't continue step loop
|
|
788
|
+
channel.send({ type: 'done' })
|
|
789
|
+
channel.close()
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
await aiRunState.updateRun(run.runId, { status: 'running' })
|
|
794
|
+
|
|
795
|
+
await continueAfterToolResult(
|
|
796
|
+
run,
|
|
797
|
+
agent,
|
|
798
|
+
packageName,
|
|
799
|
+
resolvedName,
|
|
800
|
+
storage,
|
|
801
|
+
memoryConfig,
|
|
802
|
+
agentRunner,
|
|
803
|
+
channel,
|
|
804
|
+
params,
|
|
805
|
+
aiRunState,
|
|
806
|
+
options
|
|
807
|
+
)
|
|
808
|
+
return
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (pending.type === 'agent-call') {
|
|
812
|
+
const subRun = await aiRunState.getRun(pending.agentRunId)
|
|
813
|
+
if (!subRun) {
|
|
814
|
+
throw new Error(`Sub-agent run not found: ${pending.agentRunId}`)
|
|
815
|
+
}
|
|
816
|
+
const subPending =
|
|
817
|
+
subRun.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId) ??
|
|
818
|
+
subRun.pendingApprovals?.[0]
|
|
819
|
+
if (!subPending) {
|
|
820
|
+
throw new Error(
|
|
821
|
+
`No pending approval on sub-agent run ${pending.agentRunId}`
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const subChannel = createScopedChannel(channel, subRun.agentName, 'resume')
|
|
826
|
+
channel.send({
|
|
827
|
+
type: 'agent-call',
|
|
828
|
+
agentName: subRun.agentName,
|
|
829
|
+
session: 'resume',
|
|
830
|
+
input: null,
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
await resumeAIAgent(
|
|
834
|
+
{
|
|
835
|
+
runId: pending.agentRunId,
|
|
836
|
+
toolCallId: subPending.toolCallId,
|
|
837
|
+
approved: true,
|
|
838
|
+
},
|
|
839
|
+
subChannel,
|
|
840
|
+
params,
|
|
841
|
+
options
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
channel.send({
|
|
845
|
+
type: 'agent-result',
|
|
846
|
+
agentName: subRun.agentName,
|
|
847
|
+
session: 'resume',
|
|
848
|
+
result: null,
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
if (storage) {
|
|
852
|
+
await storage.saveMessages(run.threadId, [
|
|
853
|
+
{
|
|
854
|
+
id: randomUUID(),
|
|
855
|
+
role: 'tool',
|
|
856
|
+
toolResults: [
|
|
857
|
+
{
|
|
858
|
+
id: input.toolCallId,
|
|
859
|
+
name: pending.agentName,
|
|
860
|
+
result: 'Sub-agent completed successfully',
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
createdAt: new Date(),
|
|
864
|
+
},
|
|
865
|
+
])
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
const streamContext: StreamContext = {
|
|
869
|
+
channel,
|
|
870
|
+
options: { ...options, requiresToolApproval: false },
|
|
871
|
+
}
|
|
872
|
+
const aiMiddlewaresForResume: PikkuAIMiddlewareHooks[] =
|
|
873
|
+
agent.aiMiddleware ?? []
|
|
874
|
+
const { tools } = await buildToolDefs(
|
|
875
|
+
params,
|
|
876
|
+
new Map<string, string>(),
|
|
877
|
+
run.resourceId,
|
|
878
|
+
resolvedName,
|
|
879
|
+
packageName,
|
|
880
|
+
streamContext,
|
|
881
|
+
aiMiddlewaresForResume
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
const matchingTool = tools.find((t) => t.name === pending.toolName)
|
|
885
|
+
if (!matchingTool) {
|
|
886
|
+
throw new Error(
|
|
887
|
+
`Tool "${pending.toolName}" not found in agent definition`
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const toolArgs =
|
|
892
|
+
typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args
|
|
893
|
+
|
|
894
|
+
let toolResult: unknown
|
|
895
|
+
let isError = false
|
|
896
|
+
try {
|
|
897
|
+
toolResult = await matchingTool.execute(toolArgs)
|
|
898
|
+
} catch (execErr) {
|
|
899
|
+
toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
|
|
900
|
+
isError = true
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const resultStr =
|
|
904
|
+
typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult)
|
|
905
|
+
if (storage) {
|
|
906
|
+
await storage.saveMessages(run.threadId, [
|
|
907
|
+
{
|
|
908
|
+
id: randomUUID(),
|
|
909
|
+
role: 'tool',
|
|
910
|
+
toolResults: [
|
|
911
|
+
{
|
|
912
|
+
id: input.toolCallId,
|
|
913
|
+
name: pending.toolName,
|
|
914
|
+
result: resultStr,
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
createdAt: new Date(),
|
|
918
|
+
},
|
|
919
|
+
])
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
channel.send({
|
|
923
|
+
type: 'tool-result',
|
|
924
|
+
toolCallId: input.toolCallId,
|
|
925
|
+
toolName: pending.toolName,
|
|
926
|
+
result: toolResult,
|
|
927
|
+
...(isError ? { isError: true } : {}),
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Check remaining pending approvals after processing this one
|
|
932
|
+
const updatedRun = await aiRunState.getRun(run.runId)
|
|
933
|
+
const remaining = updatedRun?.pendingApprovals ?? []
|
|
934
|
+
|
|
935
|
+
if (remaining.length > 0) {
|
|
936
|
+
// Still waiting for more approvals - don't continue step loop
|
|
937
|
+
channel.send({ type: 'done' })
|
|
938
|
+
channel.close()
|
|
939
|
+
return
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
await aiRunState.updateRun(run.runId, { status: 'running' })
|
|
943
|
+
|
|
944
|
+
await continueAfterToolResult(
|
|
945
|
+
run,
|
|
946
|
+
agent,
|
|
947
|
+
packageName,
|
|
948
|
+
resolvedName,
|
|
949
|
+
storage,
|
|
950
|
+
memoryConfig,
|
|
951
|
+
agentRunner,
|
|
952
|
+
channel,
|
|
953
|
+
params,
|
|
954
|
+
aiRunState,
|
|
955
|
+
options
|
|
956
|
+
)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function continueAfterToolResult(
|
|
960
|
+
run: AgentRunState,
|
|
961
|
+
agent: CoreAIAgent,
|
|
962
|
+
packageName: string | null,
|
|
963
|
+
resolvedName: string,
|
|
964
|
+
storage: AIStorageService | undefined,
|
|
965
|
+
memoryConfig: AIAgentMemoryConfig | undefined,
|
|
966
|
+
agentRunner: AIAgentRunnerService,
|
|
967
|
+
channel: AIStreamChannel,
|
|
968
|
+
params: RunAIAgentParams,
|
|
969
|
+
aiRunState: AIRunStateService,
|
|
970
|
+
options?: StreamAIAgentOptions
|
|
971
|
+
): Promise<void> {
|
|
972
|
+
const singletonServices = getSingletonServices()
|
|
973
|
+
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
|
|
974
|
+
const meta = agentsMeta[resolvedName]
|
|
975
|
+
const workingMemorySchemaName = meta?.workingMemorySchema ?? null
|
|
976
|
+
|
|
977
|
+
const messages = storage
|
|
978
|
+
? await storage.getMessages(run.threadId, {
|
|
979
|
+
lastN: memoryConfig?.lastMessages ?? 20,
|
|
980
|
+
})
|
|
981
|
+
: []
|
|
982
|
+
|
|
983
|
+
const workingMemoryJsonSchema = workingMemorySchemaName
|
|
984
|
+
? pikkuState(packageName, 'misc', 'schemas').get(workingMemorySchemaName)
|
|
985
|
+
: undefined
|
|
986
|
+
|
|
987
|
+
const contextMessages = await loadContextMessages(
|
|
988
|
+
memoryConfig,
|
|
989
|
+
storage,
|
|
990
|
+
{ message: '', threadId: run.threadId, resourceId: run.resourceId },
|
|
991
|
+
workingMemoryJsonSchema
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
const allMessages = [...contextMessages, ...messages]
|
|
995
|
+
const trimmedMessages = trimMessages(allMessages)
|
|
996
|
+
|
|
997
|
+
const instructions = await buildInstructions(resolvedName, packageName)
|
|
998
|
+
|
|
999
|
+
const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
|
|
1000
|
+
let modifiedMessages = trimmedMessages
|
|
1001
|
+
let modifiedInstructions = instructions
|
|
1002
|
+
for (const mw of aiMiddlewares) {
|
|
1003
|
+
if (mw.modifyInput) {
|
|
1004
|
+
const result = await mw.modifyInput(singletonServices, {
|
|
1005
|
+
messages: modifiedMessages,
|
|
1006
|
+
instructions: modifiedInstructions,
|
|
1007
|
+
})
|
|
1008
|
+
modifiedMessages = result.messages
|
|
1009
|
+
modifiedInstructions = result.instructions
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const streamMiddleware = aiMiddlewares
|
|
1014
|
+
.filter((mw) => mw.modifyOutputStream)
|
|
1015
|
+
.map((mw) => {
|
|
1016
|
+
const state: Record<string, unknown> = {}
|
|
1017
|
+
const allEvents: AIStreamEvent[] = []
|
|
1018
|
+
return async (services: any, event: any, next: any) => {
|
|
1019
|
+
allEvents.push(event)
|
|
1020
|
+
const result = await mw.modifyOutputStream!(services, {
|
|
1021
|
+
event,
|
|
1022
|
+
allEvents,
|
|
1023
|
+
state,
|
|
1024
|
+
})
|
|
1025
|
+
if (result == null) return
|
|
1026
|
+
if (Array.isArray(result)) {
|
|
1027
|
+
for (const r of result) await next(r)
|
|
1028
|
+
} else {
|
|
1029
|
+
await next(result)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
const allChannelMiddleware = combineChannelMiddleware(
|
|
1035
|
+
'agent',
|
|
1036
|
+
`stream:${run.agentName}`,
|
|
1037
|
+
{
|
|
1038
|
+
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
1039
|
+
wireChannelMiddleware: [
|
|
1040
|
+
...((agent.channelMiddleware as any[]) ?? []),
|
|
1041
|
+
...streamMiddleware,
|
|
1042
|
+
],
|
|
1043
|
+
}
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
const wrappedChannel =
|
|
1047
|
+
allChannelMiddleware.length > 0
|
|
1048
|
+
? (wrapChannelWithMiddleware(
|
|
1049
|
+
{ channel },
|
|
1050
|
+
singletonServices,
|
|
1051
|
+
allChannelMiddleware
|
|
1052
|
+
).channel as AIStreamChannel)
|
|
1053
|
+
: channel
|
|
1054
|
+
|
|
1055
|
+
const persistingChannel = createPersistingChannel(
|
|
1056
|
+
wrappedChannel,
|
|
1057
|
+
storage,
|
|
1058
|
+
run.threadId
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
const streamContext: StreamContext = { channel, options }
|
|
1062
|
+
const resumeTools = (
|
|
1063
|
+
await buildToolDefs(
|
|
1064
|
+
params,
|
|
1065
|
+
new Map<string, string>(),
|
|
1066
|
+
run.resourceId,
|
|
1067
|
+
resolvedName,
|
|
1068
|
+
packageName,
|
|
1069
|
+
streamContext,
|
|
1070
|
+
aiMiddlewares
|
|
1071
|
+
)
|
|
1072
|
+
).tools
|
|
1073
|
+
|
|
1074
|
+
const resolved = resolveModelConfig(resolvedName, agent)
|
|
1075
|
+
const maxSteps = resolved.maxSteps ?? 10
|
|
1076
|
+
|
|
1077
|
+
const runnerParams: AIAgentRunnerParams = {
|
|
1078
|
+
model: resolved.model,
|
|
1079
|
+
temperature: resolved.temperature,
|
|
1080
|
+
instructions: modifiedInstructions,
|
|
1081
|
+
messages: modifiedMessages,
|
|
1082
|
+
tools: resumeTools,
|
|
1083
|
+
maxSteps: 1,
|
|
1084
|
+
toolChoice: (agent.toolChoice ?? 'auto') as 'auto' | 'required' | 'none',
|
|
1085
|
+
outputSchema: meta?.outputSchema
|
|
1086
|
+
? pikkuState(packageName, 'misc', 'schemas').get(meta.outputSchema)
|
|
1087
|
+
: undefined,
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
const loopResult = await runStreamStepLoop({
|
|
1092
|
+
agent,
|
|
1093
|
+
runnerParams,
|
|
1094
|
+
maxSteps,
|
|
1095
|
+
agentRunner,
|
|
1096
|
+
persistingChannel,
|
|
1097
|
+
channel,
|
|
1098
|
+
runId: run.runId,
|
|
1099
|
+
aiMiddlewares,
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
if (loopResult.outcome === 'approval') {
|
|
1103
|
+
await handleApprovals(
|
|
1104
|
+
loopResult.approvals,
|
|
1105
|
+
run.runId,
|
|
1106
|
+
channel,
|
|
1107
|
+
aiRunState,
|
|
1108
|
+
persistingChannel
|
|
1109
|
+
)
|
|
1110
|
+
return
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
await postStreamCleanup(
|
|
1114
|
+
persistingChannel,
|
|
1115
|
+
aiMiddlewares,
|
|
1116
|
+
singletonServices,
|
|
1117
|
+
storage,
|
|
1118
|
+
memoryConfig,
|
|
1119
|
+
run.threadId,
|
|
1120
|
+
workingMemorySchemaName,
|
|
1121
|
+
runnerParams.messages,
|
|
1122
|
+
aiRunState,
|
|
1123
|
+
run.runId
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
channel.send({ type: 'done' })
|
|
1127
|
+
channel.close()
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
for (const mw of aiMiddlewares) {
|
|
1130
|
+
if (mw.onError) {
|
|
1131
|
+
try {
|
|
1132
|
+
await mw.onError(singletonServices, {
|
|
1133
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1134
|
+
stepNumber: -1,
|
|
1135
|
+
messages: runnerParams.messages,
|
|
1136
|
+
})
|
|
1137
|
+
} catch {
|
|
1138
|
+
// onError hooks must not affect error flow
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
await aiRunState.updateRun(run.runId, {
|
|
1143
|
+
status: 'failed',
|
|
1144
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1145
|
+
})
|
|
1146
|
+
channel.send({
|
|
1147
|
+
type: 'error',
|
|
1148
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1149
|
+
})
|
|
1150
|
+
channel.send({ type: 'done' })
|
|
1151
|
+
channel.close()
|
|
1152
|
+
}
|
|
1153
|
+
}
|