@pikku/core 0.12.80 → 0.12.82
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 +312 -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 -15
- package/dist/services/index.js +5 -5
- 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 +0 -2
- package/dist/types/state.types.d.ts +13 -0
- 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 +2 -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 +4 -0
- package/dist/wirings/rpc/addon-runner.js +19 -3
- package/dist/wirings/rpc/rpc-runner.js +2 -0
- package/dist/wirings/rpc/rpc-types.d.ts +4 -0
- package/dist/wirings/rpc/wire-addon.d.ts +13 -0
- package/dist/wirings/rpc/wire-addon.js +4 -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/index.d.ts +6 -6
- package/dist/wirings/workflow/index.js +2 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +39 -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/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 +1 -1
- 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 -1
- 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 +70 -94
- 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 +3 -43
- 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 +3 -6
- package/src/types/state.types.ts +16 -0
- 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 +2 -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 +34 -3
- package/src/wirings/rpc/addon-secrets.test.ts +261 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +2 -0
- package/src/wirings/rpc/rpc-types.ts +4 -0
- package/src/wirings/rpc/wire-addon.ts +17 -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/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -20
- package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
- 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/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 +0 -9
- 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/services/user-flow-actors-service.js +0 -1
- 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
|
@@ -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
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AIStreamChannel,
|
|
3
3
|
AIStreamEvent,
|
|
4
|
+
AIAgentStep,
|
|
4
5
|
AIMessage,
|
|
5
6
|
AIToolCall,
|
|
6
7
|
AIToolResult,
|
|
@@ -9,6 +10,10 @@ import type {
|
|
|
9
10
|
CoreAIAgent,
|
|
10
11
|
AIAgentMemoryConfig,
|
|
11
12
|
} from './ai-agent.types.js'
|
|
13
|
+
import {
|
|
14
|
+
finalizeAgentRun,
|
|
15
|
+
lastUserMessageText,
|
|
16
|
+
} from './ai-agent-finalize.js'
|
|
12
17
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
13
18
|
import { applyInputMiddleware } from './ai-agent-turn.js'
|
|
14
19
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
@@ -68,6 +73,8 @@ type PersistingChannel = AIStreamChannel & {
|
|
|
68
73
|
fullText: string
|
|
69
74
|
flush: (opts?: { interrupted?: boolean }) => Promise<void>
|
|
70
75
|
totalUsage: { inputTokens: number; outputTokens: number; model?: string }
|
|
76
|
+
/** Every tool the run called, kept for the whole run rather than per step. */
|
|
77
|
+
runToolCalls: NonNullable<AIAgentStep['toolCalls']>
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
function createPersistingChannel(
|
|
@@ -89,6 +96,9 @@ function createPersistingChannel(
|
|
|
89
96
|
inputTokens: 0,
|
|
90
97
|
outputTokens: 0,
|
|
91
98
|
}
|
|
99
|
+
// Survives the per-step flush below, which clears its own buffers: the run
|
|
100
|
+
// record needs every call the run made, not just the last step's.
|
|
101
|
+
const runToolCalls: NonNullable<AIAgentStep['toolCalls']> = []
|
|
92
102
|
|
|
93
103
|
const flushStep = async (opts?: { interrupted?: boolean }) => {
|
|
94
104
|
if (!storage) return
|
|
@@ -137,6 +147,8 @@ function createPersistingChannel(
|
|
|
137
147
|
})
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
const runToolCallIndex = new Map<string, number>()
|
|
151
|
+
|
|
140
152
|
const channel: PersistingChannel = {
|
|
141
153
|
channelId: parent.channelId,
|
|
142
154
|
openingData: parent.openingData,
|
|
@@ -149,6 +161,9 @@ function createPersistingChannel(
|
|
|
149
161
|
get totalUsage() {
|
|
150
162
|
return totalUsage
|
|
151
163
|
},
|
|
164
|
+
get runToolCalls() {
|
|
165
|
+
return runToolCalls
|
|
166
|
+
},
|
|
152
167
|
flush: flushStep,
|
|
153
168
|
close: () => parent.close(),
|
|
154
169
|
sendBinary: (data) => parent.sendBinary(data),
|
|
@@ -157,6 +172,26 @@ function createPersistingChannel(
|
|
|
157
172
|
// the client was streamed, and an interrupted run has to be able to
|
|
158
173
|
// report the fragment it got through even with persistence turned off.
|
|
159
174
|
if (event.type === 'text-delta') fullText += event.text
|
|
175
|
+
if (event.type === 'tool-call') {
|
|
176
|
+
runToolCallIndex.set(event.toolCallId, runToolCalls.length)
|
|
177
|
+
runToolCalls.push({
|
|
178
|
+
name: event.toolName,
|
|
179
|
+
args: event.args as Record<string, unknown>,
|
|
180
|
+
result: '',
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
if (event.type === 'tool-result') {
|
|
184
|
+
const index = runToolCallIndex.get(event.toolCallId)
|
|
185
|
+
const result =
|
|
186
|
+
typeof event.result === 'string'
|
|
187
|
+
? event.result
|
|
188
|
+
: JSON.stringify(event.result)
|
|
189
|
+
const call = index === undefined ? undefined : runToolCalls[index]
|
|
190
|
+
if (call) {
|
|
191
|
+
call.result = result
|
|
192
|
+
if (event.error) call.error = event.error
|
|
193
|
+
}
|
|
194
|
+
}
|
|
160
195
|
if (storage) {
|
|
161
196
|
switch (event.type) {
|
|
162
197
|
case 'text-delta':
|
|
@@ -177,6 +212,7 @@ function createPersistingChannel(
|
|
|
177
212
|
typeof event.result === 'string'
|
|
178
213
|
? event.result
|
|
179
214
|
: JSON.stringify(event.result),
|
|
215
|
+
...(event.error ? { error: event.error } : {}),
|
|
180
216
|
})
|
|
181
217
|
break
|
|
182
218
|
case 'generative-ui':
|
|
@@ -203,44 +239,67 @@ function createPersistingChannel(
|
|
|
203
239
|
return channel
|
|
204
240
|
}
|
|
205
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Agents already warned about, so a per-request hook does not become a
|
|
244
|
+
* per-request log line.
|
|
245
|
+
*/
|
|
246
|
+
const warnedUnstreamedOutputHooks = new Set<string>()
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* `modifyOutput` does not run on a streamed run at all. Nothing here could act
|
|
250
|
+
* on what it returns — the text has already reached the client, and
|
|
251
|
+
* `createPersistingChannel` flushes each step to storage as it goes, so by the
|
|
252
|
+
* time the run ends the transcript is already written.
|
|
253
|
+
*
|
|
254
|
+
* Rewriting on this path belongs to `modifyOutputStream`, which genuinely
|
|
255
|
+
* works: the stream middleware wraps the persisting channel, so what is stored
|
|
256
|
+
* and accumulated is already what the client was sent. A middleware that
|
|
257
|
+
* rewrites in `modifyOutput` only — a redaction hook, typically — is therefore
|
|
258
|
+
* silently ineffective when the agent is streamed, and is told so once.
|
|
259
|
+
*/
|
|
260
|
+
const warnUnstreamedOutputHooks = (
|
|
261
|
+
agentName: string,
|
|
262
|
+
aiMiddlewares: PikkuAIMiddlewareHooks[],
|
|
263
|
+
logger?: { warn: (...args: any[]) => void }
|
|
264
|
+
) => {
|
|
265
|
+
if (warnedUnstreamedOutputHooks.has(agentName)) return
|
|
266
|
+
const unstreamed = aiMiddlewares.some(
|
|
267
|
+
(mw) => mw.modifyOutput && !mw.modifyOutputStream
|
|
268
|
+
)
|
|
269
|
+
if (!unstreamed) return
|
|
270
|
+
warnedUnstreamedOutputHooks.add(agentName)
|
|
271
|
+
logger?.warn(
|
|
272
|
+
`Agent '${agentName}' has AI middleware with modifyOutput but no modifyOutputStream — modifyOutput does not apply to streamed runs. Implement modifyOutputStream to affect a streamed reply.`
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
206
276
|
async function postStreamCleanup(
|
|
207
277
|
persistingChannel: PersistingChannel,
|
|
208
|
-
aiMiddlewares: PikkuAIMiddlewareHooks[],
|
|
209
|
-
singletonServices: any,
|
|
210
|
-
messages: AIMessage[],
|
|
211
278
|
aiRunState: AIRunStateService,
|
|
212
|
-
runId: string
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const mw = aiMiddlewares[i]
|
|
219
|
-
if (mw.modifyOutput) {
|
|
220
|
-
const result = await mw.modifyOutput(singletonServices, {
|
|
221
|
-
text: outputText,
|
|
222
|
-
messages: outputMessages,
|
|
223
|
-
usage: {
|
|
224
|
-
inputTokens: usage.inputTokens,
|
|
225
|
-
outputTokens: usage.outputTokens,
|
|
226
|
-
},
|
|
227
|
-
})
|
|
228
|
-
outputText = result.text
|
|
229
|
-
outputMessages = result.messages
|
|
230
|
-
}
|
|
279
|
+
runId: string,
|
|
280
|
+
run: {
|
|
281
|
+
agentName: string
|
|
282
|
+
threadId: string
|
|
283
|
+
resourceId?: string
|
|
284
|
+
input: string
|
|
231
285
|
}
|
|
232
|
-
|
|
233
|
-
await aiRunState
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
await finalizeAgentRun(aiRunState, {
|
|
288
|
+
runId,
|
|
289
|
+
agentName: run.agentName,
|
|
290
|
+
threadId: run.threadId,
|
|
291
|
+
resourceId: run.resourceId,
|
|
292
|
+
input: run.input,
|
|
293
|
+
// Already what the client received: the stream middleware wraps the
|
|
294
|
+
// persisting channel, so both were accumulated post-rewrite.
|
|
295
|
+
text: persistingChannel.fullText,
|
|
296
|
+
steps: [
|
|
297
|
+
{
|
|
298
|
+
usage: persistingChannel.totalUsage,
|
|
299
|
+
toolCalls: persistingChannel.runToolCalls,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
usage: persistingChannel.totalUsage,
|
|
244
303
|
})
|
|
245
304
|
}
|
|
246
305
|
|
|
@@ -723,6 +782,8 @@ export async function streamAIAgent(
|
|
|
723
782
|
await storage.saveMessages(threadId, [persistedUserMessage])
|
|
724
783
|
}
|
|
725
784
|
|
|
785
|
+
warnUnstreamedOutputHooks(agentName, aiMiddlewares, singletonServices.logger)
|
|
786
|
+
|
|
726
787
|
const streamMiddleware = aiMiddlewares
|
|
727
788
|
.filter((mw) => mw.modifyOutputStream)
|
|
728
789
|
.map((mw) => {
|
|
@@ -849,14 +910,12 @@ export async function streamAIAgent(
|
|
|
849
910
|
return persistingChannel.fullText
|
|
850
911
|
}
|
|
851
912
|
|
|
852
|
-
await postStreamCleanup(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
runnerParams.messages,
|
|
857
|
-
|
|
858
|
-
runId
|
|
859
|
-
)
|
|
913
|
+
await postStreamCleanup(persistingChannel, aiRunState, runId, {
|
|
914
|
+
agentName,
|
|
915
|
+
threadId,
|
|
916
|
+
resourceId: input.resourceId,
|
|
917
|
+
input: lastUserMessageText(runnerParams.messages),
|
|
918
|
+
})
|
|
860
919
|
|
|
861
920
|
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
862
921
|
await outputChannel.send({ type: 'done' })
|
|
@@ -1184,16 +1243,17 @@ export async function resumeAIAgent(
|
|
|
1184
1243
|
typeof pending.args === 'string' ? JSON.parse(pending.args) : pending.args
|
|
1185
1244
|
|
|
1186
1245
|
let toolResult: unknown
|
|
1187
|
-
let
|
|
1246
|
+
let toolError: string | undefined
|
|
1188
1247
|
try {
|
|
1189
1248
|
toolResult = await matchingTool.execute(toolArgs)
|
|
1190
1249
|
} catch (execErr: any) {
|
|
1191
1250
|
if (execErr?.payload?.error === 'missing_credential') {
|
|
1192
1251
|
toolResult = execErr.payload
|
|
1252
|
+
toolError = 'missing_credential'
|
|
1193
1253
|
} else {
|
|
1194
|
-
|
|
1254
|
+
toolError = execErr instanceof Error ? execErr.message : String(execErr)
|
|
1255
|
+
toolResult = `Error: ${toolError}`
|
|
1195
1256
|
}
|
|
1196
|
-
isError = true
|
|
1197
1257
|
}
|
|
1198
1258
|
|
|
1199
1259
|
const resultStr =
|
|
@@ -1220,7 +1280,7 @@ export async function resumeAIAgent(
|
|
|
1220
1280
|
toolCallId: input.toolCallId,
|
|
1221
1281
|
toolName: pending.toolName,
|
|
1222
1282
|
result: toolResult,
|
|
1223
|
-
...(
|
|
1283
|
+
...(toolError ? { error: toolError } : {}),
|
|
1224
1284
|
})
|
|
1225
1285
|
}
|
|
1226
1286
|
|
|
@@ -1313,6 +1373,12 @@ async function continueAfterToolResult(
|
|
|
1313
1373
|
// knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
|
|
1314
1374
|
const interruptHandle = registerInterruptibleRun(run.runId)
|
|
1315
1375
|
|
|
1376
|
+
warnUnstreamedOutputHooks(
|
|
1377
|
+
run.agentName,
|
|
1378
|
+
aiMiddlewares,
|
|
1379
|
+
singletonServices.logger
|
|
1380
|
+
)
|
|
1381
|
+
|
|
1316
1382
|
const streamMiddleware = aiMiddlewares
|
|
1317
1383
|
.filter((mw) => mw.modifyOutputStream)
|
|
1318
1384
|
.map((mw) => {
|
|
@@ -1434,14 +1500,10 @@ async function continueAfterToolResult(
|
|
|
1434
1500
|
return
|
|
1435
1501
|
}
|
|
1436
1502
|
|
|
1437
|
-
await postStreamCleanup(
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
runnerParams.messages,
|
|
1442
|
-
aiRunState,
|
|
1443
|
-
run.runId
|
|
1444
|
-
)
|
|
1503
|
+
await postStreamCleanup(persistingChannel, aiRunState, run.runId, {
|
|
1504
|
+
...run,
|
|
1505
|
+
input: lastUserMessageText(runnerParams.messages),
|
|
1506
|
+
})
|
|
1445
1507
|
|
|
1446
1508
|
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
1447
1509
|
await wrappedChannel.send({ type: 'done' })
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { toAccumulatedStep } from './ai-agent-turn.js'
|
|
5
|
+
import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
|
|
6
|
+
|
|
7
|
+
const stepResult = (
|
|
8
|
+
overrides?: Partial<AIAgentStepResult>
|
|
9
|
+
): AIAgentStepResult => ({
|
|
10
|
+
text: '',
|
|
11
|
+
toolCalls: [],
|
|
12
|
+
toolResults: [],
|
|
13
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
14
|
+
finishReason: 'stop',
|
|
15
|
+
...overrides,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('toAccumulatedStep', () => {
|
|
19
|
+
test('carries a tool failure as its own field, not only as rendered text', () => {
|
|
20
|
+
const step = toAccumulatedStep(
|
|
21
|
+
stepResult({
|
|
22
|
+
toolCalls: [
|
|
23
|
+
{ toolCallId: 'call-1', toolName: 'lookupOrder', args: { id: 7 } },
|
|
24
|
+
],
|
|
25
|
+
toolResults: [
|
|
26
|
+
{
|
|
27
|
+
toolCallId: 'call-1',
|
|
28
|
+
toolName: 'lookupOrder',
|
|
29
|
+
result: 'Error: order service unreachable',
|
|
30
|
+
error: 'order service unreachable',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(step.toolCalls, [
|
|
37
|
+
{
|
|
38
|
+
name: 'lookupOrder',
|
|
39
|
+
args: { id: 7 },
|
|
40
|
+
result: 'Error: order service unreachable',
|
|
41
|
+
error: 'order service unreachable',
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('leaves error unset on a tool that returned normally, even if it says Error', () => {
|
|
47
|
+
// A tool is allowed to return the word "Error" — which is exactly why
|
|
48
|
+
// "did this fail" cannot be answered by matching on the result text.
|
|
49
|
+
const step = toAccumulatedStep(
|
|
50
|
+
stepResult({
|
|
51
|
+
toolCalls: [
|
|
52
|
+
{ toolCallId: 'call-1', toolName: 'searchLogs', args: { q: 'x' } },
|
|
53
|
+
],
|
|
54
|
+
toolResults: [
|
|
55
|
+
{
|
|
56
|
+
toolCallId: 'call-1',
|
|
57
|
+
toolName: 'searchLogs',
|
|
58
|
+
result: 'Error: connection refused (1 match)',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert.equal(step.toolCalls[0].error, undefined)
|
|
65
|
+
assert.ok(!('error' in step.toolCalls[0]))
|
|
66
|
+
})
|
|
67
|
+
})
|