@stack-spot/portal-network 1.0.0-dev.1769537511491 → 1.0.0-dev.1769793051111

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 (58) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/api/accountAssetManager.d.ts +1 -0
  3. package/dist/api/accountAssetManager.d.ts.map +1 -1
  4. package/dist/api/accountAssetManager.js.map +1 -1
  5. package/dist/api/cloudPlatform.d.ts +79 -33
  6. package/dist/api/cloudPlatform.d.ts.map +1 -1
  7. package/dist/api/cloudPlatform.js +86 -76
  8. package/dist/api/cloudPlatform.js.map +1 -1
  9. package/dist/api/codeShift.d.ts +7 -3
  10. package/dist/api/codeShift.d.ts.map +1 -1
  11. package/dist/api/codeShift.js +3 -2
  12. package/dist/api/codeShift.js.map +1 -1
  13. package/dist/api/genAiInference.d.ts +54 -3
  14. package/dist/api/genAiInference.d.ts.map +1 -1
  15. package/dist/api/genAiInference.js +55 -2
  16. package/dist/api/genAiInference.js.map +1 -1
  17. package/dist/apis.json +3 -3
  18. package/dist/client/account.d.ts +16 -0
  19. package/dist/client/account.d.ts.map +1 -1
  20. package/dist/client/account.js +9 -0
  21. package/dist/client/account.js.map +1 -1
  22. package/dist/client/ai.d.ts +5 -28
  23. package/dist/client/ai.d.ts.map +1 -1
  24. package/dist/client/ai.js +1 -560
  25. package/dist/client/ai.js.map +1 -1
  26. package/dist/client/cloud-platform.d.ts +4 -12
  27. package/dist/client/cloud-platform.d.ts.map +1 -1
  28. package/dist/client/cloud-platform.js +5 -14
  29. package/dist/client/cloud-platform.js.map +1 -1
  30. package/dist/client/code-shift.d.ts +2 -1
  31. package/dist/client/code-shift.d.ts.map +1 -1
  32. package/dist/client/discover.d.ts +11 -4
  33. package/dist/client/discover.d.ts.map +1 -1
  34. package/dist/client/discover.js +140 -136
  35. package/dist/client/discover.js.map +1 -1
  36. package/dist/client/gen-ai-inference.d.ts +4 -0
  37. package/dist/client/gen-ai-inference.d.ts.map +1 -1
  38. package/dist/client/gen-ai-inference.js +267 -0
  39. package/dist/client/gen-ai-inference.js.map +1 -1
  40. package/dist/client/types.d.ts +13 -14
  41. package/dist/client/types.d.ts.map +1 -1
  42. package/dist/utils/StreamedJson.d.ts +7 -1
  43. package/dist/utils/StreamedJson.d.ts.map +1 -1
  44. package/dist/utils/StreamedJson.js +10 -2
  45. package/dist/utils/StreamedJson.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/api/accountAssetManager.ts +1 -0
  48. package/src/api/cloudPlatform.ts +170 -111
  49. package/src/api/codeShift.ts +9 -4
  50. package/src/api/genAiInference.ts +124 -4
  51. package/src/apis.json +8 -8
  52. package/src/client/account.ts +4 -0
  53. package/src/client/ai.ts +1 -626
  54. package/src/client/cloud-platform.ts +4 -9
  55. package/src/client/discover.ts +151 -137
  56. package/src/client/gen-ai-inference.ts +281 -0
  57. package/src/client/types.ts +14 -14
  58. package/src/utils/StreamedJson.tsx +11 -2
@@ -2,16 +2,25 @@ import { HttpError } from '@oazapfts/runtime'
2
2
  import { findLast, last } from 'lodash'
3
3
  import { getApiAddresses } from '../api-addresses'
4
4
  import { ConversationResponse } from '../api/ai'
