@pikku/core 0.12.2 → 0.12.3
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 +18 -0
- package/dist/crypto-utils.d.ts +10 -0
- package/dist/crypto-utils.js +62 -0
- package/dist/function/function-runner.js +4 -4
- package/dist/function/functions.types.d.ts +26 -1
- package/dist/function/functions.types.js +16 -0
- package/dist/handle-error.d.ts +1 -1
- package/dist/handle-error.js +7 -3
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/pikku-state.js +3 -1
- package/dist/schema.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +8 -2
- package/dist/services/in-memory-workflow-service.js +28 -1
- package/dist/services/workflow-service.d.ts +8 -2
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/service-tests.d.ts +31 -0
- package/dist/testing/service-tests.js +598 -0
- package/dist/types/core.types.d.ts +9 -1
- package/dist/types/state.types.d.ts +4 -2
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +23 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-memory.js +6 -5
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
- package/dist/wirings/ai-agent/ai-agent-prepare.js +127 -47
- package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +109 -46
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +22 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -2
- package/dist/wirings/ai-agent/index.js +1 -2
- package/dist/wirings/channel/channel-handler.js +5 -1
- package/dist/wirings/channel/channel.types.d.ts +1 -7
- package/dist/wirings/channel/local/local-channel-handler.d.ts +1 -1
- package/dist/wirings/channel/local/local-channel-handler.js +5 -3
- package/dist/wirings/channel/local/local-channel-runner.js +14 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -1
- package/dist/wirings/cli/cli-runner.js +10 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -0
- package/dist/wirings/http/http-routes.js +0 -1
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +25 -3
- package/dist/wirings/http/http.types.d.ts +3 -11
- package/dist/wirings/http/index.d.ts +2 -1
- package/dist/wirings/http/index.js +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
- package/dist/wirings/http/web-request.d.ts +12 -0
- package/dist/wirings/http/web-request.js +85 -0
- package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
- package/dist/wirings/mcp/mcp-runner.js +13 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
- package/dist/wirings/rpc/rpc-runner.js +36 -14
- package/dist/wirings/rpc/rpc-types.d.ts +6 -1
- package/dist/wirings/rpc/wire-addon.d.ts +1 -0
- package/dist/wirings/rpc/wire-addon.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +1 -0
- package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
- package/dist/wirings/workflow/graph/graph-validation.js +175 -0
- package/dist/wirings/workflow/graph/index.d.ts +1 -0
- package/dist/wirings/workflow/graph/index.js +1 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
- package/dist/wirings/workflow/workflow.types.d.ts +10 -1
- package/package.json +4 -2
- package/src/crypto-utils.test.ts +99 -1
- package/src/crypto-utils.ts +114 -0
- package/src/function/function-runner.ts +5 -5
- package/src/function/functions.types.ts +38 -1
- package/src/handle-error.test.ts +65 -2
- package/src/handle-error.ts +8 -4
- package/src/index.ts +14 -0
- package/src/pikku-state.ts +3 -1
- package/src/schema.ts +1 -1
- package/src/services/in-memory-workflow-service.ts +45 -2
- package/src/services/workflow-service.ts +12 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/types/core.types.ts +9 -1
- package/src/types/state.types.ts +4 -2
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +7 -6
- package/src/wirings/ai-agent/ai-agent-prepare.ts +175 -61
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +182 -89
- package/src/wirings/ai-agent/ai-agent.types.ts +23 -1
- package/src/wirings/ai-agent/index.ts +1 -5
- package/src/wirings/channel/channel-handler.ts +11 -2
- package/src/wirings/channel/channel.types.ts +1 -7
- package/src/wirings/channel/local/local-channel-handler.ts +5 -3
- package/src/wirings/channel/local/local-channel-runner.ts +17 -7
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +12 -2
- package/src/wirings/cli/cli.types.ts +1 -0
- package/src/wirings/http/http-routes.ts +0 -1
- package/src/wirings/http/http-runner.ts +35 -10
- package/src/wirings/http/http.types.ts +3 -12
- package/src/wirings/http/index.ts +3 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +30 -12
- package/src/wirings/rpc/rpc-runner.ts +45 -24
- package/src/wirings/rpc/rpc-types.ts +6 -1
- package/src/wirings/rpc/wire-addon.ts +2 -1
- package/src/wirings/scheduler/scheduler-runner.ts +1 -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 +5 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
- package/src/wirings/workflow/workflow.types.ts +9 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -168
- package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
- package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -238
|
@@ -94,7 +94,12 @@ describe('streamAIAgent', () => {
|
|
|
94
94
|
{}
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
assert.deepEqual(updates, [
|
|
97
|
+
assert.deepEqual(updates, [
|
|
98
|
+
{
|
|
99
|
+
runId: 'run-1',
|
|
100
|
+
patch: { status: 'failed', errorMessage: 'stream failed' },
|
|
101
|
+
},
|
|
102
|
+
])
|
|
98
103
|
assert.deepEqual(
|
|
99
104
|
events.map((event) => event.type),
|
|
100
105
|
['step-start', 'error', 'done']
|
|
@@ -198,7 +203,7 @@ describe('streamAIAgent', () => {
|
|
|
198
203
|
pikkuState(null, 'rpc', 'meta')['tool-x'] = 'tool-x'
|
|
199
204
|
pikkuState(null, 'function', 'meta')['tool-x'] = {
|
|
200
205
|
description: 'test tool',
|
|
201
|
-
|
|
206
|
+
approvalRequired: true,
|
|
202
207
|
}
|
|
203
208
|
pikkuState(null, 'misc', 'schemas').set('ToolXInput', {
|
|
204
209
|
type: 'object',
|
|
@@ -260,6 +265,208 @@ describe('streamAIAgent', () => {
|
|
|
260
265
|
)
|
|
261
266
|
})
|
|
262
267
|
|
|
268
|
+
test('suspends run when multiple tool calls need approval (explicit policy)', async () => {
|
|
269
|
+
addTestAgent('multi-approval-agent')
|
|
270
|
+
|
|
271
|
+
const updates: Array<{ runId: string; patch: unknown }> = []
|
|
272
|
+
const events: AIStreamEvent[] = []
|
|
273
|
+
|
|
274
|
+
const mockServices = {
|
|
275
|
+
logger: {
|
|
276
|
+
info: () => {},
|
|
277
|
+
warn: () => {},
|
|
278
|
+
error: () => {},
|
|
279
|
+
debug: () => {},
|
|
280
|
+
},
|
|
281
|
+
aiAgentRunner: {
|
|
282
|
+
stream: async (): Promise<AIAgentStepResult> => {
|
|
283
|
+
// Simulate LLM returning 3 addTodo calls in one step
|
|
284
|
+
return makeStepResult({
|
|
285
|
+
toolCalls: [
|
|
286
|
+
{
|
|
287
|
+
toolCallId: 'tc-1',
|
|
288
|
+
toolName: 'todos__addTodo',
|
|
289
|
+
args: { title: 'todo 1' },
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
toolCallId: 'tc-2',
|
|
293
|
+
toolName: 'todos__addTodo',
|
|
294
|
+
args: { title: 'todo 2' },
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
toolCallId: 'tc-3',
|
|
298
|
+
toolName: 'todos__addTodo',
|
|
299
|
+
args: { title: 'todo 3' },
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
// No toolResults since tools have no execute (needsApproval)
|
|
303
|
+
})
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
aiRunState: {
|
|
307
|
+
createRun: async () => 'run-multi-approval',
|
|
308
|
+
updateRun: async (runId: string, patch: unknown) => {
|
|
309
|
+
updates.push({ runId, patch })
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
} as any
|
|
313
|
+
|
|
314
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
315
|
+
|
|
316
|
+
const agentName = 'multi-approval-agent'
|
|
317
|
+
pikkuState(null, 'agent', 'agentsMeta')[agentName].tools = [
|
|
318
|
+
'todos__addTodo',
|
|
319
|
+
]
|
|
320
|
+
pikkuState(null, 'rpc', 'meta')['todos__addTodo'] = 'addTodo'
|
|
321
|
+
pikkuState(null, 'function', 'meta')['addTodo'] = {
|
|
322
|
+
description: 'Add a todo',
|
|
323
|
+
approvalRequired: true,
|
|
324
|
+
inputSchemaName: 'AddTodoInput',
|
|
325
|
+
}
|
|
326
|
+
pikkuState(null, 'misc', 'schemas').set('AddTodoInput', {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: { title: { type: 'string' } },
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
await streamAIAgent(
|
|
332
|
+
agentName,
|
|
333
|
+
{
|
|
334
|
+
message: 'create 3 todos',
|
|
335
|
+
threadId: 'thread-multi-approval',
|
|
336
|
+
resourceId: 'resource-multi-approval',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
channelId: 'channel-multi-approval',
|
|
340
|
+
openingData: undefined,
|
|
341
|
+
state: 'open',
|
|
342
|
+
send: (event: AIStreamEvent) => {
|
|
343
|
+
events.push(event)
|
|
344
|
+
},
|
|
345
|
+
close: () => {},
|
|
346
|
+
},
|
|
347
|
+
{}
|
|
348
|
+
// No options → defaults to requiresToolApproval: 'explicit'
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
// Should suspend on first tool call
|
|
352
|
+
const approvalEvent = events.find(
|
|
353
|
+
(e) => e.type === 'approval-request'
|
|
354
|
+
) as any
|
|
355
|
+
assert.ok(approvalEvent, 'Should have an approval-request event')
|
|
356
|
+
assert.equal(approvalEvent.toolCallId, 'tc-1')
|
|
357
|
+
assert.equal(approvalEvent.toolName, 'todos__addTodo')
|
|
358
|
+
|
|
359
|
+
// Run should be suspended
|
|
360
|
+
assert.ok(
|
|
361
|
+
updates.some((u) => (u.patch as any).status === 'suspended'),
|
|
362
|
+
'Run should be suspended for approval'
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
assert.ok(events.some((e) => e.type === 'done'))
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test('suspends run for addon tools using namespaced resolution (explicit policy)', async () => {
|
|
369
|
+
addTestAgent('addon-approval-agent')
|
|
370
|
+
|
|
371
|
+
const updates: Array<{ runId: string; patch: unknown }> = []
|
|
372
|
+
const events: AIStreamEvent[] = []
|
|
373
|
+
|
|
374
|
+
const mockServices = {
|
|
375
|
+
logger: {
|
|
376
|
+
info: () => {},
|
|
377
|
+
warn: () => {},
|
|
378
|
+
error: () => {},
|
|
379
|
+
debug: () => {},
|
|
380
|
+
},
|
|
381
|
+
aiAgentRunner: {
|
|
382
|
+
stream: async (params: any): Promise<AIAgentStepResult> => {
|
|
383
|
+
// Verify the tool was built with needsApproval
|
|
384
|
+
const addTodoTool = params.tools.find(
|
|
385
|
+
(t: any) => t.name === 'todos__addTodo'
|
|
386
|
+
)
|
|
387
|
+
assert.ok(addTodoTool, 'Tool todos__addTodo should exist')
|
|
388
|
+
assert.equal(
|
|
389
|
+
addTodoTool.needsApproval,
|
|
390
|
+
true,
|
|
391
|
+
'Tool should have needsApproval=true'
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
// Simulate LLM calling the tool
|
|
395
|
+
return makeStepResult({
|
|
396
|
+
toolCalls: [
|
|
397
|
+
{
|
|
398
|
+
toolCallId: 'tc-1',
|
|
399
|
+
toolName: 'todos__addTodo',
|
|
400
|
+
args: { title: 'new todo' },
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
})
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
aiRunState: {
|
|
407
|
+
createRun: async () => 'run-addon-approval',
|
|
408
|
+
updateRun: async (runId: string, patch: unknown) => {
|
|
409
|
+
updates.push({ runId, patch })
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
} as any
|
|
413
|
+
|
|
414
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
415
|
+
|
|
416
|
+
// Register addon package
|
|
417
|
+
pikkuState(null, 'addons', 'packages').set('todos', {
|
|
418
|
+
package: '@test/addon-todos',
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Set up addon package's function metadata
|
|
422
|
+
pikkuState('@test/addon-todos', 'function', 'meta')['addTodo'] = {
|
|
423
|
+
description: 'Add a todo',
|
|
424
|
+
approvalRequired: true,
|
|
425
|
+
inputSchemaName: 'AddTodoInput',
|
|
426
|
+
}
|
|
427
|
+
pikkuState('@test/addon-todos', 'misc', 'schemas').set('AddTodoInput', {
|
|
428
|
+
type: 'object',
|
|
429
|
+
properties: { title: { type: 'string' } },
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// Set up agent with namespaced tool
|
|
433
|
+
const agentName = 'addon-approval-agent'
|
|
434
|
+
pikkuState(null, 'agent', 'agentsMeta')[agentName].tools = ['todos:addTodo']
|
|
435
|
+
|
|
436
|
+
await streamAIAgent(
|
|
437
|
+
agentName,
|
|
438
|
+
{
|
|
439
|
+
message: 'add a todo',
|
|
440
|
+
threadId: 'thread-addon-approval',
|
|
441
|
+
resourceId: 'resource-addon-approval',
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
channelId: 'channel-addon-approval',
|
|
445
|
+
openingData: undefined,
|
|
446
|
+
state: 'open',
|
|
447
|
+
send: (event: AIStreamEvent) => {
|
|
448
|
+
events.push(event)
|
|
449
|
+
},
|
|
450
|
+
close: () => {},
|
|
451
|
+
},
|
|
452
|
+
{}
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
// Should suspend on the tool call
|
|
456
|
+
const approvalEvent = events.find(
|
|
457
|
+
(e) => e.type === 'approval-request'
|
|
458
|
+
) as any
|
|
459
|
+
assert.ok(approvalEvent, 'Should have an approval-request event')
|
|
460
|
+
assert.equal(approvalEvent.toolCallId, 'tc-1')
|
|
461
|
+
assert.equal(approvalEvent.toolName, 'todos__addTodo')
|
|
462
|
+
|
|
463
|
+
// Run should be suspended
|
|
464
|
+
assert.ok(
|
|
465
|
+
updates.some((u) => (u.patch as any).status === 'suspended'),
|
|
466
|
+
'Run should be suspended for approval'
|
|
467
|
+
)
|
|
468
|
+
})
|
|
469
|
+
|
|
263
470
|
test('suspends with agent-call type when sub-agent returns approval sentinel', async () => {
|
|
264
471
|
addTestAgent('parent-agent')
|
|
265
472
|
|
|
@@ -39,10 +39,6 @@ import {
|
|
|
39
39
|
type StreamAIAgentOptions,
|
|
40
40
|
type StreamContext,
|
|
41
41
|
} from './ai-agent-prepare.js'
|
|
42
|
-
import {
|
|
43
|
-
createAssistantUIChannel,
|
|
44
|
-
parseAssistantUIInput,
|
|
45
|
-
} from './ai-agent-assistant-ui.js'
|
|
46
42
|
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
47
43
|
import type { AIRunStateService } from '../../services/ai-run-state-service.js'
|
|
48
44
|
import type { AIAgentRunnerService } from '../../services/ai-agent-runner-service.js'
|
|
@@ -238,7 +234,7 @@ type StepLoopParams = {
|
|
|
238
234
|
|
|
239
235
|
type StepLoopResult =
|
|
240
236
|
| { outcome: 'done' }
|
|
241
|
-
| { outcome: 'approval';
|
|
237
|
+
| { outcome: 'approval'; approvals: ToolApprovalRequired[] }
|
|
242
238
|
|
|
243
239
|
async function runStreamStepLoop(
|
|
244
240
|
params: StepLoopParams
|
|
@@ -291,13 +287,26 @@ async function runStreamStepLoop(
|
|
|
291
287
|
|
|
292
288
|
if (stepResult.toolCalls.length === 0) break
|
|
293
289
|
|
|
294
|
-
const
|
|
290
|
+
const approvalsNeeded = checkForApprovals(
|
|
295
291
|
stepResult,
|
|
296
292
|
runnerParams.tools,
|
|
297
293
|
runId
|
|
298
294
|
)
|
|
299
|
-
if (
|
|
300
|
-
|
|
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 }
|
|
301
310
|
}
|
|
302
311
|
|
|
303
312
|
appendStepMessages(runnerParams, stepResult)
|
|
@@ -306,16 +315,20 @@ async function runStreamStepLoop(
|
|
|
306
315
|
return { outcome: 'done' }
|
|
307
316
|
}
|
|
308
317
|
|
|
309
|
-
function
|
|
318
|
+
export function checkForApprovals(
|
|
310
319
|
stepResult: AIAgentStepResult,
|
|
311
320
|
tools: AIAgentRunnerParams['tools'],
|
|
312
321
|
runId: string
|
|
313
|
-
): ToolApprovalRequired
|
|
322
|
+
): ToolApprovalRequired[] {
|
|
323
|
+
const approvals: ToolApprovalRequired[] = []
|
|
314
324
|
for (const tc of stepResult.toolCalls) {
|
|
315
325
|
const toolDef = tools.find((t) => t.name === tc.toolName)
|
|
316
326
|
|
|
317
327
|
if (toolDef?.needsApproval) {
|
|
318
|
-
|
|
328
|
+
approvals.push(
|
|
329
|
+
new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args)
|
|
330
|
+
)
|
|
331
|
+
continue
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
const tr = stepResult.toolResults.find(
|
|
@@ -333,22 +346,46 @@ function checkForApproval(
|
|
|
333
346
|
displayToolName?: string
|
|
334
347
|
displayArgs?: unknown
|
|
335
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
|
+
)
|
|
336
382
|
}
|
|
337
|
-
return new ToolApprovalRequired(
|
|
338
|
-
tc.toolCallId,
|
|
339
|
-
r.toolName,
|
|
340
|
-
r.args,
|
|
341
|
-
r.reason,
|
|
342
|
-
r.displayToolName,
|
|
343
|
-
r.displayArgs,
|
|
344
|
-
r.agentRunId
|
|
345
|
-
)
|
|
346
383
|
}
|
|
347
384
|
}
|
|
348
|
-
return
|
|
385
|
+
return approvals
|
|
349
386
|
}
|
|
350
387
|
|
|
351
|
-
function appendStepMessages(
|
|
388
|
+
export function appendStepMessages(
|
|
352
389
|
runnerParams: AIAgentRunnerParams,
|
|
353
390
|
stepResult: AIAgentStepResult
|
|
354
391
|
): void {
|
|
@@ -384,8 +421,8 @@ function appendStepMessages(
|
|
|
384
421
|
}
|
|
385
422
|
}
|
|
386
423
|
|
|
387
|
-
function
|
|
388
|
-
|
|
424
|
+
function handleApprovals(
|
|
425
|
+
approvals: ToolApprovalRequired[],
|
|
389
426
|
runId: string,
|
|
390
427
|
channel: AIStreamChannel,
|
|
391
428
|
aiRunState: AIRunStateService,
|
|
@@ -394,37 +431,41 @@ function handleApproval(
|
|
|
394
431
|
return (async () => {
|
|
395
432
|
await persistingChannel.flush()
|
|
396
433
|
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
)
|
|
412
451
|
|
|
413
452
|
await aiRunState.updateRun(runId, {
|
|
414
453
|
status: 'suspended',
|
|
415
454
|
suspendReason: 'approval',
|
|
416
|
-
pendingApprovals
|
|
455
|
+
pendingApprovals,
|
|
417
456
|
})
|
|
418
457
|
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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)
|
|
426
468
|
}
|
|
427
|
-
channel.send(approvalEvent as any)
|
|
428
469
|
channel.send({ type: 'done' })
|
|
429
470
|
channel.close()
|
|
430
471
|
})()
|
|
@@ -432,34 +473,24 @@ function handleApproval(
|
|
|
432
473
|
|
|
433
474
|
export async function streamAIAgent(
|
|
434
475
|
agentName: string,
|
|
435
|
-
input:
|
|
436
|
-
|
|
437
|
-
|
|
476
|
+
input: {
|
|
477
|
+
message: string
|
|
478
|
+
threadId: string
|
|
479
|
+
resourceId: string
|
|
480
|
+
model?: string
|
|
481
|
+
temperature?: number
|
|
482
|
+
},
|
|
438
483
|
channel: AIStreamChannel,
|
|
439
484
|
params: RunAIAgentParams,
|
|
440
485
|
agentSessionMap?: Map<string, string>,
|
|
441
486
|
options?: StreamAIAgentOptions
|
|
442
|
-
): Promise<
|
|
487
|
+
): Promise<string> {
|
|
443
488
|
const sessionMap = agentSessionMap ?? new Map<string, string>()
|
|
444
489
|
|
|
445
|
-
const
|
|
446
|
-
const inputRecord = input as Record<string, unknown>
|
|
447
|
-
const useUIMessageStream =
|
|
448
|
-
resolvedAgentForProtocol.protocol === 'ui-message-stream' ||
|
|
449
|
-
Array.isArray(inputRecord.messages)
|
|
450
|
-
let normalizedInput: { message: string; threadId: string; resourceId: string }
|
|
451
|
-
if (useUIMessageStream) {
|
|
452
|
-
normalizedInput = parseAssistantUIInput(inputRecord)
|
|
453
|
-
channel = createAssistantUIChannel(channel)
|
|
454
|
-
} else {
|
|
455
|
-
normalizedInput = input as {
|
|
456
|
-
message: string
|
|
457
|
-
threadId: string
|
|
458
|
-
resourceId: string
|
|
459
|
-
}
|
|
460
|
-
}
|
|
490
|
+
const normalizedInput = input
|
|
461
491
|
|
|
462
492
|
const streamContext: StreamContext = { channel, options }
|
|
493
|
+
// delegateState is attached after prepareAgentRun resolves the agent config
|
|
463
494
|
|
|
464
495
|
const {
|
|
465
496
|
agent,
|
|
@@ -502,7 +533,7 @@ export async function streamAIAgent(
|
|
|
502
533
|
})
|
|
503
534
|
channel.send({ type: 'suspended', reason: 'rpc-missing', missingRpcs })
|
|
504
535
|
channel.send({ type: 'done' })
|
|
505
|
-
return
|
|
536
|
+
return ''
|
|
506
537
|
}
|
|
507
538
|
|
|
508
539
|
const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
|
|
@@ -580,8 +611,32 @@ export async function streamAIAgent(
|
|
|
580
611
|
).channel as AIStreamChannel)
|
|
581
612
|
: channel
|
|
582
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
|
+
|
|
583
638
|
const persistingChannel = createPersistingChannel(
|
|
584
|
-
|
|
639
|
+
outputChannel,
|
|
585
640
|
storage,
|
|
586
641
|
threadId
|
|
587
642
|
)
|
|
@@ -599,14 +654,14 @@ export async function streamAIAgent(
|
|
|
599
654
|
})
|
|
600
655
|
|
|
601
656
|
if (loopResult.outcome === 'approval') {
|
|
602
|
-
await
|
|
603
|
-
loopResult.
|
|
657
|
+
await handleApprovals(
|
|
658
|
+
loopResult.approvals,
|
|
604
659
|
runId,
|
|
605
660
|
channel,
|
|
606
661
|
aiRunState,
|
|
607
662
|
persistingChannel
|
|
608
663
|
)
|
|
609
|
-
return
|
|
664
|
+
return persistingChannel.fullText
|
|
610
665
|
}
|
|
611
666
|
|
|
612
667
|
await postStreamCleanup(
|
|
@@ -624,6 +679,7 @@ export async function streamAIAgent(
|
|
|
624
679
|
|
|
625
680
|
channel.send({ type: 'done' })
|
|
626
681
|
channel.close()
|
|
682
|
+
return persistingChannel.fullText
|
|
627
683
|
} catch (err) {
|
|
628
684
|
for (const mw of aiMiddlewares) {
|
|
629
685
|
if (mw.onError) {
|
|
@@ -638,13 +694,17 @@ export async function streamAIAgent(
|
|
|
638
694
|
}
|
|
639
695
|
}
|
|
640
696
|
}
|
|
641
|
-
await aiRunState.updateRun(runId, {
|
|
697
|
+
await aiRunState.updateRun(runId, {
|
|
698
|
+
status: 'failed',
|
|
699
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
700
|
+
})
|
|
642
701
|
channel.send({
|
|
643
702
|
type: 'error',
|
|
644
703
|
message: err instanceof Error ? err.message : String(err),
|
|
645
704
|
})
|
|
646
705
|
channel.send({ type: 'done' })
|
|
647
706
|
channel.close()
|
|
707
|
+
return persistingChannel.fullText
|
|
648
708
|
}
|
|
649
709
|
}
|
|
650
710
|
|
|
@@ -696,7 +756,8 @@ export async function resumeAIAgent(
|
|
|
696
756
|
await aiRunState.updateRun(pending.agentRunId, { status: 'failed' })
|
|
697
757
|
}
|
|
698
758
|
|
|
699
|
-
const denialResult =
|
|
759
|
+
const denialResult =
|
|
760
|
+
'The user explicitly declined this action. Inform them that it was declined and do not retry.'
|
|
700
761
|
|
|
701
762
|
if (storage) {
|
|
702
763
|
await storage.saveMessages(run.threadId, [
|
|
@@ -718,6 +779,17 @@ export async function resumeAIAgent(
|
|
|
718
779
|
])
|
|
719
780
|
}
|
|
720
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
|
+
|
|
721
793
|
await aiRunState.updateRun(run.runId, { status: 'running' })
|
|
722
794
|
|
|
723
795
|
await continueAfterToolResult(
|
|
@@ -741,7 +813,9 @@ export async function resumeAIAgent(
|
|
|
741
813
|
if (!subRun) {
|
|
742
814
|
throw new Error(`Sub-agent run not found: ${pending.agentRunId}`)
|
|
743
815
|
}
|
|
744
|
-
const subPending =
|
|
816
|
+
const subPending =
|
|
817
|
+
subRun.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId) ??
|
|
818
|
+
subRun.pendingApprovals?.[0]
|
|
745
819
|
if (!subPending) {
|
|
746
820
|
throw new Error(
|
|
747
821
|
`No pending approval on sub-agent run ${pending.agentRunId}`
|
|
@@ -797,7 +871,7 @@ export async function resumeAIAgent(
|
|
|
797
871
|
}
|
|
798
872
|
const aiMiddlewaresForResume: PikkuAIMiddlewareHooks[] =
|
|
799
873
|
agent.aiMiddleware ?? []
|
|
800
|
-
const { tools } = buildToolDefs(
|
|
874
|
+
const { tools } = await buildToolDefs(
|
|
801
875
|
params,
|
|
802
876
|
new Map<string, string>(),
|
|
803
877
|
run.resourceId,
|
|
@@ -818,10 +892,12 @@ export async function resumeAIAgent(
|
|
|
818
892
|
typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args
|
|
819
893
|
|
|
820
894
|
let toolResult: unknown
|
|
895
|
+
let isError = false
|
|
821
896
|
try {
|
|
822
897
|
toolResult = await matchingTool.execute(toolArgs)
|
|
823
898
|
} catch (execErr) {
|
|
824
899
|
toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`
|
|
900
|
+
isError = true
|
|
825
901
|
}
|
|
826
902
|
|
|
827
903
|
const resultStr =
|
|
@@ -848,9 +924,21 @@ export async function resumeAIAgent(
|
|
|
848
924
|
toolCallId: input.toolCallId,
|
|
849
925
|
toolName: pending.toolName,
|
|
850
926
|
result: toolResult,
|
|
927
|
+
...(isError ? { isError: true } : {}),
|
|
851
928
|
})
|
|
852
929
|
}
|
|
853
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
|
+
|
|
854
942
|
await aiRunState.updateRun(run.runId, { status: 'running' })
|
|
855
943
|
|
|
856
944
|
await continueAfterToolResult(
|
|
@@ -906,7 +994,7 @@ async function continueAfterToolResult(
|
|
|
906
994
|
const allMessages = [...contextMessages, ...messages]
|
|
907
995
|
const trimmedMessages = trimMessages(allMessages)
|
|
908
996
|
|
|
909
|
-
const instructions = buildInstructions(resolvedName, packageName)
|
|
997
|
+
const instructions = await buildInstructions(resolvedName, packageName)
|
|
910
998
|
|
|
911
999
|
const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
|
|
912
1000
|
let modifiedMessages = trimmedMessages
|
|
@@ -971,14 +1059,16 @@ async function continueAfterToolResult(
|
|
|
971
1059
|
)
|
|
972
1060
|
|
|
973
1061
|
const streamContext: StreamContext = { channel, options }
|
|
974
|
-
const resumeTools =
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
+
)
|
|
982
1072
|
).tools
|
|
983
1073
|
|
|
984
1074
|
const resolved = resolveModelConfig(resolvedName, agent)
|
|
@@ -1010,8 +1100,8 @@ async function continueAfterToolResult(
|
|
|
1010
1100
|
})
|
|
1011
1101
|
|
|
1012
1102
|
if (loopResult.outcome === 'approval') {
|
|
1013
|
-
await
|
|
1014
|
-
loopResult.
|
|
1103
|
+
await handleApprovals(
|
|
1104
|
+
loopResult.approvals,
|
|
1015
1105
|
run.runId,
|
|
1016
1106
|
channel,
|
|
1017
1107
|
aiRunState,
|
|
@@ -1049,7 +1139,10 @@ async function continueAfterToolResult(
|
|
|
1049
1139
|
}
|
|
1050
1140
|
}
|
|
1051
1141
|
}
|
|
1052
|
-
await aiRunState.updateRun(run.runId, {
|
|
1142
|
+
await aiRunState.updateRun(run.runId, {
|
|
1143
|
+
status: 'failed',
|
|
1144
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1145
|
+
})
|
|
1053
1146
|
channel.send({
|
|
1054
1147
|
type: 'error',
|
|
1055
1148
|
message: err instanceof Error ? err.message : String(err),
|