@pikku/core 0.12.20 → 0.12.21
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 +11 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,11 +2,19 @@ import { beforeEach, describe, test } from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
|
|
4
4
|
import { resetPikkuState, pikkuState } from '../../pikku-state.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
appendStepMessages,
|
|
7
|
+
checkForApprovals,
|
|
8
|
+
checkForCredentialRequests,
|
|
9
|
+
resumeAIAgent,
|
|
10
|
+
streamAIAgent,
|
|
11
|
+
} from './ai-agent-stream.js'
|
|
6
12
|
import { ToolApprovalRequired } from './ai-agent-prepare.js'
|
|
7
13
|
import type {
|
|
14
|
+
AgentRunState,
|
|
8
15
|
CoreAIAgent,
|
|
9
16
|
AIStreamEvent,
|
|
17
|
+
AIMessage,
|
|
10
18
|
PikkuAIMiddlewareHooks,
|
|
11
19
|
} from './ai-agent.types.js'
|
|
12
20
|
import type { AIAgentStepResult } from '../../services/ai-agent-runner-service.js'
|
|
@@ -20,7 +28,7 @@ const addTestAgent = (agentName: string) => {
|
|
|
20
28
|
name: agentName,
|
|
21
29
|
description: 'test agent',
|
|
22
30
|
instructions: 'be helpful',
|
|
23
|
-
model: 'test-model',
|
|
31
|
+
model: 'test/test-model',
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
pikkuState(null, 'agent', 'agentsMeta')[agentName] = {
|
|
@@ -30,9 +38,6 @@ const addTestAgent = (agentName: string) => {
|
|
|
30
38
|
workingMemorySchema: null,
|
|
31
39
|
}
|
|
32
40
|
pikkuState(null, 'agent', 'agents').set(agentName, agent)
|
|
33
|
-
pikkuState(null, 'models', 'config', {
|
|
34
|
-
models: { 'test-model': 'test/test-model' },
|
|
35
|
-
})
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
const makeStepResult = (
|
|
@@ -856,4 +861,538 @@ describe('streamAIAgent', () => {
|
|
|
856
861
|
assert.deepEqual(hookCalls[1].ctx.args, { q: 'test', injected: true })
|
|
857
862
|
assert.ok(hookCalls[1].ctx.durationMs >= 0)
|
|
858
863
|
})
|
|
864
|
+
|
|
865
|
+
test('suspends for credential requests and does not emit approval events', async () => {
|
|
866
|
+
addTestAgent('credential-stream-agent')
|
|
867
|
+
|
|
868
|
+
const updates: Array<{ runId: string; patch: unknown }> = []
|
|
869
|
+
const events: AIStreamEvent[] = []
|
|
870
|
+
|
|
871
|
+
const mockServices = {
|
|
872
|
+
logger: {
|
|
873
|
+
info: () => {},
|
|
874
|
+
warn: () => {},
|
|
875
|
+
error: () => {},
|
|
876
|
+
debug: () => {},
|
|
877
|
+
},
|
|
878
|
+
aiAgentRunner: {
|
|
879
|
+
stream: async (): Promise<AIAgentStepResult> =>
|
|
880
|
+
makeStepResult({
|
|
881
|
+
toolCalls: [
|
|
882
|
+
{ toolCallId: 'cred-1', toolName: 'secretTool', args: { id: 1 } },
|
|
883
|
+
],
|
|
884
|
+
toolResults: [
|
|
885
|
+
{
|
|
886
|
+
toolCallId: 'cred-1',
|
|
887
|
+
toolName: 'secretTool',
|
|
888
|
+
result: {
|
|
889
|
+
__credentialRequired: true,
|
|
890
|
+
credentialName: 'github',
|
|
891
|
+
credentialType: 'oauth2',
|
|
892
|
+
connectUrl: '/connect',
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
}),
|
|
897
|
+
},
|
|
898
|
+
aiRunState: {
|
|
899
|
+
createRun: async () => 'run-cred',
|
|
900
|
+
updateRun: async (runId: string, patch: unknown) => {
|
|
901
|
+
updates.push({ runId, patch })
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
} as any
|
|
905
|
+
|
|
906
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
907
|
+
|
|
908
|
+
await streamAIAgent(
|
|
909
|
+
'credential-stream-agent',
|
|
910
|
+
{
|
|
911
|
+
message: 'connect',
|
|
912
|
+
threadId: 'thread-cred',
|
|
913
|
+
resourceId: 'resource-cred',
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
channelId: 'channel-cred',
|
|
917
|
+
openingData: undefined,
|
|
918
|
+
state: 'open',
|
|
919
|
+
send: (event: AIStreamEvent) => events.push(event),
|
|
920
|
+
close: () => {},
|
|
921
|
+
},
|
|
922
|
+
{}
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
assert.deepEqual(updates, [
|
|
926
|
+
{
|
|
927
|
+
runId: 'run-cred',
|
|
928
|
+
patch: {
|
|
929
|
+
status: 'suspended',
|
|
930
|
+
suspendReason: 'credential',
|
|
931
|
+
pendingApprovals: [
|
|
932
|
+
{
|
|
933
|
+
type: 'credential-request',
|
|
934
|
+
toolCallId: 'cred-1',
|
|
935
|
+
toolName: 'secretTool',
|
|
936
|
+
args: { id: 1 },
|
|
937
|
+
credentialName: 'github',
|
|
938
|
+
credentialType: 'oauth2',
|
|
939
|
+
connectUrl: '/connect',
|
|
940
|
+
},
|
|
941
|
+
],
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
])
|
|
945
|
+
assert.ok(events.every((event) => event.type !== 'approval-request'))
|
|
946
|
+
assert.equal(events.at(-1)?.type, 'done')
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
test('persists streamed events, applies output stream middleware, and updates usage', async () => {
|
|
950
|
+
addTestAgent('persisted-stream-agent')
|
|
951
|
+
|
|
952
|
+
const events: AIStreamEvent[] = []
|
|
953
|
+
const savedMessages: Array<{ threadId: string; messages: AIMessage[] }> = []
|
|
954
|
+
const updates: Array<{ runId: string; patch: unknown }> = []
|
|
955
|
+
const middlewareStateSnapshots: number[] = []
|
|
956
|
+
|
|
957
|
+
const middleware: PikkuAIMiddlewareHooks = {
|
|
958
|
+
modifyOutputStream: async (_services, ctx) => {
|
|
959
|
+
const count = Number((ctx.state.count as number | undefined) ?? 0) + 1
|
|
960
|
+
ctx.state.count = count
|
|
961
|
+
middlewareStateSnapshots.push(count)
|
|
962
|
+
if (ctx.event.type === 'text-delta') {
|
|
963
|
+
return [
|
|
964
|
+
ctx.event,
|
|
965
|
+
{ type: 'reasoning-delta', text: `seen:${count}` } as AIStreamEvent,
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
return ctx.event
|
|
969
|
+
},
|
|
970
|
+
modifyOutput: async (_services, ctx) => ({
|
|
971
|
+
text: `${ctx.text} [streamed]`,
|
|
972
|
+
messages: ctx.messages,
|
|
973
|
+
}),
|
|
974
|
+
}
|
|
975
|
+
const agent = pikkuState(null, 'agent', 'agents').get(
|
|
976
|
+
'persisted-stream-agent'
|
|
977
|
+
)!
|
|
978
|
+
agent.aiMiddleware = [middleware] as any
|
|
979
|
+
pikkuState(null, 'agent', 'agents').set('persisted-stream-agent', agent)
|
|
980
|
+
|
|
981
|
+
const mockServices = {
|
|
982
|
+
logger: {
|
|
983
|
+
info: () => {},
|
|
984
|
+
warn: () => {},
|
|
985
|
+
error: () => {},
|
|
986
|
+
debug: () => {},
|
|
987
|
+
},
|
|
988
|
+
aiAgentRunner: {
|
|
989
|
+
stream: async (
|
|
990
|
+
_params: any,
|
|
991
|
+
channel: any
|
|
992
|
+
): Promise<AIAgentStepResult> => {
|
|
993
|
+
channel.send({ type: 'text-delta', text: 'Hello' })
|
|
994
|
+
channel.send({
|
|
995
|
+
type: 'tool-call',
|
|
996
|
+
toolCallId: 'tc-1',
|
|
997
|
+
toolName: 'echo',
|
|
998
|
+
args: { value: 1 },
|
|
999
|
+
})
|
|
1000
|
+
channel.send({
|
|
1001
|
+
type: 'tool-result',
|
|
1002
|
+
toolCallId: 'tc-1',
|
|
1003
|
+
toolName: 'echo',
|
|
1004
|
+
result: { ok: true },
|
|
1005
|
+
})
|
|
1006
|
+
channel.send({
|
|
1007
|
+
type: 'usage',
|
|
1008
|
+
tokens: { input: 3, output: 4 },
|
|
1009
|
+
model: 'test/test-model',
|
|
1010
|
+
} as AIStreamEvent)
|
|
1011
|
+
return makeStepResult({
|
|
1012
|
+
text: 'Hello',
|
|
1013
|
+
usage: { inputTokens: 3, outputTokens: 4 },
|
|
1014
|
+
finishReason: 'stop',
|
|
1015
|
+
})
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
aiRunState: {
|
|
1019
|
+
createRun: async () => 'run-persisted',
|
|
1020
|
+
updateRun: async (runId: string, patch: unknown) => {
|
|
1021
|
+
updates.push({ runId, patch })
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
aiStorage: {
|
|
1025
|
+
createThread: async () => {},
|
|
1026
|
+
getMessages: async () => [],
|
|
1027
|
+
saveMessages: async (threadId: string, messages: AIMessage[]) => {
|
|
1028
|
+
savedMessages.push({ threadId, messages })
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
} as any
|
|
1032
|
+
|
|
1033
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
1034
|
+
|
|
1035
|
+
const result = await streamAIAgent(
|
|
1036
|
+
'persisted-stream-agent',
|
|
1037
|
+
{
|
|
1038
|
+
message: 'hello',
|
|
1039
|
+
threadId: 'thread-persisted',
|
|
1040
|
+
resourceId: 'resource-persisted',
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
channelId: 'channel-persisted',
|
|
1044
|
+
openingData: undefined,
|
|
1045
|
+
state: 'open',
|
|
1046
|
+
send: (event: AIStreamEvent) => events.push(event),
|
|
1047
|
+
close: () => {},
|
|
1048
|
+
},
|
|
1049
|
+
{}
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
assert.equal(result, 'Hello')
|
|
1053
|
+
assert.ok(events.some((event) => event.type === 'reasoning-delta'))
|
|
1054
|
+
assert.deepEqual(middlewareStateSnapshots, [1, 2, 3, 4])
|
|
1055
|
+
assert.equal(savedMessages[0].messages[0].role, 'user')
|
|
1056
|
+
assert.equal(savedMessages[1].messages[0].role, 'assistant')
|
|
1057
|
+
assert.equal(savedMessages[1].messages[1].role, 'tool')
|
|
1058
|
+
assert.deepEqual(updates, [
|
|
1059
|
+
{
|
|
1060
|
+
runId: 'run-persisted',
|
|
1061
|
+
patch: {
|
|
1062
|
+
status: 'completed',
|
|
1063
|
+
usage: {
|
|
1064
|
+
inputTokens: 3,
|
|
1065
|
+
outputTokens: 4,
|
|
1066
|
+
model: 'test/test-model',
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
])
|
|
1071
|
+
})
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
describe('ai-agent-stream helpers', () => {
|
|
1075
|
+
test('checkForApprovals expands nested sub-approvals and explicit tool approvals', () => {
|
|
1076
|
+
const approvals = checkForApprovals(
|
|
1077
|
+
{
|
|
1078
|
+
text: '',
|
|
1079
|
+
toolCalls: [
|
|
1080
|
+
{ toolCallId: 'tc-1', toolName: 'toolA', args: { id: 1 } },
|
|
1081
|
+
{ toolCallId: 'tc-2', toolName: 'toolB', args: { id: 2 } },
|
|
1082
|
+
],
|
|
1083
|
+
toolResults: [
|
|
1084
|
+
{
|
|
1085
|
+
toolCallId: 'tc-2',
|
|
1086
|
+
toolName: 'toolB',
|
|
1087
|
+
result: {
|
|
1088
|
+
__approvalRequired: true,
|
|
1089
|
+
toolName: 'sub-agent',
|
|
1090
|
+
args: { task: 'x' },
|
|
1091
|
+
agentRunId: 'sub-run-1',
|
|
1092
|
+
subApprovals: [
|
|
1093
|
+
{
|
|
1094
|
+
toolCallId: 'sub-1',
|
|
1095
|
+
toolName: 'deleteTodo',
|
|
1096
|
+
args: { todoId: '1' },
|
|
1097
|
+
runId: 'sub-run-1',
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1104
|
+
finishReason: 'tool-calls',
|
|
1105
|
+
},
|
|
1106
|
+
[
|
|
1107
|
+
{
|
|
1108
|
+
name: 'toolA',
|
|
1109
|
+
description: '',
|
|
1110
|
+
inputSchema: {},
|
|
1111
|
+
execute: async () => {},
|
|
1112
|
+
needsApproval: true,
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
'run-1'
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
assert.equal(approvals.length, 2)
|
|
1119
|
+
assert.ok(approvals[0] instanceof ToolApprovalRequired)
|
|
1120
|
+
assert.equal(approvals[0].toolName, 'toolA')
|
|
1121
|
+
assert.equal(approvals[1].toolName, 'sub-agent')
|
|
1122
|
+
assert.equal(approvals[1].displayToolName, 'deleteTodo')
|
|
1123
|
+
assert.equal(approvals[1].agentRunId, 'sub-run-1')
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
test('checkForCredentialRequests and appendStepMessages handle structured results', () => {
|
|
1127
|
+
const requests = checkForCredentialRequests(
|
|
1128
|
+
{
|
|
1129
|
+
text: '',
|
|
1130
|
+
toolCalls: [
|
|
1131
|
+
{ toolCallId: 'tc-1', toolName: 'secretTool', args: { id: 1 } },
|
|
1132
|
+
],
|
|
1133
|
+
toolResults: [
|
|
1134
|
+
{
|
|
1135
|
+
toolCallId: 'tc-1',
|
|
1136
|
+
toolName: 'secretTool',
|
|
1137
|
+
result: {
|
|
1138
|
+
__credentialRequired: true,
|
|
1139
|
+
credentialName: 'github',
|
|
1140
|
+
credentialType: 'oauth2',
|
|
1141
|
+
connectUrl: '/connect',
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1146
|
+
finishReason: 'tool-calls',
|
|
1147
|
+
},
|
|
1148
|
+
'run-1'
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
assert.equal(requests[0].toolCallId, 'tc-1')
|
|
1152
|
+
assert.equal(requests[0].toolName, 'secretTool')
|
|
1153
|
+
assert.deepEqual(requests[0].args, { id: 1 })
|
|
1154
|
+
assert.equal(requests[0].credentialName, 'github')
|
|
1155
|
+
assert.equal(requests[0].credentialType, 'oauth2')
|
|
1156
|
+
assert.equal(requests[0].connectUrl, '/connect')
|
|
1157
|
+
|
|
1158
|
+
const runnerParams = { messages: [] as AIMessage[] } as any
|
|
1159
|
+
appendStepMessages(runnerParams, {
|
|
1160
|
+
text: 'hello',
|
|
1161
|
+
toolCalls: [
|
|
1162
|
+
{ toolCallId: 'tc-1', toolName: 'secretTool', args: { id: 1 } },
|
|
1163
|
+
],
|
|
1164
|
+
toolResults: [
|
|
1165
|
+
{ toolCallId: 'tc-1', toolName: 'secretTool', result: { ok: true } },
|
|
1166
|
+
],
|
|
1167
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1168
|
+
finishReason: 'tool-calls',
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
assert.equal(runnerParams.messages.length, 2)
|
|
1172
|
+
assert.equal(runnerParams.messages[0].role, 'assistant')
|
|
1173
|
+
assert.equal(runnerParams.messages[1].role, 'tool')
|
|
1174
|
+
assert.equal(
|
|
1175
|
+
runnerParams.messages[1].toolResults[0].result,
|
|
1176
|
+
JSON.stringify({ ok: true })
|
|
1177
|
+
)
|
|
1178
|
+
})
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
describe('resumeAIAgent', () => {
|
|
1182
|
+
test('rejecting one approval while others remain only resolves and finishes the stream', async () => {
|
|
1183
|
+
addTestAgent('resume-stream-agent')
|
|
1184
|
+
|
|
1185
|
+
const events: AIStreamEvent[] = []
|
|
1186
|
+
const updates: any[] = []
|
|
1187
|
+
const resolveCalls: any[] = []
|
|
1188
|
+
const savedMessages: any[] = []
|
|
1189
|
+
let remainingRun: AgentRunState
|
|
1190
|
+
|
|
1191
|
+
const initialRun: AgentRunState = {
|
|
1192
|
+
runId: 'run-1',
|
|
1193
|
+
agentName: 'resume-stream-agent',
|
|
1194
|
+
threadId: 'thread-1',
|
|
1195
|
+
resourceId: 'resource-1',
|
|
1196
|
+
status: 'suspended',
|
|
1197
|
+
suspendReason: 'approval',
|
|
1198
|
+
pendingApprovals: [
|
|
1199
|
+
{
|
|
1200
|
+
type: 'tool-call',
|
|
1201
|
+
toolCallId: 'tool-1',
|
|
1202
|
+
toolName: 'deploy',
|
|
1203
|
+
args: { env: 'prod' },
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
type: 'tool-call',
|
|
1207
|
+
toolCallId: 'tool-2',
|
|
1208
|
+
toolName: 'deploy',
|
|
1209
|
+
args: { env: 'stage' },
|
|
1210
|
+
},
|
|
1211
|
+
],
|
|
1212
|
+
usage: { inputTokens: 0, outputTokens: 0, model: 'test/test-model' },
|
|
1213
|
+
createdAt: new Date(),
|
|
1214
|
+
updatedAt: new Date(),
|
|
1215
|
+
}
|
|
1216
|
+
remainingRun = {
|
|
1217
|
+
...initialRun,
|
|
1218
|
+
pendingApprovals: [initialRun.pendingApprovals![1]],
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const mockServices = {
|
|
1222
|
+
logger: {
|
|
1223
|
+
info: () => {},
|
|
1224
|
+
warn: () => {},
|
|
1225
|
+
error: () => {},
|
|
1226
|
+
debug: () => {},
|
|
1227
|
+
},
|
|
1228
|
+
aiAgentRunner: {
|
|
1229
|
+
stream: async () => {
|
|
1230
|
+
throw new Error('should not continue')
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
aiRunState: {
|
|
1234
|
+
getRun: async () => remainingRun,
|
|
1235
|
+
resolveApproval: async (toolCallId: string, status: string) => {
|
|
1236
|
+
resolveCalls.push({ toolCallId, status })
|
|
1237
|
+
},
|
|
1238
|
+
updateRun: async (_runId: string, patch: any) => {
|
|
1239
|
+
updates.push(patch)
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
aiStorage: {
|
|
1243
|
+
saveMessages: async (_threadId: string, messages: AIMessage[]) => {
|
|
1244
|
+
savedMessages.push(messages)
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
} as any
|
|
1248
|
+
|
|
1249
|
+
let first = true
|
|
1250
|
+
mockServices.aiRunState.getRun = async () => {
|
|
1251
|
+
if (first) {
|
|
1252
|
+
first = false
|
|
1253
|
+
return initialRun
|
|
1254
|
+
}
|
|
1255
|
+
return remainingRun
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
1259
|
+
|
|
1260
|
+
await resumeAIAgent(
|
|
1261
|
+
{ runId: 'run-1', toolCallId: 'tool-1', approved: false },
|
|
1262
|
+
{
|
|
1263
|
+
channelId: 'resume-channel',
|
|
1264
|
+
openingData: undefined,
|
|
1265
|
+
state: 'open',
|
|
1266
|
+
send: (event: AIStreamEvent) => events.push(event),
|
|
1267
|
+
close: () => {},
|
|
1268
|
+
},
|
|
1269
|
+
{}
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
assert.deepEqual(resolveCalls, [{ toolCallId: 'tool-1', status: 'denied' }])
|
|
1273
|
+
assert.match(
|
|
1274
|
+
savedMessages[0][0].toolResults[0].result,
|
|
1275
|
+
/explicitly declined/
|
|
1276
|
+
)
|
|
1277
|
+
assert.deepEqual(updates, [])
|
|
1278
|
+
assert.equal(events.at(-1)?.type, 'done')
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
test('approving a tool executes it, streams the result, and resumes completion', async () => {
|
|
1282
|
+
addTestAgent('resume-stream-agent')
|
|
1283
|
+
pikkuState(null, 'agent', 'agentsMeta')['resume-stream-agent'].tools = [
|
|
1284
|
+
'deploy',
|
|
1285
|
+
]
|
|
1286
|
+
pikkuState(null, 'rpc', 'meta').deploy = 'deploy'
|
|
1287
|
+
pikkuState(null, 'function', 'meta').deploy = {
|
|
1288
|
+
description: 'Deploy',
|
|
1289
|
+
inputSchemaName: 'DeployInput',
|
|
1290
|
+
}
|
|
1291
|
+
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
1292
|
+
type: 'object',
|
|
1293
|
+
properties: { env: { type: 'string' } },
|
|
1294
|
+
})
|
|
1295
|
+
pikkuState(null, 'function', 'functions').set('deploy', {
|
|
1296
|
+
func: async (_services: any, input: any) => ({ deployed: input.env }),
|
|
1297
|
+
})
|
|
1298
|
+
|
|
1299
|
+
const events: AIStreamEvent[] = []
|
|
1300
|
+
const updates: any[] = []
|
|
1301
|
+
const resolveCalls: any[] = []
|
|
1302
|
+
const savedMessages: any[] = []
|
|
1303
|
+
|
|
1304
|
+
const run: AgentRunState = {
|
|
1305
|
+
runId: 'run-approve',
|
|
1306
|
+
agentName: 'resume-stream-agent',
|
|
1307
|
+
threadId: 'thread-approve',
|
|
1308
|
+
resourceId: 'resource-approve',
|
|
1309
|
+
status: 'suspended',
|
|
1310
|
+
suspendReason: 'approval',
|
|
1311
|
+
pendingApprovals: [
|
|
1312
|
+
{
|
|
1313
|
+
type: 'tool-call',
|
|
1314
|
+
toolCallId: 'tool-1',
|
|
1315
|
+
toolName: 'deploy',
|
|
1316
|
+
args: { env: 'prod' },
|
|
1317
|
+
},
|
|
1318
|
+
],
|
|
1319
|
+
usage: { inputTokens: 0, outputTokens: 0, model: 'test/test-model' },
|
|
1320
|
+
createdAt: new Date(),
|
|
1321
|
+
updatedAt: new Date(),
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
let getRunCalls = 0
|
|
1325
|
+
const mockServices = {
|
|
1326
|
+
logger: {
|
|
1327
|
+
info: () => {},
|
|
1328
|
+
warn: () => {},
|
|
1329
|
+
error: () => {},
|
|
1330
|
+
debug: () => {},
|
|
1331
|
+
},
|
|
1332
|
+
aiAgentRunner: {
|
|
1333
|
+
stream: async (
|
|
1334
|
+
_params: any,
|
|
1335
|
+
channel: any
|
|
1336
|
+
): Promise<AIAgentStepResult> => {
|
|
1337
|
+
channel.send({ type: 'text-delta', text: 'continued' })
|
|
1338
|
+
channel.send({
|
|
1339
|
+
type: 'usage',
|
|
1340
|
+
tokens: { input: 2, output: 3 },
|
|
1341
|
+
model: 'test/test-model',
|
|
1342
|
+
} as AIStreamEvent)
|
|
1343
|
+
return makeStepResult({
|
|
1344
|
+
text: 'continued',
|
|
1345
|
+
usage: { inputTokens: 2, outputTokens: 3 },
|
|
1346
|
+
finishReason: 'stop',
|
|
1347
|
+
})
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
aiRunState: {
|
|
1351
|
+
getRun: async () => {
|
|
1352
|
+
getRunCalls++
|
|
1353
|
+
return getRunCalls === 1 ? run : { ...run, pendingApprovals: [] }
|
|
1354
|
+
},
|
|
1355
|
+
resolveApproval: async (toolCallId: string, status: string) => {
|
|
1356
|
+
resolveCalls.push({ toolCallId, status })
|
|
1357
|
+
},
|
|
1358
|
+
updateRun: async (_runId: string, patch: any) => {
|
|
1359
|
+
updates.push(patch)
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
aiStorage: {
|
|
1363
|
+
saveMessages: async (_threadId: string, messages: AIMessage[]) => {
|
|
1364
|
+
savedMessages.push(messages)
|
|
1365
|
+
},
|
|
1366
|
+
getMessages: async () => [],
|
|
1367
|
+
},
|
|
1368
|
+
} as any
|
|
1369
|
+
|
|
1370
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
1371
|
+
|
|
1372
|
+
await resumeAIAgent(
|
|
1373
|
+
{ runId: 'run-approve', toolCallId: 'tool-1', approved: true },
|
|
1374
|
+
{
|
|
1375
|
+
channelId: 'resume-channel',
|
|
1376
|
+
openingData: undefined,
|
|
1377
|
+
state: 'open',
|
|
1378
|
+
send: (event: AIStreamEvent) => events.push(event),
|
|
1379
|
+
close: () => {},
|
|
1380
|
+
},
|
|
1381
|
+
{}
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
assert.deepEqual(resolveCalls, [
|
|
1385
|
+
{ toolCallId: 'tool-1', status: 'approved' },
|
|
1386
|
+
])
|
|
1387
|
+
assert.equal(events[0].type, 'tool-result')
|
|
1388
|
+
assert.equal(events.at(-1)?.type, 'done')
|
|
1389
|
+
assert.deepEqual(updates, [
|
|
1390
|
+
{ status: 'running' },
|
|
1391
|
+
{
|
|
1392
|
+
status: 'completed',
|
|
1393
|
+
usage: { inputTokens: 2, outputTokens: 3, model: 'test/test-model' },
|
|
1394
|
+
},
|
|
1395
|
+
])
|
|
1396
|
+
assert.equal(savedMessages.length, 2)
|
|
1397
|
+
})
|
|
859
1398
|
})
|