@pikku/core 0.12.63 → 0.12.64
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 +147 -0
- package/dist/dev/hot-reload.js +13 -55
- package/dist/dev/module-runner.d.ts +14 -0
- package/dist/dev/module-runner.js +59 -0
- package/dist/dev/reload-meta.d.ts +13 -0
- package/dist/dev/reload-meta.js +22 -0
- package/dist/errors/errors.d.ts +12 -0
- package/dist/errors/errors.js +19 -0
- package/dist/function/function-runner.d.ts +5 -5
- package/dist/function/function-runner.js +39 -111
- package/dist/function/functions.types.d.ts +17 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/middleware/auth-apikey.d.ts +1 -0
- package/dist/middleware/auth-bearer.d.ts +1 -0
- package/dist/middleware/auth-cookie.d.ts +1 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.d.ts +1 -0
- package/dist/middleware/telemetry.d.ts +2 -0
- package/dist/middleware/timeout.d.ts +1 -0
- package/dist/permissions.d.ts +32 -25
- package/dist/permissions.js +104 -185
- package/dist/pikku-state.js +0 -2
- package/dist/scopes.d.ts +17 -0
- package/dist/scopes.js +59 -0
- package/dist/services/ai-embedding-service.d.ts +31 -0
- package/dist/services/ai-embedding-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +3 -1
- package/dist/services/credential-wire-service.js +10 -4
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meta-service.d.ts +15 -2
- package/dist/services/meta-service.js +51 -1
- package/dist/services/queue-webhook-service.d.ts +55 -0
- package/dist/services/queue-webhook-service.js +136 -0
- package/dist/services/scope-service.d.ts +67 -0
- package/dist/services/scope-service.js +1 -0
- package/dist/services/typed-secret-service.d.ts +8 -0
- package/dist/services/typed-secret-service.js +39 -4
- package/dist/services/webhook-service.d.ts +140 -0
- package/dist/services/webhook-service.js +44 -0
- package/dist/services/workflow-service.d.ts +7 -0
- package/dist/types/core.types.d.ts +34 -6
- package/dist/types/state.types.d.ts +20 -2
- package/dist/utils/hmac.d.ts +16 -0
- package/dist/utils/hmac.js +26 -0
- package/dist/utils/safe-fetch.d.ts +51 -0
- package/dist/utils/safe-fetch.js +192 -0
- package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +41 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +146 -6
- package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +61 -10
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +27 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -0
- package/dist/wirings/ai-agent/index.js +1 -0
- package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
- package/dist/wirings/ai-agent/voice-input.js +2 -38
- package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
- package/dist/wirings/channel/channel-common.js +0 -1
- package/dist/wirings/channel/channel-handler.js +1 -4
- package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
- package/dist/wirings/channel/channel-middleware-runner.js +34 -19
- package/dist/wirings/channel/channel.types.d.ts +2 -6
- package/dist/wirings/cli/cli-runner.js +0 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -2
- package/dist/wirings/http/http-routes.js +0 -3
- package/dist/wirings/http/http-runner.d.ts +9 -29
- package/dist/wirings/http/http-runner.js +9 -34
- package/dist/wirings/http/http.types.d.ts +1 -9
- package/dist/wirings/mcp/mcp-runner.js +0 -2
- package/dist/wirings/mcp/mcp.types.d.ts +5 -11
- package/dist/wirings/oauth2/index.d.ts +0 -3
- package/dist/wirings/oauth2/index.js +1 -2
- package/dist/wirings/rpc/addon-runner.d.ts +28 -0
- package/dist/wirings/rpc/addon-runner.js +173 -0
- package/dist/wirings/rpc/index.d.ts +5 -1
- package/dist/wirings/rpc/index.js +3 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
- package/dist/wirings/rpc/remote-addon-auth.js +43 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
- package/dist/wirings/rpc/rpc-runner.js +109 -3
- package/dist/wirings/rpc/rpc-types.d.ts +15 -1
- package/dist/wirings/rpc/wire-addon.js +9 -0
- package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
- package/dist/wirings/rpc/wire-remote-addon.js +19 -0
- package/dist/wirings/scope/index.d.ts +3 -0
- package/dist/wirings/scope/index.js +2 -0
- package/dist/wirings/scope/scope.types.d.ts +39 -0
- package/dist/wirings/scope/scope.types.js +1 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
- package/dist/wirings/scope/validate-scope-definitions.js +76 -0
- package/dist/wirings/scope/wire-scope.d.ts +33 -0
- package/dist/wirings/scope/wire-scope.js +32 -0
- package/dist/wirings/workflow/dsl/index.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +53 -1
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
- package/dist/wirings/workflow/index.d.ts +2 -2
- package/dist/wirings/workflow/index.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +51 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +171 -0
- package/dist/wirings/workflow/workflow.types.d.ts +1 -1
- package/package.json +5 -1
- package/run-tests.sh +1 -0
- package/src/dev/hot-reload.ts +13 -68
- package/src/dev/module-runner.test.ts +169 -0
- package/src/dev/module-runner.ts +103 -0
- package/src/dev/reload-meta.test.ts +31 -2
- package/src/dev/reload-meta.ts +26 -0
- package/src/errors/errors.ts +24 -0
- package/src/function/function-runner.test.ts +276 -142
- package/src/function/function-runner.ts +52 -145
- package/src/function/functions.types.ts +28 -38
- package/src/index.ts +8 -6
- package/src/middleware/index.ts +1 -5
- package/src/permissions.test.ts +153 -372
- package/src/permissions.ts +139 -259
- package/src/pikku-state.ts +0 -2
- package/src/scopes.test.ts +167 -0
- package/src/scopes.ts +69 -0
- package/src/services/ai-embedding-service.ts +31 -0
- package/src/services/credential-wire-service.ts +10 -4
- package/src/services/index.ts +20 -0
- package/src/services/meta-service.ts +68 -3
- package/src/services/queue-webhook-service.test.ts +408 -0
- package/src/services/queue-webhook-service.ts +182 -0
- package/src/services/scope-service.ts +75 -0
- package/src/services/typed-secret-service.test.ts +35 -0
- package/src/services/typed-secret-service.ts +39 -4
- package/src/services/webhook-service.ts +180 -0
- package/src/services/workflow-service.ts +7 -0
- package/src/types/core.types.ts +42 -14
- package/src/types/state.types.ts +23 -3
- package/src/utils/hmac.ts +27 -0
- package/src/utils/safe-fetch.test.ts +373 -0
- package/src/utils/safe-fetch.ts +213 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +308 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +192 -8
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +21 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +198 -10
- package/src/wirings/ai-agent/ai-agent-stream.ts +84 -10
- package/src/wirings/ai-agent/ai-agent.types.ts +28 -0
- package/src/wirings/ai-agent/index.ts +1 -0
- package/src/wirings/ai-agent/voice-input.ts +6 -35
- package/src/wirings/channel/channel-common.ts +0 -1
- package/src/wirings/channel/channel-handler.ts +1 -9
- package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
- package/src/wirings/channel/channel-middleware-runner.ts +39 -27
- package/src/wirings/channel/channel.types.ts +0 -6
- package/src/wirings/cli/cli-runner.ts +0 -2
- package/src/wirings/cli/cli.types.ts +0 -2
- package/src/wirings/http/http-routes.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +1 -11
- package/src/wirings/http/http-runner.ts +16 -39
- package/src/wirings/http/http.types.ts +1 -10
- package/src/wirings/mcp/mcp-runner.ts +0 -2
- package/src/wirings/mcp/mcp.types.ts +3 -11
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/rpc/addon-runner.ts +233 -0
- package/src/wirings/rpc/index.ts +17 -1
- package/src/wirings/rpc/remote-addon-auth.ts +69 -0
- package/src/wirings/rpc/rpc-runner.test.ts +319 -16
- package/src/wirings/rpc/rpc-runner.ts +155 -3
- package/src/wirings/rpc/rpc-types.ts +16 -5
- package/src/wirings/rpc/wire-addon.test.ts +6 -3
- package/src/wirings/rpc/wire-addon.ts +9 -0
- package/src/wirings/rpc/wire-remote-addon.ts +57 -0
- package/src/wirings/scope/index.ts +14 -0
- package/src/wirings/scope/scope.test.ts +135 -0
- package/src/wirings/scope/scope.types.ts +44 -0
- package/src/wirings/scope/validate-scope-definitions.ts +110 -0
- package/src/wirings/scope/wire-scope.ts +34 -0
- package/src/wirings/workflow/dsl/index.ts +4 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +61 -0
- package/src/wirings/workflow/graph/graph-node.ts +2 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/wirings/workflow/index.ts +5 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +236 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
- package/src/wirings/oauth2/oauth2-client.ts +0 -331
- package/src/wirings/oauth2/oauth2-routes.ts +0 -234
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { randomUUID } from './ai-agent-utils.js';
|
|
2
|
+
function resultToString(result) {
|
|
3
|
+
if (typeof result === 'string')
|
|
4
|
+
return result;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(result, (_key, val) => typeof val === 'bigint' ? val.toString() : val);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return String(result);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function wrapChannelWithAGUI(inner, options) {
|
|
13
|
+
const threadId = options?.threadId ?? randomUUID();
|
|
14
|
+
let runId = options?.runId ?? null;
|
|
15
|
+
function resolveRunId() {
|
|
16
|
+
if (!runId) {
|
|
17
|
+
runId = options?.getRunId?.() ?? randomUUID();
|
|
18
|
+
}
|
|
19
|
+
return runId;
|
|
20
|
+
}
|
|
21
|
+
let textMessageId = null;
|
|
22
|
+
let thinkingMessageId = null;
|
|
23
|
+
let openStepName = null;
|
|
24
|
+
let stepSeq = 0;
|
|
25
|
+
let runStartedSent = false;
|
|
26
|
+
let terminal = false;
|
|
27
|
+
let sawUsage = false;
|
|
28
|
+
let usageModel;
|
|
29
|
+
const usageTotals = { input: 0, output: 0 };
|
|
30
|
+
// The AG-UI client rejects any event arriving before RUN_STARTED, so the
|
|
31
|
+
// run is opened lazily with the first translated event.
|
|
32
|
+
function send(event) {
|
|
33
|
+
if (!runStartedSent) {
|
|
34
|
+
runStartedSent = true;
|
|
35
|
+
inner.send({
|
|
36
|
+
type: 'RUN_STARTED',
|
|
37
|
+
threadId,
|
|
38
|
+
runId: resolveRunId(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
inner.send(event);
|
|
42
|
+
}
|
|
43
|
+
function endTextMessage() {
|
|
44
|
+
if (textMessageId) {
|
|
45
|
+
send({ type: 'TEXT_MESSAGE_END', messageId: textMessageId });
|
|
46
|
+
textMessageId = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function endThinkingMessage() {
|
|
50
|
+
if (thinkingMessageId) {
|
|
51
|
+
send({ type: 'THINKING_TEXT_MESSAGE_END', messageId: thinkingMessageId });
|
|
52
|
+
send({ type: 'THINKING_END' });
|
|
53
|
+
thinkingMessageId = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function endStep() {
|
|
57
|
+
if (openStepName) {
|
|
58
|
+
send({ type: 'STEP_FINISHED', stepName: openStepName });
|
|
59
|
+
openStepName = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function ensureTextMessage() {
|
|
63
|
+
if (!textMessageId) {
|
|
64
|
+
textMessageId = randomUUID();
|
|
65
|
+
send({ type: 'TEXT_MESSAGE_START', messageId: textMessageId });
|
|
66
|
+
}
|
|
67
|
+
return textMessageId;
|
|
68
|
+
}
|
|
69
|
+
function ensureThinkingMessage() {
|
|
70
|
+
if (!thinkingMessageId) {
|
|
71
|
+
thinkingMessageId = randomUUID();
|
|
72
|
+
send({ type: 'THINKING_START' });
|
|
73
|
+
send({
|
|
74
|
+
type: 'THINKING_TEXT_MESSAGE_START',
|
|
75
|
+
messageId: thinkingMessageId,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return thinkingMessageId;
|
|
79
|
+
}
|
|
80
|
+
// The AG-UI client treats RUN_FINISHED as terminal and rejects everything
|
|
81
|
+
// after it, so it is emitted exactly once — on 'done' — with the usage
|
|
82
|
+
// accumulated across all steps (per-step 'usage' events must NOT finish
|
|
83
|
+
// the run: on multi-step tool runs later steps would arrive after
|
|
84
|
+
// RUN_FINISHED and the client would drop the whole stream).
|
|
85
|
+
function finishRun() {
|
|
86
|
+
endTextMessage();
|
|
87
|
+
endThinkingMessage();
|
|
88
|
+
endStep();
|
|
89
|
+
send({
|
|
90
|
+
type: 'RUN_FINISHED',
|
|
91
|
+
threadId,
|
|
92
|
+
runId: resolveRunId(),
|
|
93
|
+
...(usageModel ? { model: usageModel } : {}),
|
|
94
|
+
...(sawUsage
|
|
95
|
+
? {
|
|
96
|
+
usage: {
|
|
97
|
+
promptTokens: usageTotals.input,
|
|
98
|
+
completionTokens: usageTotals.output,
|
|
99
|
+
totalTokens: usageTotals.input + usageTotals.output,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
});
|
|
104
|
+
terminal = true;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
channelId: inner.channelId,
|
|
108
|
+
openingData: inner.openingData,
|
|
109
|
+
get state() {
|
|
110
|
+
return inner.state;
|
|
111
|
+
},
|
|
112
|
+
setState: (s) => inner.setState(s),
|
|
113
|
+
getState: () => inner.getState(),
|
|
114
|
+
clearState: () => inner.clearState(),
|
|
115
|
+
sendBinary: (data) => inner.sendBinary(data),
|
|
116
|
+
close: () => inner.close(),
|
|
117
|
+
send: (event) => {
|
|
118
|
+
if (terminal)
|
|
119
|
+
return;
|
|
120
|
+
switch (event.type) {
|
|
121
|
+
case 'text-delta': {
|
|
122
|
+
endThinkingMessage();
|
|
123
|
+
const id = ensureTextMessage();
|
|
124
|
+
send({
|
|
125
|
+
type: 'TEXT_MESSAGE_CONTENT',
|
|
126
|
+
messageId: id,
|
|
127
|
+
delta: event.text,
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'reasoning-delta': {
|
|
132
|
+
endTextMessage();
|
|
133
|
+
const id = ensureThinkingMessage();
|
|
134
|
+
send({
|
|
135
|
+
type: 'THINKING_TEXT_MESSAGE_CONTENT',
|
|
136
|
+
messageId: id,
|
|
137
|
+
delta: event.text,
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'tool-call': {
|
|
142
|
+
endTextMessage();
|
|
143
|
+
endThinkingMessage();
|
|
144
|
+
send({
|
|
145
|
+
type: 'TOOL_CALL_START',
|
|
146
|
+
toolCallId: event.toolCallId,
|
|
147
|
+
toolCallName: event.toolName,
|
|
148
|
+
});
|
|
149
|
+
send({
|
|
150
|
+
type: 'TOOL_CALL_ARGS',
|
|
151
|
+
toolCallId: event.toolCallId,
|
|
152
|
+
delta: resultToString(event.args) || '{}',
|
|
153
|
+
});
|
|
154
|
+
send({
|
|
155
|
+
type: 'TOOL_CALL_END',
|
|
156
|
+
toolCallId: event.toolCallId,
|
|
157
|
+
toolCallName: event.toolName,
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'tool-result': {
|
|
162
|
+
send({
|
|
163
|
+
type: 'TOOL_CALL_RESULT',
|
|
164
|
+
messageId: randomUUID(),
|
|
165
|
+
toolCallId: event.toolCallId,
|
|
166
|
+
role: 'tool',
|
|
167
|
+
content: resultToString(event.result),
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'usage': {
|
|
172
|
+
endTextMessage();
|
|
173
|
+
endThinkingMessage();
|
|
174
|
+
sawUsage = true;
|
|
175
|
+
usageTotals.input += event.tokens.input;
|
|
176
|
+
usageTotals.output += event.tokens.output;
|
|
177
|
+
if (event.model)
|
|
178
|
+
usageModel = event.model;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'error': {
|
|
182
|
+
endTextMessage();
|
|
183
|
+
endThinkingMessage();
|
|
184
|
+
endStep();
|
|
185
|
+
send({ type: 'RUN_ERROR', message: event.message });
|
|
186
|
+
terminal = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'done': {
|
|
190
|
+
finishRun();
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
// Step names must be unique among active steps on the client and
|
|
194
|
+
// sub-agents reuse step numbers on the shared channel, so each
|
|
195
|
+
// step-start closes the previous step and gets a sequential name.
|
|
196
|
+
case 'step-start': {
|
|
197
|
+
endTextMessage();
|
|
198
|
+
endThinkingMessage();
|
|
199
|
+
endStep();
|
|
200
|
+
stepSeq += 1;
|
|
201
|
+
openStepName = `${event.agent ?? 'step'}#${stepSeq}`;
|
|
202
|
+
send({ type: 'STEP_STARTED', stepName: openStepName });
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'approval-request': {
|
|
206
|
+
endTextMessage();
|
|
207
|
+
endThinkingMessage();
|
|
208
|
+
send({
|
|
209
|
+
type: 'CUSTOM',
|
|
210
|
+
name: 'pikku:approval-request',
|
|
211
|
+
value: {
|
|
212
|
+
toolCallId: event.toolCallId,
|
|
213
|
+
toolName: event.toolName,
|
|
214
|
+
args: event.args,
|
|
215
|
+
reason: event.reason,
|
|
216
|
+
runId: event.runId,
|
|
217
|
+
agent: event.agent,
|
|
218
|
+
session: event.session,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case 'credential-request': {
|
|
224
|
+
endTextMessage();
|
|
225
|
+
endThinkingMessage();
|
|
226
|
+
send({
|
|
227
|
+
type: 'CUSTOM',
|
|
228
|
+
name: 'pikku:credential-request',
|
|
229
|
+
value: {
|
|
230
|
+
toolCallId: event.toolCallId,
|
|
231
|
+
toolName: event.toolName,
|
|
232
|
+
args: event.args,
|
|
233
|
+
credentialName: event.credentialName,
|
|
234
|
+
credentialType: event.credentialType,
|
|
235
|
+
connectUrl: event.connectUrl,
|
|
236
|
+
runId: event.runId,
|
|
237
|
+
agent: event.agent,
|
|
238
|
+
session: event.session,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'generative-ui': {
|
|
244
|
+
send({
|
|
245
|
+
type: 'CUSTOM',
|
|
246
|
+
name: 'pikku:generative-ui',
|
|
247
|
+
value: { spec: event.spec },
|
|
248
|
+
});
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case 'data': {
|
|
252
|
+
send({
|
|
253
|
+
type: 'CUSTOM',
|
|
254
|
+
name: 'pikku:data',
|
|
255
|
+
value: { name: event.name, data: event.data },
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case 'workflow-created': {
|
|
260
|
+
send({
|
|
261
|
+
type: 'CUSTOM',
|
|
262
|
+
name: 'pikku:workflow-created',
|
|
263
|
+
value: { workflowName: event.workflowName, graph: event.graph },
|
|
264
|
+
});
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'agent-call': {
|
|
268
|
+
send({
|
|
269
|
+
type: 'CUSTOM',
|
|
270
|
+
name: 'pikku:agent-call',
|
|
271
|
+
value: {
|
|
272
|
+
agentName: event.agentName,
|
|
273
|
+
session: event.session,
|
|
274
|
+
input: event.input,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case 'agent-result': {
|
|
280
|
+
send({
|
|
281
|
+
type: 'CUSTOM',
|
|
282
|
+
name: 'pikku:agent-result',
|
|
283
|
+
value: {
|
|
284
|
+
agentName: event.agentName,
|
|
285
|
+
session: event.session,
|
|
286
|
+
result: event.result,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'suspended': {
|
|
292
|
+
send({
|
|
293
|
+
type: 'CUSTOM',
|
|
294
|
+
name: 'pikku:suspended',
|
|
295
|
+
value: { reason: event.reason, missingRpcs: event.missingRpcs },
|
|
296
|
+
});
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'audio-delta':
|
|
300
|
+
case 'audio-done':
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CoreUserSession } from '../../types/core.types.js';
|
|
2
|
-
import type { CoreAIAgent, AIAgentInput, AIAgentToolDef, AIMessage, AIStreamChannel, PikkuAIMiddlewareHooks } from './ai-agent.types.js';
|
|
2
|
+
import type { CoreAIAgent, AIAgentInput, AIAgentToolDef, AIMessage, AIStreamChannel, PikkuAIMiddlewareHooks, SessionScope } from './ai-agent.types.js';
|
|
3
3
|
import type { AIAgentRunnerParams } from '../../services/ai-agent-runner-service.js';
|
|
4
4
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
5
5
|
import type { SessionService } from '../../services/user-session-service.js';
|
|
@@ -8,9 +8,48 @@ export type RunAIAgentParams = {
|
|
|
8
8
|
/** Credential accessor for the current request — used to read per-user overrides (e.g. AI_API_KEY). */
|
|
9
9
|
getCredential?: <T = unknown>(name: string) => T | null | Promise<T | null>;
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* The ownership key for a thread/run: the trusted principal (the authenticated
|
|
13
|
+
* `session.userId` for `'user'` scope, or `session.orgId` for `'org'` scope)
|
|
14
|
+
* composed with the client-supplied `resourceId` as a sub-partition —
|
|
15
|
+
* `principal:resourceId`. Because the principal is always the prefix, a client
|
|
16
|
+
* `resourceId` can sub-divide within the caller's own boundary but can never
|
|
17
|
+
* widen access to another user's or org's threads.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: a value that is already within the caller's namespace (top-level
|
|
20
|
+
* re-entry, sub-agent recursion, or resume, which all pass an already-composite
|
|
21
|
+
* `resourceId`) is returned unchanged rather than double-composed.
|
|
22
|
+
*
|
|
23
|
+
* `'org'` scope with no session org throws {@link ForbiddenError} — falling back
|
|
24
|
+
* to a bare/shared key would leak across organizations. Sessionless `'user'`
|
|
25
|
+
* wirings have no trusted principal and fall back to the requested `resourceId`
|
|
26
|
+
* as a best-effort partition key.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveOwnerResourceId(params: RunAIAgentParams, sessionScope: SessionScope | undefined, requestedResourceId: string): string;
|
|
29
|
+
/** The declared {@link SessionScope} for an agent, defaulting to `'user'`. */
|
|
30
|
+
export declare function agentSessionScope(agentName: string): SessionScope;
|
|
31
|
+
/**
|
|
32
|
+
* Enforce that a stored thread/run belongs to the caller's owner resource,
|
|
33
|
+
* throwing {@link ForbiddenError} (without echoing the id, to avoid an
|
|
34
|
+
* existence oracle) on a mismatch.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertResourceOwner(ownerResourceId: string, storedResourceId: string, kind: 'thread' | 'run'): void;
|
|
11
37
|
export type StreamAIAgentOptions = {
|
|
12
38
|
requiresToolApproval?: 'all' | 'explicit' | false;
|
|
39
|
+
/**
|
|
40
|
+
* Invoked as soon as the run's AIRunStateService id exists, so transport
|
|
41
|
+
* wrappers (e.g. the AG-UI bridge) can stamp the real runId on RUN_STARTED.
|
|
42
|
+
*/
|
|
43
|
+
onRunCreated?: (runId: string) => void;
|
|
13
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Non-forgeable brand for the sub-agent approval marker. Only framework code
|
|
47
|
+
* (the delegating sub-agent tools below) sets this Symbol on a tool result; a
|
|
48
|
+
* sub-agent's LLM-shaped `result.object` — plain JSON — cannot carry a Symbol,
|
|
49
|
+
* so `checkForApprovals` trusts the brand rather than the `__approvalRequired`
|
|
50
|
+
* string key to decide an approval must be surfaced.
|
|
51
|
+
*/
|
|
52
|
+
export declare const APPROVAL_REQUIRED: unique symbol;
|
|
14
53
|
export declare class ToolApprovalRequired extends PikkuError {
|
|
15
54
|
readonly toolCallId: string;
|
|
16
55
|
readonly toolName: string;
|
|
@@ -60,6 +99,7 @@ export type ScopedChannel = AIStreamChannel & {
|
|
|
60
99
|
toolCallId: string;
|
|
61
100
|
toolName: string;
|
|
62
101
|
args: unknown;
|
|
102
|
+
reason?: string;
|
|
63
103
|
runId: string;
|
|
64
104
|
}>;
|
|
65
105
|
};
|
|
@@ -1,14 +1,75 @@
|
|
|
1
1
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
2
2
|
import { checkAuthPermissions } from '../../permissions.js';
|
|
3
3
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
4
|
+
import { ForbiddenError } from '../../errors/errors.js';
|
|
4
5
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
5
6
|
import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
|
|
6
7
|
import { randomUUID } from './ai-agent-utils.js';
|
|
7
8
|
import { streamAIAgent } from './ai-agent-stream.js';
|
|
8
9
|
import { runAIAgent } from './ai-agent-runner.js';
|
|
9
10
|
import { resolveNamespace, ContextAwareRPCService, } from '../../wirings/rpc/rpc-runner.js';
|
|
11
|
+
import { getOrCreatePackageSingletonServices } from '../../wirings/rpc/addon-runner.js';
|
|
10
12
|
import { resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
|
|
11
13
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
14
|
+
/**
|
|
15
|
+
* The ownership key for a thread/run: the trusted principal (the authenticated
|
|
16
|
+
* `session.userId` for `'user'` scope, or `session.orgId` for `'org'` scope)
|
|
17
|
+
* composed with the client-supplied `resourceId` as a sub-partition —
|
|
18
|
+
* `principal:resourceId`. Because the principal is always the prefix, a client
|
|
19
|
+
* `resourceId` can sub-divide within the caller's own boundary but can never
|
|
20
|
+
* widen access to another user's or org's threads.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent: a value that is already within the caller's namespace (top-level
|
|
23
|
+
* re-entry, sub-agent recursion, or resume, which all pass an already-composite
|
|
24
|
+
* `resourceId`) is returned unchanged rather than double-composed.
|
|
25
|
+
*
|
|
26
|
+
* `'org'` scope with no session org throws {@link ForbiddenError} — falling back
|
|
27
|
+
* to a bare/shared key would leak across organizations. Sessionless `'user'`
|
|
28
|
+
* wirings have no trusted principal and fall back to the requested `resourceId`
|
|
29
|
+
* as a best-effort partition key.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveOwnerResourceId(params, sessionScope, requestedResourceId) {
|
|
32
|
+
const session = params.sessionService?.get();
|
|
33
|
+
let principal;
|
|
34
|
+
if ((sessionScope ?? 'user') === 'org') {
|
|
35
|
+
if (!session?.orgId) {
|
|
36
|
+
throw new ForbiddenError('This agent is org-scoped but the session has no organization');
|
|
37
|
+
}
|
|
38
|
+
principal = session.orgId;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
principal = session?.userId;
|
|
42
|
+
}
|
|
43
|
+
if (!principal)
|
|
44
|
+
return requestedResourceId;
|
|
45
|
+
if (requestedResourceId === principal ||
|
|
46
|
+
requestedResourceId.startsWith(`${principal}:`)) {
|
|
47
|
+
return requestedResourceId;
|
|
48
|
+
}
|
|
49
|
+
return `${principal}:${requestedResourceId}`;
|
|
50
|
+
}
|
|
51
|
+
/** The declared {@link SessionScope} for an agent, defaulting to `'user'`. */
|
|
52
|
+
export function agentSessionScope(agentName) {
|
|
53
|
+
return resolveAgent(agentName).agent.sessionScope ?? 'user';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Enforce that a stored thread/run belongs to the caller's owner resource,
|
|
57
|
+
* throwing {@link ForbiddenError} (without echoing the id, to avoid an
|
|
58
|
+
* existence oracle) on a mismatch.
|
|
59
|
+
*/
|
|
60
|
+
export function assertResourceOwner(ownerResourceId, storedResourceId, kind) {
|
|
61
|
+
if (storedResourceId !== ownerResourceId) {
|
|
62
|
+
throw new ForbiddenError(`Not authorized to access this ${kind}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Non-forgeable brand for the sub-agent approval marker. Only framework code
|
|
67
|
+
* (the delegating sub-agent tools below) sets this Symbol on a tool result; a
|
|
68
|
+
* sub-agent's LLM-shaped `result.object` — plain JSON — cannot carry a Symbol,
|
|
69
|
+
* so `checkForApprovals` trusts the brand rather than the `__approvalRequired`
|
|
70
|
+
* string key to decide an approval must be surfaced.
|
|
71
|
+
*/
|
|
72
|
+
export const APPROVAL_REQUIRED = Symbol('pikku.ai.approvalRequired');
|
|
12
73
|
export class ToolApprovalRequired extends PikkuError {
|
|
13
74
|
toolCallId;
|
|
14
75
|
toolName;
|
|
@@ -112,7 +173,7 @@ export async function buildInstructions(agentName, packageName) {
|
|
|
112
173
|
if (meta?.goal)
|
|
113
174
|
parts.push(meta.goal);
|
|
114
175
|
let instructions = parts.join('\n\n');
|
|
115
|
-
if (meta?.tools?.length) {
|
|
176
|
+
if (meta?.tools?.length || meta?.workflows?.length) {
|
|
116
177
|
instructions +=
|
|
117
178
|
'\n\nTool usage rules:\n' +
|
|
118
179
|
'- Act immediately with the information given. Do NOT ask clarifying questions unless a required field is truly missing.\n' +
|
|
@@ -151,6 +212,7 @@ export function createScopedChannel(parent, agentName, session) {
|
|
|
151
212
|
toolCallId: event.toolCallId,
|
|
152
213
|
toolName: event.toolName,
|
|
153
214
|
args: event.args,
|
|
215
|
+
...(event.reason !== undefined ? { reason: event.reason } : {}),
|
|
154
216
|
runId: event.runId,
|
|
155
217
|
});
|
|
156
218
|
return;
|
|
@@ -244,13 +306,22 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
244
306
|
if (funcConfig?.approvalDescription) {
|
|
245
307
|
const descFn = funcConfig.approvalDescription;
|
|
246
308
|
const capturedPkg = resolvedPkg;
|
|
309
|
+
const capturedAddonConfig = resolved?.addonConfig;
|
|
310
|
+
const capturedNamespace = toolName.includes(':')
|
|
311
|
+
? toolName.slice(0, toolName.indexOf(':'))
|
|
312
|
+
: null;
|
|
247
313
|
approvalDescriptionFn = async (input) => {
|
|
248
314
|
let services = singletonServices;
|
|
249
315
|
if (capturedPkg) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
316
|
+
const addonInstance = capturedNamespace
|
|
317
|
+
? {
|
|
318
|
+
namespace: capturedNamespace,
|
|
319
|
+
secretOverrides: capturedAddonConfig?.secretOverrides,
|
|
320
|
+
variableOverrides: capturedAddonConfig?.variableOverrides,
|
|
321
|
+
credentialOverrides: capturedAddonConfig?.credentialOverrides,
|
|
322
|
+
}
|
|
323
|
+
: undefined;
|
|
324
|
+
services = await getOrCreatePackageSingletonServices(capturedPkg, singletonServices, addonInstance);
|
|
254
325
|
}
|
|
255
326
|
return descFn(services, input);
|
|
256
327
|
};
|
|
@@ -291,6 +362,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
291
362
|
tools.push({
|
|
292
363
|
name: subAgentName,
|
|
293
364
|
description: subMeta.description,
|
|
365
|
+
forwardsApproval: true,
|
|
294
366
|
inputSchema: {
|
|
295
367
|
type: 'object',
|
|
296
368
|
properties: {
|
|
@@ -339,6 +411,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
339
411
|
const resultText = await streamAIAgent(subAgentName, { message, threadId, resourceId }, effectiveChannel, params, agentSessionMap, streamContext.options);
|
|
340
412
|
if (subChannel.approvals.length > 0) {
|
|
341
413
|
return {
|
|
414
|
+
[APPROVAL_REQUIRED]: true,
|
|
342
415
|
__approvalRequired: true,
|
|
343
416
|
toolName: subAgentName,
|
|
344
417
|
args: toolInput,
|
|
@@ -359,6 +432,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
359
432
|
if (result.status === 'suspended' &&
|
|
360
433
|
result.pendingApprovals?.length) {
|
|
361
434
|
return {
|
|
435
|
+
[APPROVAL_REQUIRED]: true,
|
|
362
436
|
__approvalRequired: true,
|
|
363
437
|
toolName: subAgentName,
|
|
364
438
|
args: toolInput,
|
|
@@ -367,6 +441,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
367
441
|
toolCallId: a.toolCallId,
|
|
368
442
|
toolName: a.toolName,
|
|
369
443
|
args: a.args,
|
|
444
|
+
reason: a.reason,
|
|
370
445
|
runId: a.runId,
|
|
371
446
|
})),
|
|
372
447
|
};
|
|
@@ -376,6 +451,64 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
376
451
|
});
|
|
377
452
|
}
|
|
378
453
|
}
|
|
454
|
+
const metaWorkflows = meta.workflows;
|
|
455
|
+
if (metaWorkflows?.length) {
|
|
456
|
+
const workflowMetaMap = pikkuState(null, 'workflows', 'meta');
|
|
457
|
+
const functionMeta = pikkuState(null, 'function', 'meta');
|
|
458
|
+
const schemas = pikkuState(null, 'misc', 'schemas');
|
|
459
|
+
for (const workflowName of metaWorkflows) {
|
|
460
|
+
const wfMeta = workflowMetaMap[workflowName];
|
|
461
|
+
if (!wfMeta) {
|
|
462
|
+
missingRpcs.push(workflowName);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const inputSchemaName = wfMeta.pikkuFuncId
|
|
466
|
+
? functionMeta[wfMeta.pikkuFuncId]?.inputSchemaName
|
|
467
|
+
: undefined;
|
|
468
|
+
let inputSchema = inputSchemaName
|
|
469
|
+
? schemas.get(inputSchemaName)
|
|
470
|
+
: undefined;
|
|
471
|
+
if (!inputSchema ||
|
|
472
|
+
(typeof inputSchema === 'object' &&
|
|
473
|
+
inputSchema.type === 'object' &&
|
|
474
|
+
!inputSchema.properties)) {
|
|
475
|
+
inputSchema = { type: 'object', properties: {} };
|
|
476
|
+
}
|
|
477
|
+
tools.push({
|
|
478
|
+
name: workflowName,
|
|
479
|
+
description: wfMeta.description || workflowName,
|
|
480
|
+
inputSchema,
|
|
481
|
+
execute: async (toolInput) => {
|
|
482
|
+
const workflowService = singletonServices.workflowService;
|
|
483
|
+
if (!workflowService) {
|
|
484
|
+
throw new Error(`workflowService is not configured — cannot run workflow tool '${workflowName}'`);
|
|
485
|
+
}
|
|
486
|
+
const wire = params.sessionService
|
|
487
|
+
? { ...createMiddlewareSessionWireProps(params.sessionService) }
|
|
488
|
+
: {};
|
|
489
|
+
const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
|
|
490
|
+
return workflowService.runToCompletion(workflowName, toolInput, rpcService, { wire: { type: 'internal' } });
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// A tool's execute() can throw (bad input, RPC failure, DB error, ...). The AI
|
|
496
|
+
// SDK catches that at the tool-call boundary and turns it into a conversational
|
|
497
|
+
// "tool error" reply — without this, the exception never reaches pikku's own
|
|
498
|
+
// logger, so a failing tool is undiagnosable server-side. Log unconditionally,
|
|
499
|
+
// even when no aiMiddleware afterToolCall hook is registered to see it.
|
|
500
|
+
for (const tool of tools) {
|
|
501
|
+
const originalExecute = tool.execute;
|
|
502
|
+
tool.execute = async (toolInput) => {
|
|
503
|
+
try {
|
|
504
|
+
return await originalExecute(toolInput);
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
singletonServices.logger.error(`AI agent tool '${tool.name}' threw during execute()`, err);
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
379
512
|
const hasToolHooks = aiMiddlewares?.some((mw) => mw.beforeToolCall || mw.afterToolCall);
|
|
380
513
|
if (hasToolHooks) {
|
|
381
514
|
for (const tool of tools) {
|
|
@@ -458,10 +591,17 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
|
|
|
458
591
|
? pikkuState(packageName, 'misc', 'schemas').get(workingMemorySchemaName)
|
|
459
592
|
: undefined;
|
|
460
593
|
if (storage) {
|
|
594
|
+
let thread = null;
|
|
461
595
|
try {
|
|
462
|
-
await storage.getThread(threadId);
|
|
596
|
+
thread = await storage.getThread(threadId);
|
|
463
597
|
}
|
|
464
598
|
catch {
|
|
599
|
+
thread = null;
|
|
600
|
+
}
|
|
601
|
+
if (thread) {
|
|
602
|
+
assertResourceOwner(input.resourceId, thread.resourceId, 'thread');
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
465
605
|
await storage.createThread(input.resourceId, { threadId });
|
|
466
606
|
}
|
|
467
607
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
|
|
2
|
-
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, } from './ai-agent-prepare.js';
|
|
2
|
+
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, } from './ai-agent-prepare.js';
|
|
3
3
|
import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
|
|
4
4
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
5
5
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
@@ -32,6 +32,10 @@ function extractStructuredAssistantOutput(object) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
34
34
|
const sessionMap = agentSessionMap ?? new Map();
|
|
35
|
+
input = {
|
|
36
|
+
...input,
|
|
37
|
+
resourceId: resolveOwnerResourceId(params, agentSessionScope(agentName), input.resourceId),
|
|
38
|
+
};
|
|
35
39
|
const { agent, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, input, params, sessionMap);
|
|
36
40
|
runnerParams.agentId = agentName;
|
|
37
41
|
const singletonServices = getSingletonServices();
|
|
@@ -281,6 +285,7 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
281
285
|
const run = await aiRunState.getRun(runId);
|
|
282
286
|
if (!run)
|
|
283
287
|
throw new Error(`No run found for runId ${runId}`);
|
|
288
|
+
assertResourceOwner(resolveOwnerResourceId(params, agentSessionScope(run.agentName), run.resourceId), run.resourceId, 'run');
|
|
284
289
|
if (expectedAgentName && run.agentName !== expectedAgentName) {
|
|
285
290
|
throw new Error(`Run ${runId} belongs to agent '${run.agentName}', not '${expectedAgentName}'`);
|
|
286
291
|
}
|