5
- import { create, create1, create2, defaults, deleteById, deleteById1, deleteById2, fetchInsights, getAll, getAll1, getAll2, getAllByHypothesis, getById, getById1, getById2, getInsightById, GetOpportunityResponse, AiChatRequest, refreshInsights } from '../api/discover'
5
+ import { AiChatRequest, create, create1, create2, defaults, deleteById, deleteById1, deleteById2, fetchInsights, getAll, getAll1, getAll2, getAllByHypothesis, getById, getById1, getById2, getInsightById, GetOpportunityResponse, refreshInsights } from '../api/discover'
6
6
  import { DefaultAPIError } from '../error/DefaultAPIError'
7
+ import { baseDictionary } from '../error/dictionary/base'
7
8
  import { StackspotAPIError } from '../error/StackspotAPIError'
9
+ import { ReactQueryNetworkClient } from '../network/ReactQueryNetworkClient'
8
10
  import { StreamedJson } from '../utils/StreamedJson'
9
- import { baseDictionary } from '../error/dictionary/base'
10
11
  import { formatJson } from '../utils/string'
11
- import { ReactQueryNetworkClient } from '../network/ReactQueryNetworkClient'
12
- import { aiClient } from './ai'
13
- import { ChatAgentTool, ChatResponseWithSteps, FixedChatResponse, StepChatStep } from './types'
14
12
  import { agentToolsClient } from './agent-tools'
13
+ import { aiClient } from './ai'
14
+ import { AgentInfo, ChatAgentTool, ChatResponse, ChatStep, StepChatStep } from './types'
15
+
16
+ export interface FixedDiscoveryChatResponse extends ChatResponse {
17
+ agent_info: AgentInfo[],
18
+ tools?: string[],
19
+ }
20
+
21
+ export interface DiscoveryChatResponseWithSteps extends FixedDiscoveryChatResponse {
22
+ steps: ChatStep[],
23
+ }
15
24
 
