@pikku/core 0.12.20 → 0.12.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. 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 { streamAIAgent } from './ai-agent-stream.js'
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
  })