@pikku/core 0.12.80 → 0.12.83
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 +345 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +2 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +3 -3
- package/dist/pikku-state.js +4 -0
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +10 -0
- package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
- package/dist/services/in-memory-ai-run-state-service.js +9 -0
- package/dist/services/index.d.ts +15 -16
- package/dist/services/index.js +5 -5
- package/dist/services/meta-service.d.ts +2 -1
- package/dist/services/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
- package/dist/types/core.types.d.ts +2 -3
- package/dist/types/state.types.d.ts +19 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
- package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- package/dist/wirings/channel/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +13 -1
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/index.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/index.d.ts +1 -2
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
- package/dist/wirings/mcp/mcp-runner.js +18 -5
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +8 -0
- package/dist/wirings/rpc/addon-runner.js +31 -3
- package/dist/wirings/rpc/rpc-runner.js +4 -0
- package/dist/wirings/rpc/rpc-types.d.ts +8 -0
- package/dist/wirings/rpc/wire-addon.d.ts +25 -0
- package/dist/wirings/rpc/wire-addon.js +8 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/trigger/index.d.ts +1 -1
- package/dist/wirings/virtual-user/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
- package/dist/wirings/workflow/feature.d.ts +2 -1
- package/dist/wirings/workflow/index.d.ts +5 -16
- package/dist/wirings/workflow/index.js +1 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +48 -13
- package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/scenario.types.d.ts +37 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
- package/dist/wirings/workflow/workflow-approval.js +65 -2
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
- package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
- package/dist/wirings/workflow/workflow.types.d.ts +2 -37
- package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
- package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
- package/knowledge/decisions/internals/index.md +30 -3
- package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
- package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
- package/knowledge/decisions/security/index.md +3 -1
- package/knowledge/questions/index.md +1 -1
- package/package.json +3 -2
- package/scripts/generate-api-report.mts +143 -18
- package/src/api-report.test.ts +2 -2
- package/src/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +52 -0
- package/src/function/function-runner.ts +5 -9
- package/src/function/index.ts +0 -2
- package/src/index.ts +0 -35
- package/src/pikku-state.ts +5 -0
- package/src/public-surface.json +81 -118
- package/src/services/ai-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +11 -0
- package/src/services/in-memory-ai-run-state-service.ts +13 -0
- package/src/services/index.ts +7 -58
- package/src/services/meta-service.ts +2 -4
- package/src/services/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
- package/src/types/core.types.ts +4 -7
- package/src/types/state.types.ts +21 -1
- package/src/wirings/actor-flow/index.ts +0 -3
- package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- package/src/wirings/channel/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +8 -1
- package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +18 -1
- package/src/wirings/credential/index.ts +0 -1
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/index.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +35 -5
- package/src/wirings/persona/index.ts +0 -8
- package/src/wirings/queue/index.ts +0 -14
- package/src/wirings/rpc/addon-runner.ts +62 -3
- package/src/wirings/rpc/addon-secrets.test.ts +391 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +4 -0
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/rpc/wire-addon.ts +33 -0
- package/src/wirings/scheduler/index.ts +0 -1
- package/src/wirings/trigger/index.ts +0 -1
- package/src/wirings/virtual-user/index.ts +0 -16
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
- package/src/wirings/workflow/feature.ts +2 -5
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -68
- package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/scenario.types.ts +63 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +113 -9
- package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
- package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
- package/src/wirings/workflow/workflow.types.ts +1 -63
- package/src/wirings-stay-decoupled.test.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- package/dist/wirings/workflow/graph/index.js +0 -4
- /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
|
@@ -49,6 +49,11 @@ import {
|
|
|
49
49
|
notifyAfterStep,
|
|
50
50
|
toAccumulatedStep,
|
|
51
51
|
} from './ai-agent-turn.js'
|
|
52
|
+
import {
|
|
53
|
+
applyOutputMiddleware,
|
|
54
|
+
finalizeAgentRun,
|
|
55
|
+
lastUserMessageText,
|
|
56
|
+
} from './ai-agent-finalize.js'
|
|
52
57
|
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
53
58
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
54
59
|
import { randomUUID } from './ai-agent-utils.js'
|
|
@@ -303,20 +308,13 @@ export async function runAIAgent(
|
|
|
303
308
|
steps: accumulatedSteps,
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
messages: outputMessages,
|
|
314
|
-
usage: totalUsage,
|
|
315
|
-
})
|
|
316
|
-
outputText = modResult.text
|
|
317
|
-
outputMessages = modResult.messages
|
|
318
|
-
}
|
|
319
|
-
}
|
|
311
|
+
const { text: outputText, steps: outputSteps } =
|
|
312
|
+
await applyOutputMiddleware(aiMiddlewares, singletonServices, {
|
|
313
|
+
text: finalText,
|
|
314
|
+
messages: runnerParams.messages,
|
|
315
|
+
steps: result.steps,
|
|
316
|
+
usage: totalUsage,
|
|
317
|
+
})
|
|
320
318
|
|
|
321
319
|
await saveMessages(
|
|
322
320
|
storage,
|
|
@@ -325,14 +323,20 @@ export async function runAIAgent(
|
|
|
325
323
|
memoryConfig,
|
|
326
324
|
persistedUserMessage,
|
|
327
325
|
{
|
|
328
|
-
...result,
|
|
329
326
|
text: outputText,
|
|
327
|
+
steps: outputSteps,
|
|
330
328
|
uiSpec: structuredOutput.uiSpec,
|
|
331
329
|
}
|
|
332
330
|
)
|
|
333
331
|
|
|
334
|
-
await aiRunState
|
|
335
|
-
|
|
332
|
+
await finalizeAgentRun(aiRunState, {
|
|
333
|
+
runId,
|
|
334
|
+
agentName,
|
|
335
|
+
threadId,
|
|
336
|
+
resourceId: input.resourceId,
|
|
337
|
+
input: lastUserMessageText(runnerParams.messages),
|
|
338
|
+
text: outputText,
|
|
339
|
+
steps: outputSteps,
|
|
336
340
|
usage: { ...totalUsage, model: agent.model },
|
|
337
341
|
})
|
|
338
342
|
|
|
@@ -341,7 +345,7 @@ export async function runAIAgent(
|
|
|
341
345
|
text: outputText,
|
|
342
346
|
object: finalObject,
|
|
343
347
|
threadId,
|
|
344
|
-
steps:
|
|
348
|
+
steps: outputSteps,
|
|
345
349
|
usage: totalUsage,
|
|
346
350
|
}
|
|
347
351
|
} catch (error) {
|
|
@@ -462,6 +466,7 @@ export async function resumeAIAgentSync(
|
|
|
462
466
|
toolName: string
|
|
463
467
|
args: any
|
|
464
468
|
result: string
|
|
469
|
+
error?: string
|
|
465
470
|
}[] = []
|
|
466
471
|
|
|
467
472
|
for (const pending of savedPendingApprovals) {
|
|
@@ -471,6 +476,7 @@ export async function resumeAIAgentSync(
|
|
|
471
476
|
if (!claimedIds.has(toolCallId)) continue
|
|
472
477
|
|
|
473
478
|
let resultStr: string
|
|
479
|
+
let toolError: string | undefined
|
|
474
480
|
|
|
475
481
|
if (rejectedIds.has(toolCallId)) {
|
|
476
482
|
resultStr =
|
|
@@ -497,7 +503,8 @@ export async function resumeAIAgentSync(
|
|
|
497
503
|
if (err?.payload?.error === 'missing_credential') {
|
|
498
504
|
resultStr = JSON.stringify(err.payload)
|
|
499
505
|
} else {
|
|
500
|
-
|
|
506
|
+
toolError = err instanceof Error ? err.message : String(err)
|
|
507
|
+
resultStr = `Error: ${toolError}`
|
|
501
508
|
}
|
|
502
509
|
}
|
|
503
510
|
} else {
|
|
@@ -512,6 +519,7 @@ export async function resumeAIAgentSync(
|
|
|
512
519
|
? JSON.parse(pending.args)
|
|
513
520
|
: pending.args,
|
|
514
521
|
result: resultStr,
|
|
522
|
+
...(toolError ? { error: toolError } : {}),
|
|
515
523
|
})
|
|
516
524
|
}
|
|
517
525
|
|
|
@@ -541,7 +549,21 @@ export async function resumeAIAgentSync(
|
|
|
541
549
|
memoryConfig,
|
|
542
550
|
agentRunner,
|
|
543
551
|
params,
|
|
544
|
-
aiRunState
|
|
552
|
+
aiRunState,
|
|
553
|
+
// The approved tools were executed here, before the model was re-entered,
|
|
554
|
+
// so they belong to the run's step record — otherwise a tool that failed
|
|
555
|
+
// after approval leaves no trace on the run at all.
|
|
556
|
+
toolCallMessages.length > 0
|
|
557
|
+
? {
|
|
558
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
559
|
+
toolCalls: toolCallMessages.map((tc) => ({
|
|
560
|
+
name: tc.toolName,
|
|
561
|
+
args: (tc.args ?? {}) as Record<string, unknown>,
|
|
562
|
+
result: tc.result,
|
|
563
|
+
...(tc.error ? { error: tc.error } : {}),
|
|
564
|
+
})),
|
|
565
|
+
}
|
|
566
|
+
: undefined
|
|
545
567
|
)
|
|
546
568
|
}
|
|
547
569
|
|
|
@@ -554,7 +576,8 @@ async function continueAfterToolResultSync(
|
|
|
554
576
|
memoryConfig: AIAgentMemoryConfig | undefined,
|
|
555
577
|
agentRunner: AIAgentRunnerService,
|
|
556
578
|
params: RunAIAgentParams,
|
|
557
|
-
aiRunState: AIRunStateService
|
|
579
|
+
aiRunState: AIRunStateService,
|
|
580
|
+
resumedToolStep?: AIAgentStep
|
|
558
581
|
): Promise<AIAgentOutput> {
|
|
559
582
|
const singletonServices = getSingletonServices()
|
|
560
583
|
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
|
|
@@ -639,6 +662,13 @@ async function continueAfterToolResultSync(
|
|
|
639
662
|
runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle)
|
|
640
663
|
|
|
641
664
|
try {
|
|
665
|
+
// Kept out of `accumulatedSteps` deliberately: that array drives
|
|
666
|
+
// `saveMessages`, and the approved tool's messages were already written to
|
|
667
|
+
// the thread before the model was re-entered. It belongs to the run's step
|
|
668
|
+
// record, not to persistence.
|
|
669
|
+
const withResumedStep = (steps: AIAgentStep[]): AIAgentStep[] =>
|
|
670
|
+
resumedToolStep ? [resumedToolStep, ...steps] : steps
|
|
671
|
+
|
|
642
672
|
const accumulatedSteps: AIAgentStep[] = []
|
|
643
673
|
const totalUsage = { inputTokens: 0, outputTokens: 0 }
|
|
644
674
|
let lastStepResult: AIAgentStepResult | null = null
|
|
@@ -717,7 +747,7 @@ async function continueAfterToolResultSync(
|
|
|
717
747
|
runId: run.runId,
|
|
718
748
|
text: suspendedText,
|
|
719
749
|
threadId: run.threadId,
|
|
720
|
-
steps: accumulatedSteps,
|
|
750
|
+
steps: withResumedStep(accumulatedSteps),
|
|
721
751
|
usage: totalUsage,
|
|
722
752
|
status: 'suspended',
|
|
723
753
|
pendingApprovals: approvalsNeeded.map((a) => ({
|
|
@@ -741,20 +771,13 @@ async function continueAfterToolResultSync(
|
|
|
741
771
|
steps: accumulatedSteps,
|
|
742
772
|
}
|
|
743
773
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
messages: outputMessages,
|
|
752
|
-
usage: totalUsage,
|
|
753
|
-
})
|
|
754
|
-
outputText = modResult.text
|
|
755
|
-
outputMessages = modResult.messages
|
|
756
|
-
}
|
|
757
|
-
}
|
|
774
|
+
const { text: outputText, steps: outputSteps } =
|
|
775
|
+
await applyOutputMiddleware(aiMiddlewares, singletonServices, {
|
|
776
|
+
text: finalText,
|
|
777
|
+
messages: runnerParams.messages,
|
|
778
|
+
steps: withResumedStep(result.steps),
|
|
779
|
+
usage: totalUsage,
|
|
780
|
+
})
|
|
758
781
|
|
|
759
782
|
await saveMessages(
|
|
760
783
|
storage,
|
|
@@ -763,13 +786,21 @@ async function continueAfterToolResultSync(
|
|
|
763
786
|
memoryConfig,
|
|
764
787
|
null,
|
|
765
788
|
{
|
|
766
|
-
...result,
|
|
767
789
|
text: outputText,
|
|
790
|
+
// The approved tool's messages were written before the model was
|
|
791
|
+
// re-entered, so only the steps this leg generated are persisted here.
|
|
792
|
+
steps: accumulatedSteps,
|
|
768
793
|
}
|
|
769
794
|
)
|
|
770
795
|
|
|
771
|
-
await aiRunState
|
|
772
|
-
|
|
796
|
+
await finalizeAgentRun(aiRunState, {
|
|
797
|
+
runId: run.runId,
|
|
798
|
+
agentName: resolvedName,
|
|
799
|
+
threadId: run.threadId,
|
|
800
|
+
resourceId: run.resourceId,
|
|
801
|
+
input: lastUserMessageText(runnerParams.messages),
|
|
802
|
+
text: outputText,
|
|
803
|
+
steps: outputSteps,
|
|
773
804
|
usage: { ...totalUsage, model: agent.model },
|
|
774
805
|
})
|
|
775
806
|
|
|
@@ -778,7 +809,7 @@ async function continueAfterToolResultSync(
|
|
|
778
809
|
text: outputText,
|
|
779
810
|
object: finalObject,
|
|
780
811
|
threadId: run.threadId,
|
|
781
|
-
steps:
|
|
812
|
+
steps: outputSteps,
|
|
782
813
|
usage: totalUsage,
|
|
783
814
|
}
|
|
784
815
|
} catch (error) {
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { beforeEach, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { resetPikkuState, pikkuState } from '../../pikku-state.js'
|
|
5
|
+
import { streamAIAgent } from './ai-agent-stream.js'
|
|
6
|
+
import type { CoreAIAgent, PikkuAIMiddlewareHooks } from './ai-agent.types.js'
|
|
7
|
+
import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resetPikkuState()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const addTestAgent = (agentName: string) => {
|
|
14
|
+
const agent: CoreAIAgent = {
|
|
15
|
+
name: agentName,
|
|
16
|
+
description: 'test agent',
|
|
17
|
+
instructions: 'be helpful',
|
|
18
|
+
model: 'test/test-model',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pikkuState(null, 'agent', 'agentsMeta')[agentName] = {
|
|
22
|
+
...agent,
|
|
23
|
+
inputSchema: null,
|
|
24
|
+
outputSchema: null,
|
|
25
|
+
workingMemorySchema: null,
|
|
26
|
+
}
|
|
27
|
+
pikkuState(null, 'agent', 'agents').set(agentName, agent)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const makeStepResult = (
|
|
31
|
+
overrides?: Partial<AIAgentStepResult>
|
|
32
|
+
): AIAgentStepResult => ({
|
|
33
|
+
text: '',
|
|
34
|
+
toolCalls: [],
|
|
35
|
+
toolResults: [],
|
|
36
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
37
|
+
finishReason: 'stop',
|
|
38
|
+
...overrides,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('streamAIAgent output hooks', () => {
|
|
42
|
+
test('does not run modifyOutput on a streamed run, and warns the hook it is inert there', async () => {
|
|
43
|
+
addTestAgent('stream-modify-output-agent')
|
|
44
|
+
|
|
45
|
+
const warnings: unknown[][] = []
|
|
46
|
+
const modifyOutputCalls: unknown[] = []
|
|
47
|
+
const sideEffects: string[] = []
|
|
48
|
+
|
|
49
|
+
const middleware: PikkuAIMiddlewareHooks = {
|
|
50
|
+
modifyOutput: async (_services, ctx) => {
|
|
51
|
+
modifyOutputCalls.push(ctx)
|
|
52
|
+
sideEffects.push(ctx.text)
|
|
53
|
+
return { text: `${ctx.text} [redacted]`, messages: ctx.messages }
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
const agent = pikkuState(null, 'agent', 'agents').get(
|
|
57
|
+
'stream-modify-output-agent'
|
|
58
|
+
)!
|
|
59
|
+
agent.aiMiddleware = [middleware] as any
|
|
60
|
+
pikkuState(null, 'agent', 'agents').set('stream-modify-output-agent', agent)
|
|
61
|
+
|
|
62
|
+
const mockServices = {
|
|
63
|
+
logger: {
|
|
64
|
+
info: () => {},
|
|
65
|
+
warn: (...args: unknown[]) => warnings.push(args),
|
|
66
|
+
error: () => {},
|
|
67
|
+
debug: () => {},
|
|
68
|
+
},
|
|
69
|
+
aiAgentRunner: {
|
|
70
|
+
stream: async (_params: any, channel: any) => {
|
|
71
|
+
channel.send({ type: 'text-delta', text: 'Hello' })
|
|
72
|
+
return makeStepResult({ text: 'Hello', finishReason: 'stop' })
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
aiRunState: {
|
|
76
|
+
createRun: async () => 'run-modify-output',
|
|
77
|
+
updateRun: async () => {},
|
|
78
|
+
},
|
|
79
|
+
} as any
|
|
80
|
+
|
|
81
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
82
|
+
|
|
83
|
+
const result = await streamAIAgent(
|
|
84
|
+
'stream-modify-output-agent',
|
|
85
|
+
{
|
|
86
|
+
message: 'hello',
|
|
87
|
+
threadId: 'thread-modify-output',
|
|
88
|
+
resourceId: 'resource-modify-output',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
channelId: 'channel-modify-output',
|
|
92
|
+
openingData: undefined,
|
|
93
|
+
state: 'open',
|
|
94
|
+
send: () => {},
|
|
95
|
+
close: () => {},
|
|
96
|
+
},
|
|
97
|
+
{}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// It does not run at all — nothing on this path could act on what it
|
|
101
|
+
// returns, and the one hook that used to rely on the side effect (working
|
|
102
|
+
// memory) now persists from its own modifyOutputStream.
|
|
103
|
+
assert.equal(modifyOutputCalls.length, 0)
|
|
104
|
+
assert.deepEqual(sideEffects, [])
|
|
105
|
+
assert.equal(result, 'Hello')
|
|
106
|
+
|
|
107
|
+
// And the author of that hook has to be told, or the gap is silent.
|
|
108
|
+
assert.equal(
|
|
109
|
+
warnings.filter((args) =>
|
|
110
|
+
args.some(
|
|
111
|
+
(arg) =>
|
|
112
|
+
typeof arg === 'string' &&
|
|
113
|
+
arg.includes('modifyOutput') &&
|
|
114
|
+
arg.includes('stream-modify-output-agent')
|
|
115
|
+
)
|
|
116
|
+
).length,
|
|
117
|
+
1
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('persists working memory from a streamed run', async () => {
|
|
122
|
+
addTestAgent('stream-working-memory-agent')
|
|
123
|
+
|
|
124
|
+
const savedWorkingMemory: unknown[] = []
|
|
125
|
+
|
|
126
|
+
const agent = pikkuState(null, 'agent', 'agents').get(
|
|
127
|
+
'stream-working-memory-agent'
|
|
128
|
+
)!
|
|
129
|
+
agent.memory = { workingMemory: true } as any
|
|
130
|
+
pikkuState(null, 'agent', 'agents').set(
|
|
131
|
+
'stream-working-memory-agent',
|
|
132
|
+
agent
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const mockServices = {
|
|
136
|
+
logger: {
|
|
137
|
+
info: () => {},
|
|
138
|
+
warn: () => {},
|
|
139
|
+
error: () => {},
|
|
140
|
+
debug: () => {},
|
|
141
|
+
},
|
|
142
|
+
aiAgentRunner: {
|
|
143
|
+
stream: async (_params: any, channel: any) => {
|
|
144
|
+
channel.send({
|
|
145
|
+
type: 'text-delta',
|
|
146
|
+
text: 'Noted <working_memory>{"city":"Berlin"}</working_memory>',
|
|
147
|
+
})
|
|
148
|
+
return makeStepResult({ text: 'Noted', finishReason: 'stop' })
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
aiRunState: {
|
|
152
|
+
createRun: async () => 'run-working-memory',
|
|
153
|
+
updateRun: async () => {},
|
|
154
|
+
},
|
|
155
|
+
aiStorage: {
|
|
156
|
+
createThread: async () => {},
|
|
157
|
+
getMessages: async () => [],
|
|
158
|
+
saveMessages: async () => {},
|
|
159
|
+
getWorkingMemory: async () => ({}),
|
|
160
|
+
saveWorkingMemory: async (
|
|
161
|
+
threadId: string,
|
|
162
|
+
scope: string,
|
|
163
|
+
value: unknown
|
|
164
|
+
) => {
|
|
165
|
+
savedWorkingMemory.push({ threadId, scope, value })
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
} as any
|
|
169
|
+
|
|
170
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
171
|
+
|
|
172
|
+
await streamAIAgent(
|
|
173
|
+
'stream-working-memory-agent',
|
|
174
|
+
{
|
|
175
|
+
message: 'remember I live in Berlin',
|
|
176
|
+
threadId: 'thread-working-memory',
|
|
177
|
+
resourceId: 'resource-working-memory',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
channelId: 'channel-working-memory',
|
|
181
|
+
openingData: undefined,
|
|
182
|
+
state: 'open',
|
|
183
|
+
send: () => {},
|
|
184
|
+
close: () => {},
|
|
185
|
+
},
|
|
186
|
+
{}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// The block never reaches modifyOutput on this path: the middleware's own
|
|
190
|
+
// stream hook strips it before the persisting channel accumulates the text.
|
|
191
|
+
// Persisting has to happen from the stream hook, where the raw text is.
|
|
192
|
+
assert.deepEqual(savedWorkingMemory, [
|
|
193
|
+
{
|
|
194
|
+
threadId: 'thread-working-memory',
|
|
195
|
+
scope: 'thread',
|
|
196
|
+
value: { city: 'Berlin' },
|
|
197
|
+
},
|
|
198
|
+
])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('a failing tool on a streamed run is persisted as a failure, not as text that reads like one', async () => {
|
|
202
|
+
addTestAgent('stream-tool-error-agent')
|
|
203
|
+
|
|
204
|
+
const savedMessages: any[] = []
|
|
205
|
+
|
|
206
|
+
const mockServices = {
|
|
207
|
+
logger: {
|
|
208
|
+
info: () => {},
|
|
209
|
+
warn: () => {},
|
|
210
|
+
error: () => {},
|
|
211
|
+
debug: () => {},
|
|
212
|
+
},
|
|
213
|
+
aiAgentRunner: {
|
|
214
|
+
stream: async (_params: any, channel: any) => {
|
|
215
|
+
channel.send({
|
|
216
|
+
type: 'tool-call',
|
|
217
|
+
toolCallId: 'call-1',
|
|
218
|
+
toolName: 'lookup',
|
|
219
|
+
args: { city: 'Berlin' },
|
|
220
|
+
})
|
|
221
|
+
channel.send({
|
|
222
|
+
type: 'tool-result',
|
|
223
|
+
toolCallId: 'call-1',
|
|
224
|
+
toolName: 'lookup',
|
|
225
|
+
result: 'Error: upstream refused',
|
|
226
|
+
error: 'upstream refused',
|
|
227
|
+
})
|
|
228
|
+
channel.send({
|
|
229
|
+
type: 'tool-call',
|
|
230
|
+
toolCallId: 'call-2',
|
|
231
|
+
toolName: 'echo',
|
|
232
|
+
args: {},
|
|
233
|
+
})
|
|
234
|
+
channel.send({
|
|
235
|
+
type: 'tool-result',
|
|
236
|
+
toolCallId: 'call-2',
|
|
237
|
+
toolName: 'echo',
|
|
238
|
+
result: 'Error: this is just what the tool said',
|
|
239
|
+
})
|
|
240
|
+
return makeStepResult({ text: 'done', finishReason: 'stop' })
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
aiRunState: {
|
|
244
|
+
createRun: async () => 'run-tool-error',
|
|
245
|
+
updateRun: async () => {},
|
|
246
|
+
},
|
|
247
|
+
aiStorage: {
|
|
248
|
+
createThread: async () => {},
|
|
249
|
+
getMessages: async () => [],
|
|
250
|
+
saveMessages: async (_threadId: string, messages: any[]) => {
|
|
251
|
+
savedMessages.push(...messages)
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
} as any
|
|
255
|
+
|
|
256
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
257
|
+
|
|
258
|
+
await streamAIAgent(
|
|
259
|
+
'stream-tool-error-agent',
|
|
260
|
+
{
|
|
261
|
+
message: 'look it up',
|
|
262
|
+
threadId: 'thread-tool-error',
|
|
263
|
+
resourceId: 'resource-tool-error',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
channelId: 'channel-tool-error',
|
|
267
|
+
openingData: undefined,
|
|
268
|
+
state: 'open',
|
|
269
|
+
send: () => {},
|
|
270
|
+
close: () => {},
|
|
271
|
+
},
|
|
272
|
+
{}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const toolResults = savedMessages
|
|
276
|
+
.filter((message) => message.role === 'tool')
|
|
277
|
+
.flatMap((message) => message.toolResults ?? [])
|
|
278
|
+
|
|
279
|
+
assert.deepEqual(
|
|
280
|
+
toolResults.map((r: any) => [r.name, r.error]),
|
|
281
|
+
[
|
|
282
|
+
['lookup', 'upstream refused'],
|
|
283
|
+
['echo', undefined],
|
|
284
|
+
]
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('does not warn about modifyOutput when the middleware also handles the stream', async () => {
|
|
289
|
+
addTestAgent('stream-both-hooks-agent')
|
|
290
|
+
|
|
291
|
+
const warnings: unknown[][] = []
|
|
292
|
+
|
|
293
|
+
const middleware: PikkuAIMiddlewareHooks = {
|
|
294
|
+
modifyOutput: async (_services, ctx) => ({
|
|
295
|
+
text: ctx.text,
|
|
296
|
+
messages: ctx.messages,
|
|
297
|
+
}),
|
|
298
|
+
modifyOutputStream: async (_services, ctx) => ctx.event,
|
|
299
|
+
}
|
|
300
|
+
const agent = pikkuState(null, 'agent', 'agents').get(
|
|
301
|
+
'stream-both-hooks-agent'
|
|
302
|
+
)!
|
|
303
|
+
agent.aiMiddleware = [middleware] as any
|
|
304
|
+
pikkuState(null, 'agent', 'agents').set('stream-both-hooks-agent', agent)
|
|
305
|
+
|
|
306
|
+
const mockServices = {
|
|
307
|
+
logger: {
|
|
308
|
+
info: () => {},
|
|
309
|
+
warn: (...args: unknown[]) => warnings.push(args),
|
|
310
|
+
error: () => {},
|
|
311
|
+
debug: () => {},
|
|
312
|
+
},
|
|
313
|
+
aiAgentRunner: {
|
|
314
|
+
stream: async (_params: any, channel: any) => {
|
|
315
|
+
channel.send({ type: 'text-delta', text: 'Hi' })
|
|
316
|
+
return makeStepResult({ text: 'Hi', finishReason: 'stop' })
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
aiRunState: {
|
|
320
|
+
createRun: async () => 'run-both-hooks',
|
|
321
|
+
updateRun: async () => {},
|
|
322
|
+
},
|
|
323
|
+
} as any
|
|
324
|
+
|
|
325
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
326
|
+
|
|
327
|
+
await streamAIAgent(
|
|
328
|
+
'stream-both-hooks-agent',
|
|
329
|
+
{
|
|
330
|
+
message: 'hello',
|
|
331
|
+
threadId: 'thread-both-hooks',
|
|
332
|
+
resourceId: 'resource-both-hooks',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
channelId: 'channel-both-hooks',
|
|
336
|
+
openingData: undefined,
|
|
337
|
+
state: 'open',
|
|
338
|
+
send: () => {},
|
|
339
|
+
close: () => {},
|
|
340
|
+
},
|
|
341
|
+
{}
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
assert.deepEqual(
|
|
345
|
+
warnings.filter((args) =>
|
|
346
|
+
args.some(
|
|
347
|
+
(arg) => typeof arg === 'string' && arg.includes('modifyOutput')
|
|
348
|
+
)
|
|
349
|
+
),
|
|
350
|
+
[]
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
})
|