@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.
Files changed (198) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/dev/hot-reload.js +13 -55
  3. package/dist/dev/module-runner.d.ts +14 -0
  4. package/dist/dev/module-runner.js +59 -0
  5. package/dist/dev/reload-meta.d.ts +13 -0
  6. package/dist/dev/reload-meta.js +22 -0
  7. package/dist/errors/errors.d.ts +12 -0
  8. package/dist/errors/errors.js +19 -0
  9. package/dist/function/function-runner.d.ts +5 -5
  10. package/dist/function/function-runner.js +39 -111
  11. package/dist/function/functions.types.d.ts +17 -7
  12. package/dist/index.d.ts +5 -1
  13. package/dist/index.js +3 -1
  14. package/dist/middleware/auth-apikey.d.ts +1 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -0
  16. package/dist/middleware/auth-cookie.d.ts +1 -0
  17. package/dist/middleware/cors.d.ts +1 -0
  18. package/dist/middleware/index.d.ts +1 -1
  19. package/dist/middleware/index.js +1 -1
  20. package/dist/middleware/remote-auth.d.ts +1 -0
  21. package/dist/middleware/telemetry.d.ts +2 -0
  22. package/dist/middleware/timeout.d.ts +1 -0
  23. package/dist/permissions.d.ts +32 -25
  24. package/dist/permissions.js +104 -185
  25. package/dist/pikku-state.js +0 -2
  26. package/dist/scopes.d.ts +17 -0
  27. package/dist/scopes.js +59 -0
  28. package/dist/services/ai-embedding-service.d.ts +31 -0
  29. package/dist/services/ai-embedding-service.js +1 -0
  30. package/dist/services/credential-wire-service.d.ts +3 -1
  31. package/dist/services/credential-wire-service.js +10 -4
  32. package/dist/services/index.d.ts +4 -0
  33. package/dist/services/index.js +2 -0
  34. package/dist/services/meta-service.d.ts +15 -2
  35. package/dist/services/meta-service.js +51 -1
  36. package/dist/services/queue-webhook-service.d.ts +55 -0
  37. package/dist/services/queue-webhook-service.js +136 -0
  38. package/dist/services/scope-service.d.ts +67 -0
  39. package/dist/services/scope-service.js +1 -0
  40. package/dist/services/typed-secret-service.d.ts +8 -0
  41. package/dist/services/typed-secret-service.js +39 -4
  42. package/dist/services/webhook-service.d.ts +140 -0
  43. package/dist/services/webhook-service.js +44 -0
  44. package/dist/services/workflow-service.d.ts +7 -0
  45. package/dist/types/core.types.d.ts +34 -6
  46. package/dist/types/state.types.d.ts +20 -2
  47. package/dist/utils/hmac.d.ts +16 -0
  48. package/dist/utils/hmac.js +26 -0
  49. package/dist/utils/safe-fetch.d.ts +51 -0
  50. package/dist/utils/safe-fetch.js +192 -0
  51. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  53. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +41 -1
  54. package/dist/wirings/ai-agent/ai-agent-prepare.js +146 -6
  55. package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
  56. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -10
  57. package/dist/wirings/ai-agent/ai-agent.types.d.ts +27 -1
  58. package/dist/wirings/ai-agent/index.d.ts +1 -0
  59. package/dist/wirings/ai-agent/index.js +1 -0
  60. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  61. package/dist/wirings/ai-agent/voice-input.js +2 -38
  62. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  63. package/dist/wirings/channel/channel-common.js +0 -1
  64. package/dist/wirings/channel/channel-handler.js +1 -4
  65. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  66. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  67. package/dist/wirings/channel/channel.types.d.ts +2 -6
  68. package/dist/wirings/cli/cli-runner.js +0 -2
  69. package/dist/wirings/cli/cli.types.d.ts +1 -2
  70. package/dist/wirings/http/http-routes.js +0 -3
  71. package/dist/wirings/http/http-runner.d.ts +9 -29
  72. package/dist/wirings/http/http-runner.js +9 -34
  73. package/dist/wirings/http/http.types.d.ts +1 -9
  74. package/dist/wirings/mcp/mcp-runner.js +0 -2
  75. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  76. package/dist/wirings/oauth2/index.d.ts +0 -3
  77. package/dist/wirings/oauth2/index.js +1 -2
  78. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  79. package/dist/wirings/rpc/addon-runner.js +173 -0
  80. package/dist/wirings/rpc/index.d.ts +5 -1
  81. package/dist/wirings/rpc/index.js +3 -1
  82. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  83. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  84. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  85. package/dist/wirings/rpc/rpc-runner.js +109 -3
  86. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  87. package/dist/wirings/rpc/wire-addon.js +9 -0
  88. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  89. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  90. package/dist/wirings/scope/index.d.ts +3 -0
  91. package/dist/wirings/scope/index.js +2 -0
  92. package/dist/wirings/scope/scope.types.d.ts +39 -0
  93. package/dist/wirings/scope/scope.types.js +1 -0
  94. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  95. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  96. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  97. package/dist/wirings/scope/wire-scope.js +32 -0
  98. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +53 -1
  100. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  101. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  102. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  103. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  104. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  105. package/dist/wirings/workflow/index.d.ts +2 -2
  106. package/dist/wirings/workflow/index.js +1 -1
  107. package/dist/wirings/workflow/pikku-workflow-service.d.ts +51 -1
  108. package/dist/wirings/workflow/pikku-workflow-service.js +171 -0
  109. package/dist/wirings/workflow/workflow.types.d.ts +1 -1
  110. package/package.json +5 -1
  111. package/run-tests.sh +1 -0
  112. package/src/dev/hot-reload.ts +13 -68
  113. package/src/dev/module-runner.test.ts +169 -0
  114. package/src/dev/module-runner.ts +103 -0
  115. package/src/dev/reload-meta.test.ts +31 -2
  116. package/src/dev/reload-meta.ts +26 -0
  117. package/src/errors/errors.ts +24 -0
  118. package/src/function/function-runner.test.ts +276 -142
  119. package/src/function/function-runner.ts +52 -145
  120. package/src/function/functions.types.ts +28 -38
  121. package/src/index.ts +8 -6
  122. package/src/middleware/index.ts +1 -5
  123. package/src/permissions.test.ts +153 -372
  124. package/src/permissions.ts +139 -259
  125. package/src/pikku-state.ts +0 -2
  126. package/src/scopes.test.ts +167 -0
  127. package/src/scopes.ts +69 -0
  128. package/src/services/ai-embedding-service.ts +31 -0
  129. package/src/services/credential-wire-service.ts +10 -4
  130. package/src/services/index.ts +20 -0
  131. package/src/services/meta-service.ts +68 -3
  132. package/src/services/queue-webhook-service.test.ts +408 -0
  133. package/src/services/queue-webhook-service.ts +182 -0
  134. package/src/services/scope-service.ts +75 -0
  135. package/src/services/typed-secret-service.test.ts +35 -0
  136. package/src/services/typed-secret-service.ts +39 -4
  137. package/src/services/webhook-service.ts +180 -0
  138. package/src/services/workflow-service.ts +7 -0
  139. package/src/types/core.types.ts +42 -14
  140. package/src/types/state.types.ts +23 -3
  141. package/src/utils/hmac.ts +27 -0
  142. package/src/utils/safe-fetch.test.ts +373 -0
  143. package/src/utils/safe-fetch.ts +213 -0
  144. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  145. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  146. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +308 -0
  147. package/src/wirings/ai-agent/ai-agent-prepare.ts +192 -8
  148. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  149. package/src/wirings/ai-agent/ai-agent-runner.ts +21 -0
  150. package/src/wirings/ai-agent/ai-agent-stream.test.ts +198 -10
  151. package/src/wirings/ai-agent/ai-agent-stream.ts +84 -10
  152. package/src/wirings/ai-agent/ai-agent.types.ts +28 -0
  153. package/src/wirings/ai-agent/index.ts +1 -0
  154. package/src/wirings/ai-agent/voice-input.ts +6 -35
  155. package/src/wirings/channel/channel-common.ts +0 -1
  156. package/src/wirings/channel/channel-handler.ts +1 -9
  157. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  158. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  159. package/src/wirings/channel/channel.types.ts +0 -6
  160. package/src/wirings/cli/cli-runner.ts +0 -2
  161. package/src/wirings/cli/cli.types.ts +0 -2
  162. package/src/wirings/http/http-routes.ts +0 -3
  163. package/src/wirings/http/http-runner.test.ts +1 -11
  164. package/src/wirings/http/http-runner.ts +16 -39
  165. package/src/wirings/http/http.types.ts +1 -10
  166. package/src/wirings/mcp/mcp-runner.ts +0 -2
  167. package/src/wirings/mcp/mcp.types.ts +3 -11
  168. package/src/wirings/oauth2/index.ts +0 -3
  169. package/src/wirings/rpc/addon-runner.ts +233 -0
  170. package/src/wirings/rpc/index.ts +17 -1
  171. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  172. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  173. package/src/wirings/rpc/rpc-runner.ts +155 -3
  174. package/src/wirings/rpc/rpc-types.ts +16 -5
  175. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  176. package/src/wirings/rpc/wire-addon.ts +9 -0
  177. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  178. package/src/wirings/scope/index.ts +14 -0
  179. package/src/wirings/scope/scope.test.ts +135 -0
  180. package/src/wirings/scope/scope.types.ts +44 -0
  181. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  182. package/src/wirings/scope/wire-scope.ts +34 -0
  183. package/src/wirings/workflow/dsl/index.ts +4 -0
  184. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +61 -0
  185. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  186. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  187. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  188. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  189. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  190. package/src/wirings/workflow/index.ts +5 -0
  191. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  192. package/src/wirings/workflow/pikku-workflow-service.ts +236 -0
  193. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  194. package/src/wirings/workflow/workflow.types.ts +4 -0
  195. package/tsconfig.tsbuildinfo +1 -1
  196. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  197. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  198. 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 pkgServices = pikkuState(capturedPkg, 'package', 'singletonServices');
251
- if (pkgServices) {
252
- services = pkgServices;
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
  }