16
25
  export interface ChatConversionDetails extends ConversationResponse {
17
26
  opportunityName?: string,
@@ -121,7 +130,8 @@ class DiscoverClient extends ReactQueryNetworkClient {
121
130
  }
122
131
 
123
132
 
124
- sendChatMessage(request: AiChatRequest & { agentId: string }, minChangeIntervalMS?: number): StreamedJson<ChatResponseWithSteps> {
133
+ sendChatMessage(request: AiChatRequest & { agentId: string }, minChangeIntervalMS?: number)
134
+ : StreamedJson<DiscoveryChatResponseWithSteps> {
125
135
  const abortController = new AbortController()
126
136
  const headers = {
127
137
  'Content-Type': 'application/json',
@@ -136,172 +146,176 @@ class DiscoverClient extends ReactQueryNetworkClient {
136
146
  * normal streamings of data, we need this separate function to deal with them. It transforms the internal data model of the
137
147
  * StreamedJson object whenever an event is triggered.
138
148
  */
139
- async function transform(event: Partial<FixedChatResponse>, data: Partial<ChatResponseWithSteps>) {
140
- const info = event.agent_info
149
+ async function transform(event: Partial<FixedDiscoveryChatResponse>, data: Partial<DiscoveryChatResponseWithSteps>) {
150
+ const agentInfoList = event.agent_info
141
151
 
142
- if (!info) return
152
+ if (!agentInfoList?.length) return
143
153
 
144
154
  const tools = await DiscoverClient.toolsOfAgent(request.agentId)
145
155
  data.steps = data.steps ? [...data.steps] : []
156
+
157
+ for (const info of agentInfoList) {
158
+ if (!info) continue
146
159
 
147
- if (info.type === 'planning' && info.action === 'end') {
148
- data.steps.push({
149
- id: 'planning',
150
- type: 'planning',
151
- status: 'success',
152
- duration: info.duration || 0,
153
- steps: info.data?.steps?.map(s => s.goal) ?? [],
154
- goal: info.data?.plan_goal ?? '',
155
- })
156
-
157
- info.data?.steps.forEach(s => data.steps?.push({
158
- id: s.id,
159
- type: 'step',
160
- status: 'pending',
161
- input: s.goal,
162
- attempts: [{
163
- tools: s.tools?.map(t => ({
164
- ...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
165
- executionId: t.tool_execution_id,
166
- goal: t.goal,
167
- })),
168
- }],
169
- }))
170
- data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
171
- }
172
-
173
- if (info.type === 'planning' && info.action === 'awaiting_approval') {
174
- data.steps.push({
175
- id: 'planning',
176
- type: 'planning',
177
- status: 'awaiting_approval',
178
- user_question: info.data?.user_question,
179
- duration: info.duration || 0,
180
- steps: info.data?.steps?.map(s => s.goal) ?? [],
181
- goal: info.data?.plan_goal ?? '',
182
- })
183
- info.data?.steps.forEach(s => data.steps?.push({
184
- id: s.id,
185
- type: 'step',
186
- status: 'pending',
187
- input: s.goal,
188
- attempts: [{
189
- tools: s.tools?.map(t => ({
190
- ...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
191
- executionId: t.tool_execution_id,
192
- goal: t.goal,
193
- })),
194
- }],
195
- }))
196
- data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
197
- }
198
-
199
- if (info.type === 'step' && info.action === 'start') {
200
- const step = data.steps.find(s => s.id === info.id)
201
- if (step) step.status = 'running'
202
- }
160
+ if (info.type === 'planning' && info.action === 'end') {
161
+ data.steps.push({
162
+ id: 'planning',
163
+ type: 'planning',
164
+ status: 'success',
165
+ duration: info.duration || 0,
166
+ steps: info.data?.steps?.map(s => s.goal) ?? [],
167
+ goal: info.data?.plan_goal ?? '',
168
+ })
203
169
 
204
- if (info.type === 'step' && info.action === 'end') {
205
- const step = data.steps.find(s => s.id === info.id) as StepChatStep
206
- if (step) {
207
- step.status = 'success'
208
- step.duration = info.duration
209
- const lastToolId = last(step.attempts[0].tools)?.id
210
- const lastAttemptOfLastTool = findLast(step.attempts.map(a => a.tools).flat(), t => t?.id === lastToolId)
211
- step.output = lastAttemptOfLastTool?.output
170
+ info.data?.steps.forEach(s => data.steps?.push({
171
+ id: s.id,
172
+ type: 'step',
173
+ status: 'pending',
174
+ input: s.goal,
175
+ attempts: [{
176
+ tools: s.tools?.map(t => ({
177
+ ...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
178
+ executionId: t.tool_execution_id,
179
+ goal: t.goal,
180
+ })),
181
+ }],
182
+ }))
183
+ data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
212
184
  }
213
- }
214
185
 
215
- if (info.type === 'tool' && info.action === 'awaiting_approval') {
216
- const tool = tools.find(({ id }) => id === info.data?.tool_id)
217
- data.steps.push({
218
- id: info.id,
219
- type: 'tool',
220
- status: 'awaiting_approval',
221
- duration: info.duration || 0,
222
- input: info.data?.input,
223
- user_question: info.data?.user_question,
224
- attempts: [{
225
- tools: [{
226
- executionId: info.id,
227
- id: info.data?.tool_id ?? '',
228
- name: tool?.name ?? '',
229
- goal: tool?.goal,
230
- ...tool,
186
+ if (info.type === 'planning' && info.action === 'awaiting_approval') {
187
+ data.steps.push({
188
+ id: 'planning',
189
+ type: 'planning',
190
+ status: 'awaiting_approval',
191
+ user_question: info.data?.user_question,
192
+ duration: info.duration || 0,
193
+ steps: info.data?.steps?.map(s => s.goal) ?? [],
194
+ goal: info.data?.plan_goal ?? '',
195
+ })
196
+ info.data?.steps.forEach(s => data.steps?.push({
197
+ id: s.id,
198
+ type: 'step',
199
+ status: 'pending',
200
+ input: s.goal,
201
+ attempts: [{
202
+ tools: s.tools?.map(t => ({
203
+ ...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
204
+ executionId: t.tool_execution_id,
205
+ goal: t.goal,
206
+ })),
231
207
  }],
232
- }],
233
- })
234
- data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
235
- }
208
+ }))
209
+ data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
210
+ }
211
+
212
+ if (info.type === 'step' && info.action === 'start') {
213
+ const step = data.steps.find(s => s.id === info.id)
214
+ if (step) step.status = 'running'
215
+ }
236
216
 
237
- if (info.type === 'tool' && info.action === 'start') {
238
- const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
239
- if (!info.data) return
217
+ if (info.type === 'step' && info.action === 'end') {
218
+ const step = data.steps.find(s => s.id === info.id) as StepChatStep
219
+ if (step) {
220
+ step.status = 'success'
221
+ step.duration = info.duration
222
+ const lastToolId = last(step.attempts[0].tools)?.id
223
+ const lastAttemptOfLastTool = findLast(step.attempts.map(a => a.tools).flat(), t => t?.id === lastToolId)
224
+ step.output = lastAttemptOfLastTool?.output
225
+ }
226
+ }
240
227
 
241
- //There might be a tool with status awaiting_approval, so we want to inform tool has already started
242
- if (!currentStep || !currentStep.attempts[0].tools) {
243
- const input = formatJson(info.data.input)
244
- const tool = tools.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
228
+ if (info.type === 'tool' && info.action === 'awaiting_approval') {
229
+ const tool = tools.find(({ id }) => id === info.data?.tool_id)
245
230
  data.steps.push({
246
231
  id: info.id,
247
232
  type: 'tool',
248
- status: 'running',
233
+ status: 'awaiting_approval',
249
234
  duration: info.duration || 0,
250
235
  input: info.data?.input,
251
236
  user_question: info.data?.user_question,
252
237
  attempts: [{
253
- tools: [{ ...tool, executionId: info.id, input }],
238
+ tools: [{
239
+ executionId: info.id,
240
+ id: info.data?.tool_id ?? '',
241
+ name: tool?.name ?? '',
242
+ goal: tool?.goal,
243
+ ...tool,
244
+ }],
254
245
  }],
255
246
  })
256
- } else {
257
- const toolInFirstAttempt = currentStep.attempts[0].tools?.find(t => t.executionId === info.id)
258
- //One step might have multiple tools. When in an approval mode, we might not have all the tools in the array yet.
259
- //So we make sure to add any tools that are not in there.
260
- if (!toolInFirstAttempt) {
247
+ data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
248
+ }
249
+
250
+ if (info.type === 'tool' && info.action === 'start') {
251
+ const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
252
+ if (!info.data) return
253
+
254
+ //There might be a tool with status awaiting_approval, so we want to inform tool has already started
255
+ if (!currentStep || !currentStep.attempts[0].tools) {
261
256
  const input = formatJson(info.data.input)
262
- const tool = tools?.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
263
- currentStep.attempts[info.data.attempt - 1].tools?.push({
264
- ...tool,
265
- executionId: info.id,
266
- input,
257
+ const tool = tools.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
258
+ data.steps.push({
259
+ id: info.id,
260
+ type: 'tool',
261
+ status: 'running',
262
+ duration: info.duration || 0,
263
+ input: info.data?.input,
264
+ user_question: info.data?.user_question,
265
+ attempts: [{
266
+ tools: [{ ...tool, executionId: info.id, input }],
267
+ }],
267
268
  })
268
269
  } else {
269
- const input = formatJson(info.data.input)
270
- if (info.data.attempt === 1) {
271
- toolInFirstAttempt.input = input
272
- } else {
273
- const tool = tools.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
274
- currentStep.attempts[info.data.attempt - 1] ??= { tools: [] }
270
+ const toolInFirstAttempt = currentStep.attempts[0].tools?.find(t => t.executionId === info.id)
271
+ //One step might have multiple tools. When in an approval mode, we might not have all the tools in the array yet.
272
+ //So we make sure to add any tools that are not in there.
273
+ if (!toolInFirstAttempt) {
274
+ const input = formatJson(info.data.input)
275
+ const tool = tools?.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
275
276
  currentStep.attempts[info.data.attempt - 1].tools?.push({
276
277
  ...tool,
277
278
  executionId: info.id,
278
279
  input,
279
280
  })
281
+ } else {
282
+ const input = formatJson(info.data.input)
283
+ if (info.data.attempt === 1) {
284
+ toolInFirstAttempt.input = input
285
+ } else {
286
+ const tool = tools.find(({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
287
+ currentStep.attempts[info.data.attempt - 1] ??= { tools: [] }
288
+ currentStep.attempts[info.data.attempt - 1].tools?.push({
289
+ ...tool,
290
+ executionId: info.id,
291
+ input,
292
+ })
293
+ }
280
294
  }
281
295
  }
282
296
  }
283
- }
284
297
 
285
- if (info.type === 'tool' && info.action === 'end') {
286
- const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
287
- if (!currentStep || !info.data) return
288
- const tool = currentStep.attempts[info.data.attempt - 1]?.tools?.find(t => t.executionId === info.id)
289
- if (tool) {
290
- tool.output = formatJson(info.data.output)
291
- tool.duration = info.duration
298
+ if (info.type === 'tool' && info.action === 'end') {
299
+ const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
300
+ if (!currentStep || !info.data) return
301
+ const tool = currentStep.attempts[info.data.attempt - 1]?.tools?.find(t => t.executionId === info.id)
302
+ if (tool) {
303
+ tool.output = formatJson(info.data.output)
304
+ tool.duration = info.duration
305
+ }
292
306
  }
293
- }
294
307
 
295
- if (info.type === 'final_answer' && info.action === 'start') {
296
- const answerStep = last(data.steps)
297
- if (answerStep) answerStep.status = 'running'
298
- }
308
+ if (info.type === 'final_answer' && info.action === 'start') {
309
+ const answerStep = last(data.steps)
310
+ if (answerStep) answerStep.status = 'running'
311
+ }
299
312
 
300
- if (info.type === 'chat' && info.action === 'end') {
301
- const answerStep = last(data.steps)
302
- if (answerStep) {
303
- answerStep.status = 'success'
304
- answerStep.duration = info.duration
313
+ if (info.type === 'chat' && info.action === 'end') {
314
+ const answerStep = last(data.steps)
315
+ if (answerStep) {
316
+ answerStep.status = 'success'
317
+ answerStep.duration = info.duration
318
+ }
305
319
  }
306
320
  }
307
321
  }
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { HttpError } from '@oazapfts/runtime'
3
+ import { findLast, last } from 'lodash'
3
4
  import { getApiAddresses } from '../api-addresses'
4
5
  import { addSelfHostedModelV1LlmModelsPost, agentChatV1AgentAgentIdChatPost, defaults, deleteModelResourcesV1LlmResourcesResourceIdDelete, deleteV1LlmModelsModelIdDelete, getModelV1LlmModelsModelIdGet, listLlmProvidersV1LlmProvidersGet, listModelsV1LlmModelsGet, saveOrUpdateModelResourcesV1LlmModelsModelIdResourcesPut, toggleModelStatusV1LlmModelsModelIdPatch, updateV1LlmModelsModelIdPut } from '../api/genAiInference'
5
6
  import { DefaultAPIError } from '../error/DefaultAPIError'
@@ -7,6 +8,10 @@ import { inferenceDictionary } from '../error/dictionary/ai-inference'
7
8
  import { StackspotAPIError } from '../error/StackspotAPIError'
8
9
  import { ReactQueryNetworkClient } from '../network/ReactQueryNetworkClient'
9
10
  import { removeAuthorizationParam } from '../utils/remove-authorization-param'
11
+ import { StreamedJson } from '../utils/StreamedJson'
12
+ import { formatJson } from '../utils/string'
13
+ import { agentToolsClient } from './agent-tools'
14
+ import { AgentInfo, ChatAgentTool, ChatResponseWithSteps, FixedChatRequest, FixedChatResponse, StepChatStep } from './types'
10
15
 
11
16
  class GenAiInference extends ReactQueryNetworkClient {
12
17
  constructor() {
@@ -60,6 +65,282 @@ class GenAiInference extends ReactQueryNetworkClient {
60
65
  * Interaction with a specific AI agent
61
66
  */
62
67
  sendAgentMessage = this.mutation(removeAuthorizationParam(agentChatV1AgentAgentIdChatPost))
68
+
69
+
70
+ private static async toolsOfAgent(agentId?: string) {
71
+ try {
72
+ const agent = agentId ? await agentToolsClient.agent.query({ agentId }) : undefined
73
+ if (!agent) return []
74
+ const tools: (Omit<ChatAgentTool, 'duration' | 'prompt' | 'output'>)[] = []
75
+ agent.toolkits?.builtin_toolkits?.forEach(kit => kit.tools?.forEach(({ id, name, description }) => {
76
+ if (id) tools.push({ image: kit.image_url, id, name: name || id, description })
77
+ }))
78
+ agent.toolkits?.custom_toolkits?.forEach(kit => kit.tools?.forEach(({ id, name, description }) => {
79
+ if (id) tools.push({ image: kit.avatar ?? undefined, id, name: name || id, description })
80
+ }))
81
+ return tools
82
+ } catch {
83
+ return []
84
+ }
85
+ }
86
+
87
+ sendChatMessage(request: FixedChatRequest, minChangeIntervalMS?: number): StreamedJson<ChatResponseWithSteps> {
88
+ const abortController = new AbortController()
89
+ const { context, ...body } = request ?? {}
90
+ const headers = {
91
+ 'Content-Type': 'application/json',
92
+ 'Accept': 'text/event-stream',
93
+ 'x-platform': context?.platform ?? '',
94
+ 'x-os': context?.os ?? '',
95
+ 'x-platform-version': context?.platform_version ?? '',
96
+ 'x-stackspot-ai-version': context?.stackspot_ai_version ?? '',
97
+ 'x-request-origin': 'chat',
98
+ }
99
+ const events = this.stream(
100
+ this.resolveURL(`/v1/agent/${context?.agent_id}/chat`),
101
+ { method: 'post', body: JSON.stringify(body), headers, signal: abortController.signal },
102
+ )
103
+
104
+ const DYNAMIC_TOOL_ID = 'dynamic'
105
+ function isDynamicTool(info: AgentInfo) {
106
+ return info.type === 'tool' && info.id === DYNAMIC_TOOL_ID
107
+ }
108
+ /**
109
+ * This function treats events in the streaming that deals with the execution of tools. Since these events are not concatenated like
110
+ * normal streamings of data, we need this separate function to deal with them. It transforms the internal data model of the
111
+ * StreamedJson object whenever an event is triggered.
112
+ */
113
+ async function transform(event: Partial<FixedChatResponse>, data: Partial<ChatResponseWithSteps>) {
114
+ // The API send a final snapshot containing the full `agent_info`.
115
+ // When `stop_reason === 'stop'` we can ignore it to avoid duplicating steps.
116
+ if (event?.stop_reason === 'stop') return
117
+
118
+ const infoList = event.agent_info ?? []
119
+ if (infoList.length === 0) return
120
+
121
+ const tools = await GenAiInference.toolsOfAgent(request.context?.agent_id)
122
+ data.steps = data.steps ? [...data.steps] : []
123
+
124
+ for (const info of infoList) {
125
+
126
+ if (info.type === 'planning' && info.action === 'end') {
127
+ data.steps.push({
128
+ id: 'planning',
129
+ type: 'planning',
130
+ status: 'success',
131
+ duration: info.duration || 0,
132
+ steps: info.data?.steps?.map(s => s.goal) ?? [],
133
+ goal: info.data?.plan_goal ?? '',
134
+ })
135
+
136
+ info.data?.steps.forEach(s => data.steps?.push({
137
+ id: s.id,
138
+ type: 'step',
139
+ status: 'pending',
140
+ input: s.goal,
141
+ attempts: [{
142
+ tools: s.tools?.map(t => ({
143
+ ...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
144
+ executionId: t.tool_execution_id,
145
+ goal: t.goal,
146
+ })),
147
+ }],
148
+ }))
149
+ data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
150
+ }
151
+
152
+ if (info.type === 'planning' && info.action === 'awaiting_approval') {
153
+ data.steps.push({
154
+ id: 'planning',
155
+ type: 'planning',
156
+ status: 'awaiting_approval',
157
+ user_question: info.data?.user_question,
158
+ duration: info.duration || 0,
159
+ steps: info.data?.steps?.map(s => s.goal) ?? [],
160
+ goal: info.data?.plan_goal ?? '',
161
+ })
162
+ info.data?.steps.forEach(s => data.steps?.push({
163
+ id: s.id,
164
+ type: 'step',
165
+ status: 'pending',
166
+ input: s.goal,
167
+ attempts: [{
168
+ tools: s.tools?.map(t => ({
169
+ ...(tools.find(({ id }) => id === t.tool_id) ?? { id: t.tool_id, name: t.tool_id }),
170
+ executionId: t.tool_execution_id,
171
+ goal: t.goal,
172
+ })),
173
+ }],
174
+ }))
175
+ data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
176
+ }
177
+
178
+ if (info.type === 'step' && info.action === 'start') {
179
+ const step = data.steps.find(s => s.id === info.id)
180
+ if (step) step.status = 'running'
181
+ }
182
+
183
+ if (info.type === 'step' && info.action === 'end') {
184
+ const step = data.steps.find(s => s.id === info.id) as StepChatStep
185
+ if (step) {
186
+ step.status = 'success'
187
+ step.duration = info.duration
188
+ const lastToolId = last(step.attempts[0].tools)?.id
189
+ const lastAttemptOfLastTool = findLast(step.attempts.map(a => a.tools).flat(), t => t?.id === lastToolId)
190
+ step.output = lastAttemptOfLastTool?.output
191
+ }
192
+ }
193
+
194
+ if (info.type === 'tool_calls' && info.action === 'start') {
195
+ const hasPlanning = data.steps.find(s => s.type === 'planning')
196
+ // On the first tool_calls:start, create the synthetic planning ("dynamic") step.
197
+ if (!hasPlanning) {
198
+ const userPrompt = request.user_prompt === 'string' ? request.user_prompt : JSON.stringify(request.user_prompt)
199
+ data.steps.push({
200
+ id: 'dynamic',
201
+ type: 'planning',
202
+ status: 'success',
203
+ steps: [],
204
+ goal: userPrompt,
205
+ user_question: userPrompt,
206
+ })
207
+ }
208
+ const toolsStepId = data.steps.filter(s => s.id === 'tools' || s.id.startsWith('tools-')).length + 1
209
+ data.steps.push({
210
+ id: `tools-${toolsStepId.toString()}`,
211
+ type: 'step',
212
+ status: 'running',
213
+ attempts: [{ tools: [] }],
214
+ } as StepChatStep)
215
+ }
216
+
217
+ if (info.type === 'tool_calls' && info.action === 'end') {
218
+ const lastStep = findLast(data.steps, s => s.id === 'tools' || s.id.startsWith('tools-')) as StepChatStep
219
+ if (lastStep) {
220
+ lastStep.status = 'success'
221
+ lastStep.duration = info.duration
222
+ const lastAttemptOfLastTool = last(lastStep.attempts.map(a => a.tools).flat())
223
+ lastStep.output = lastAttemptOfLastTool?.output
224
+ }
225
+ }
226
+
227
+ if (info.type === 'tool' && info.action === 'awaiting_approval') {
228
+ const tool = tools.find(({ id }) => id === info.data?.tool_id)
229
+ data.steps.push({
230
+ id: info.id,
231
+ type: 'tool',
232
+ status: 'awaiting_approval',
233
+ duration: info.duration || 0,
234
+ input: info.data?.input,
235
+ user_question: info.data?.user_question,
236
+ attempts: [{
237
+ tools: [{
238
+ executionId: info.id,
239
+ id: info.data?.tool_id ?? '',
240
+ name: tool?.name ?? '',
241
+ goal: tool?.goal,
242
+ ...tool,
243
+ }],
244
+ }],
245
+ })
246
+ data.steps.push({ id: 'answer', type: 'answer', status: 'pending' })
247
+ }
248
+
249
+ if (info.type === 'tool' && info.action === 'start') {
250
+ if (!info.data) return
251
+ const input = formatJson(info.data.input)
252
+ const tool = findLast(tools, ({ id }) => id === info.data?.tool_id) ?? { id: info.data?.tool_id, name: info.data?.tool_id }
253
+
254
+ const currentStep = findLast(data.steps, s => s.status === 'running') as StepChatStep
255
+
256
+ //There might be a tool with status awaiting_approval, so we want to inform tool has already started
257
+ if (!currentStep || !currentStep?.attempts?.[0]?.tools) {
258
+ data.steps.push({
259
+ id: info.id,
260
+ type: 'tool',
261
+ status: 'running',
262
+ duration: info.duration || 0,
263
+ input: info.data?.input,
264
+ user_question: info.data?.user_question,
265
+ attempts: [{
266
+ tools: [{ ...tool, executionId: info.id, input }],
267
+ }],
268
+ })
269
+ } else {
270
+ const toolInFirstAttempt = findLast(currentStep?.attempts?.[0]?.tools, t => t.executionId === info.id)
271
+ //One step might have multiple tools. When in an approval mode, we might not have all the tools in the array yet.
272
+ //For dynamic tools (id === 'dynamic'), we always push a new tool, since dynamic executions can trigger
273
+ //multiple tool runs in the same step and do not follow the planned tool structure.
274
+ //So we make sure to add any tools that are not in there, or always add for dynamic tools.
275
+ if (!toolInFirstAttempt || isDynamicTool(info)) {
276
+ currentStep.attempts?.[0].tools?.push({
277
+ ...tool,
278
+ executionId: info.id,
279
+ input,
280
+ status: 'running',
281
+ })
282
+ } else {
283
+ const input = formatJson(info.data.input)
284
+ if (info.data.attempt === 1) {
285
+ toolInFirstAttempt.input = input
286
+ } else {
287
+ currentStep.attempts[info.data.attempt - 1] ??= { tools: [] }
288
+ currentStep.attempts[info.data.attempt - 1].tools?.push({
289
+ ...tool,
290
+ executionId: info.id,
291
+ input,
292
+ })
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ if (info.type === 'tool' && info.action === 'end') {
299
+ const currentStep = data.steps.find(s => s.status === 'running') as StepChatStep
300
+ if (!currentStep || !info.data) return
301
+
302
+ // attempt index for tool execution starts at 0 for dynamically executed tools,while for planned tools it starts at 1
303
+ const attempt = isDynamicTool(info) ? info.data.attempt : info.data.attempt - 1
304
+ const tool = last(currentStep?.attempts?.[attempt]?.tools)
305
+ if (tool) {
306
+ tool.output = formatJson(info.data.output)
307
+ tool.duration = info.duration
308
+ tool.status = 'success'
309
+ }
310
+ }
311
+
312
+ if (info.type === 'final_answer' && info.action === 'start') {
313
+ const answerStep = last(data.steps)
314
+ if (answerStep) answerStep.status = 'running'
315
+ }
316
+
317
+
318
+ if (info.type === 'chat' && info.action === 'end') {
319
+ const lastStep = last(data.steps)
320
+ if (lastStep?.type === 'answer') {
321
+ lastStep.status = 'success'
322
+ lastStep.duration = info.duration
323
+ } else {
324
+ data.steps.push({ id: 'answer', type: 'answer', status: 'success' })
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ return new StreamedJson({
331
+ eventsPromise: events,
332
+ abortController,
333
+ ignoreKeys: ['agent_info'],
334
+ transform,
335
+ textFromErrorEvent: data => data.message ?? 'Unknown error',
336
+ minChangeIntervalMS,
337
+ minChangeIntervalMSFromEvent: (event) => {
338
+ if (event?.agent_info?.length) return 0
339
+ return minChangeIntervalMS
340
+ },
341
+ })
342
+ }
343
+
63
344
  }
64
345
 
65
346
  export const genAiInferenceClient = new GenAiInference()