@tanstack/ai-devtools-core 0.5.17 → 0.5.18
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/dist/{production/Shell/EBEFU2GI.js → Shell/7YKZZJIO.js} +623 -173
- package/dist/Shell/{Y4V4YNYM.js → LR2PUWRS.js} +623 -173
- package/dist/Shell/{3BTXBRTQ.js → TTHBDBPP.js} +530 -141
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{Shell/EBEFU2GI.js → production/Shell/7YKZZJIO.js} +623 -173
- package/dist/production/Shell/{Y4V4YNYM.js → LR2PUWRS.js} +623 -173
- package/dist/production/Shell/{3BTXBRTQ.js → TTHBDBPP.js} +530 -141
- package/dist/production/dev.js +1 -1
- package/dist/production/index.js +1 -1
- package/dist/production/server.js +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/src/components/conversation/IterationTimeline.tsx +137 -45
- package/src/components/hooks/HookDetails.tsx +248 -86
- package/src/store/ai-context.tsx +3 -0
- package/src/store/subagent-steps.ts +387 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import type { Iteration, Message, ToolCall } from './ai-context'
|
|
2
|
+
|
|
3
|
+
/** One child agent from a chat snapshot. Nested children are listed too. */
|
|
4
|
+
export interface SubagentInfo {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
/** Agent names from the root, for example `researcher > writer`. */
|
|
8
|
+
path: string
|
|
9
|
+
status?: string
|
|
10
|
+
error?: string
|
|
11
|
+
/**
|
|
12
|
+
* Zero-based index of the user turn this child belongs to (the last root
|
|
13
|
+
* user message before it). A nested child takes its parent's turn.
|
|
14
|
+
*/
|
|
15
|
+
turn: number
|
|
16
|
+
messages: Array<unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Child steps in the shape the iteration timeline renders. */
|
|
20
|
+
export interface SubagentSteps {
|
|
21
|
+
iterations: Array<Iteration>
|
|
22
|
+
messages: Array<Message>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function text(value: unknown): string {
|
|
30
|
+
return typeof value === 'string' ? value : ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every subagent in a list of snapshot messages, in the order they appear.
|
|
35
|
+
* A nested child comes right after its parent.
|
|
36
|
+
*/
|
|
37
|
+
export function collectSubagents(
|
|
38
|
+
messages: Array<unknown>,
|
|
39
|
+
): Array<SubagentInfo> {
|
|
40
|
+
const out: Array<SubagentInfo> = []
|
|
41
|
+
let turn = -1
|
|
42
|
+
const visit = (
|
|
43
|
+
list: Array<unknown>,
|
|
44
|
+
parentPath: string | undefined,
|
|
45
|
+
parentTurn: number | undefined,
|
|
46
|
+
) => {
|
|
47
|
+
for (const message of list) {
|
|
48
|
+
if (!isRecord(message)) continue
|
|
49
|
+
if (parentTurn === undefined && message.role === 'user') turn += 1
|
|
50
|
+
if (!Array.isArray(message.parts)) continue
|
|
51
|
+
for (const part of message.parts) {
|
|
52
|
+
if (!isRecord(part) || part.type !== 'subagent') continue
|
|
53
|
+
const subagent = part.subagent
|
|
54
|
+
if (!isRecord(subagent) || typeof subagent.id !== 'string') continue
|
|
55
|
+
const name = text(subagent.name) || 'subagent'
|
|
56
|
+
const path = parentPath ? `${parentPath} > ${name}` : name
|
|
57
|
+
const error = isRecord(subagent.error)
|
|
58
|
+
? text(subagent.error.message)
|
|
59
|
+
: ''
|
|
60
|
+
const childMessages = Array.isArray(subagent.messages)
|
|
61
|
+
? subagent.messages
|
|
62
|
+
: []
|
|
63
|
+
const info: SubagentInfo = {
|
|
64
|
+
id: subagent.id,
|
|
65
|
+
name,
|
|
66
|
+
path,
|
|
67
|
+
...(typeof subagent.status === 'string'
|
|
68
|
+
? { status: subagent.status }
|
|
69
|
+
: {}),
|
|
70
|
+
...(error ? { error } : {}),
|
|
71
|
+
turn: parentTurn ?? Math.max(turn, 0),
|
|
72
|
+
messages: childMessages,
|
|
73
|
+
}
|
|
74
|
+
out.push(info)
|
|
75
|
+
visit(childMessages, path, info.turn)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
visit(messages, undefined, undefined)
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The subagent a server run belongs to. A child run id is
|
|
85
|
+
* `<parentRunId>:<subagentRunId>`, and a card id is the `subagentRunId`.
|
|
86
|
+
*/
|
|
87
|
+
export function subagentIdForRunId(
|
|
88
|
+
runId: string | undefined,
|
|
89
|
+
ids: ReadonlySet<string>,
|
|
90
|
+
): string | undefined {
|
|
91
|
+
if (!runId) return undefined
|
|
92
|
+
if (ids.has(runId)) return runId
|
|
93
|
+
const last = runId.slice(runId.lastIndexOf(':') + 1)
|
|
94
|
+
return last !== runId && ids.has(last) ? last : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One child's steps: from server iterations when present, else the snapshot. */
|
|
98
|
+
export interface AgentStepGroup {
|
|
99
|
+
agent: SubagentInfo
|
|
100
|
+
iterations: Array<Iteration>
|
|
101
|
+
messages: Array<Message>
|
|
102
|
+
source: 'server' | 'snapshot'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A user message, the root steps it started, and its child agents. */
|
|
106
|
+
export interface TimelineGroup {
|
|
107
|
+
userMessage: Message | null
|
|
108
|
+
iterations: Array<Iteration>
|
|
109
|
+
agents: Array<AgentStepGroup>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Request id to agent path, for server runs that belong to a child. */
|
|
113
|
+
export function childRequestAgents(
|
|
114
|
+
iterations: Array<Iteration>,
|
|
115
|
+
subagents: Array<SubagentInfo>,
|
|
116
|
+
): Map<string, string> {
|
|
117
|
+
const ids = new Set(subagents.map((agent) => agent.id))
|
|
118
|
+
const pathById = new Map(subagents.map((agent) => [agent.id, agent.path]))
|
|
119
|
+
const out = new Map<string, string>()
|
|
120
|
+
for (const iteration of iterations) {
|
|
121
|
+
const id = subagentIdForRunId(iteration.runId, ids)
|
|
122
|
+
const path = id ? pathById.get(id) : undefined
|
|
123
|
+
if (path && iteration.requestId) out.set(iteration.requestId, path)
|
|
124
|
+
}
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface StepSource {
|
|
129
|
+
iterations: Array<Iteration>
|
|
130
|
+
messages: Array<Message>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The root conversation's steps plus each child's server steps. A child run
|
|
135
|
+
* may have its own thread id (`<threadId>:<agent>` unless `sandbox: 'inherit'`),
|
|
136
|
+
* so the store can file its steps under another conversation. They are found
|
|
137
|
+
* by run id.
|
|
138
|
+
*/
|
|
139
|
+
export function collectServerSteps(
|
|
140
|
+
root: StepSource | undefined,
|
|
141
|
+
conversations: Array<StepSource>,
|
|
142
|
+
subagents: Array<SubagentInfo>,
|
|
143
|
+
): StepSource {
|
|
144
|
+
const ids = new Set(subagents.map((agent) => agent.id))
|
|
145
|
+
const iterations: Array<Iteration> = [...(root?.iterations ?? [])]
|
|
146
|
+
const messages: Array<Message> = [...(root?.messages ?? [])]
|
|
147
|
+
const seenSteps = new Set(
|
|
148
|
+
iterations.map((iteration) => `${iteration.requestId}:${iteration.index}`),
|
|
149
|
+
)
|
|
150
|
+
const seenMessages = new Set(messages.map((message) => message.id))
|
|
151
|
+
for (const conversation of conversations) {
|
|
152
|
+
if (conversation === root) continue
|
|
153
|
+
const child = conversation.iterations.filter((iteration) =>
|
|
154
|
+
subagentIdForRunId(iteration.runId, ids),
|
|
155
|
+
)
|
|
156
|
+
if (child.length === 0) continue
|
|
157
|
+
for (const iteration of child) {
|
|
158
|
+
const key = `${iteration.requestId}:${iteration.index}`
|
|
159
|
+
if (seenSteps.has(key)) continue
|
|
160
|
+
seenSteps.add(key)
|
|
161
|
+
iterations.push(iteration)
|
|
162
|
+
}
|
|
163
|
+
for (const message of conversation.messages) {
|
|
164
|
+
if (seenMessages.has(message.id)) continue
|
|
165
|
+
seenMessages.add(message.id)
|
|
166
|
+
messages.push(message)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { iterations, messages }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function timestampOf(value: unknown): number | undefined {
|
|
173
|
+
if (value instanceof Date) return value.getTime()
|
|
174
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined
|
|
175
|
+
const time = new Date(value).getTime()
|
|
176
|
+
return Number.isNaN(time) ? undefined : time
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The user messages of a chat snapshot, as the turns of the timeline. The
|
|
181
|
+
* snapshot is what the user sees, so it has every turn, also a routed turn
|
|
182
|
+
* with no parent server steps. Empty when a user message has no `createdAt`.
|
|
183
|
+
*/
|
|
184
|
+
export function snapshotTurns(messages: Array<unknown>): Array<Message> {
|
|
185
|
+
const turns: Array<Message> = []
|
|
186
|
+
for (const message of messages) {
|
|
187
|
+
if (!isRecord(message) || message.role !== 'user') continue
|
|
188
|
+
const timestamp = timestampOf(message.createdAt)
|
|
189
|
+
if (timestamp === undefined || typeof message.id !== 'string') return []
|
|
190
|
+
const content = Array.isArray(message.parts)
|
|
191
|
+
? message.parts
|
|
192
|
+
.map((part) =>
|
|
193
|
+
isRecord(part) && part.type === 'text' ? text(part.content) : '',
|
|
194
|
+
)
|
|
195
|
+
.join('')
|
|
196
|
+
: text(message.content)
|
|
197
|
+
turns.push({ id: message.id, role: 'user', content, timestamp })
|
|
198
|
+
}
|
|
199
|
+
return turns
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Group steps under the user message that started them. Child server steps
|
|
204
|
+
* leave the root list and join their agent. A child without server steps gets
|
|
205
|
+
* steps built from the snapshot. A child `chat()` re-sends the history, so a
|
|
206
|
+
* user message from a child request is not a new turn. Pass `turns` (from
|
|
207
|
+
* {@link snapshotTurns}) to use the chat's own user messages as the turns.
|
|
208
|
+
*/
|
|
209
|
+
export function groupTimeline(
|
|
210
|
+
iterations: Array<Iteration>,
|
|
211
|
+
messages: Array<Message>,
|
|
212
|
+
subagents: Array<SubagentInfo>,
|
|
213
|
+
turns: Array<Message> = [],
|
|
214
|
+
): Array<TimelineGroup> {
|
|
215
|
+
const ids = new Set(subagents.map((agent) => agent.id))
|
|
216
|
+
const childRequests = childRequestAgents(iterations, subagents)
|
|
217
|
+
const rootIterations = iterations.filter(
|
|
218
|
+
(iteration) => !subagentIdForRunId(iteration.runId, ids),
|
|
219
|
+
)
|
|
220
|
+
const agents: Array<AgentStepGroup> = subagents.map((agent) => {
|
|
221
|
+
const server = iterations.filter(
|
|
222
|
+
(iteration) => subagentIdForRunId(iteration.runId, ids) === agent.id,
|
|
223
|
+
)
|
|
224
|
+
return server.length > 0
|
|
225
|
+
? { agent, iterations: server, messages, source: 'server' }
|
|
226
|
+
: { agent, ...buildSubagentSteps(agent), source: 'snapshot' }
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// ponytail: snapshot turns use the browser clock and server steps use the
|
|
230
|
+
// server clock. Fine for local dev; a large skew can move a step one turn.
|
|
231
|
+
const users = (
|
|
232
|
+
turns.length > 0
|
|
233
|
+
? [...turns]
|
|
234
|
+
: messages.filter(
|
|
235
|
+
(message) =>
|
|
236
|
+
message.role === 'user' &&
|
|
237
|
+
!(message.requestId && childRequests.has(message.requestId)),
|
|
238
|
+
)
|
|
239
|
+
).sort((a, b) => a.timestamp - b.timestamp)
|
|
240
|
+
|
|
241
|
+
if (users.length === 0) {
|
|
242
|
+
return rootIterations.length > 0 || agents.length > 0
|
|
243
|
+
? [{ userMessage: null, iterations: rootIterations, agents }]
|
|
244
|
+
: []
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const groups: Array<TimelineGroup> = []
|
|
248
|
+
const first = users[0]
|
|
249
|
+
const early = first
|
|
250
|
+
? rootIterations.filter(
|
|
251
|
+
(iteration) => iteration.startedAt < first.timestamp,
|
|
252
|
+
)
|
|
253
|
+
: []
|
|
254
|
+
if (early.length > 0) {
|
|
255
|
+
groups.push({ userMessage: null, iterations: early, agents: [] })
|
|
256
|
+
}
|
|
257
|
+
users.forEach((user, turn) => {
|
|
258
|
+
const next = users[turn + 1]
|
|
259
|
+
const own = rootIterations.filter(
|
|
260
|
+
(iteration) =>
|
|
261
|
+
iteration.startedAt >= user.timestamp &&
|
|
262
|
+
(!next || iteration.startedAt < next.timestamp),
|
|
263
|
+
)
|
|
264
|
+
const isLast = turn === users.length - 1
|
|
265
|
+
const ownAgents = agents.filter((group) =>
|
|
266
|
+
isLast ? group.agent.turn >= turn : group.agent.turn === turn,
|
|
267
|
+
)
|
|
268
|
+
if (own.length > 0 || ownAgents.length > 0) {
|
|
269
|
+
groups.push({ userMessage: user, iterations: own, agents: ownAgents })
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
return groups
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function resultText(value: unknown): string {
|
|
276
|
+
if (typeof value === 'string') return value
|
|
277
|
+
if (value === undefined) return ''
|
|
278
|
+
try {
|
|
279
|
+
return JSON.stringify(value)
|
|
280
|
+
} catch {
|
|
281
|
+
return String(value)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Split one child's messages into steps. A step ends after its tool results,
|
|
287
|
+
* which is where the child's `chat()` starts its next iteration.
|
|
288
|
+
*/
|
|
289
|
+
export function buildSubagentSteps(agent: SubagentInfo): SubagentSteps {
|
|
290
|
+
const iterations: Array<Iteration> = []
|
|
291
|
+
const messages: Array<Message> = []
|
|
292
|
+
let current:
|
|
293
|
+
| { message: Message; results: Array<Message>; hasResult: boolean }
|
|
294
|
+
| undefined
|
|
295
|
+
|
|
296
|
+
const close = () => {
|
|
297
|
+
if (!current) return
|
|
298
|
+
const index = iterations.length
|
|
299
|
+
const { message, results } = current
|
|
300
|
+
const hasContent =
|
|
301
|
+
message.content ||
|
|
302
|
+
message.thinkingContent ||
|
|
303
|
+
(message.toolCalls?.length ?? 0) > 0 ||
|
|
304
|
+
results.length > 0
|
|
305
|
+
if (hasContent) {
|
|
306
|
+
messages.push(message, ...results)
|
|
307
|
+
iterations.push({
|
|
308
|
+
requestId: `subagent:${agent.id}`,
|
|
309
|
+
index,
|
|
310
|
+
messageId: message.id,
|
|
311
|
+
// ponytail: the snapshot has no timestamps, so steps have no duration.
|
|
312
|
+
startedAt: 1,
|
|
313
|
+
completedAt: 1,
|
|
314
|
+
finishReason:
|
|
315
|
+
(message.toolCalls?.length ?? 0) > 0 ? 'tool_calls' : 'stop',
|
|
316
|
+
middlewareEvents: [],
|
|
317
|
+
messageIds: [message.id, ...results.map((result) => result.id)],
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
current = undefined
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const open = () => {
|
|
324
|
+
const index = iterations.length
|
|
325
|
+
current = {
|
|
326
|
+
message: {
|
|
327
|
+
id: `${agent.id}:step-${index}`,
|
|
328
|
+
role: 'assistant',
|
|
329
|
+
content: '',
|
|
330
|
+
timestamp: 1,
|
|
331
|
+
toolCalls: [],
|
|
332
|
+
},
|
|
333
|
+
results: [],
|
|
334
|
+
hasResult: false,
|
|
335
|
+
}
|
|
336
|
+
return current
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const message of agent.messages) {
|
|
340
|
+
if (!isRecord(message) || message.role !== 'assistant') continue
|
|
341
|
+
if (!Array.isArray(message.parts)) continue
|
|
342
|
+
for (const part of message.parts) {
|
|
343
|
+
if (!isRecord(part)) continue
|
|
344
|
+
// A tool result joins the step of its call. Any other part after a
|
|
345
|
+
// result starts the next step.
|
|
346
|
+
const step =
|
|
347
|
+
part.type === 'tool-result'
|
|
348
|
+
? (current ?? open())
|
|
349
|
+
: current && !current.hasResult
|
|
350
|
+
? current
|
|
351
|
+
: (close(), open())
|
|
352
|
+
if (part.type === 'thinking') {
|
|
353
|
+
step.message.thinkingContent =
|
|
354
|
+
(step.message.thinkingContent ?? '') + text(part.content)
|
|
355
|
+
} else if (part.type === 'text') {
|
|
356
|
+
step.message.content += text(part.content)
|
|
357
|
+
} else if (part.type === 'tool-call') {
|
|
358
|
+
const toolCall: ToolCall = {
|
|
359
|
+
id: text(part.id),
|
|
360
|
+
name: text(part.name) || 'tool',
|
|
361
|
+
arguments: text(part.arguments),
|
|
362
|
+
state: text(part.state),
|
|
363
|
+
...(part.output !== undefined ? { result: part.output } : {}),
|
|
364
|
+
}
|
|
365
|
+
step.message.toolCalls?.push(toolCall)
|
|
366
|
+
} else if (part.type === 'tool-result') {
|
|
367
|
+
const toolCallId = text(part.toolCallId)
|
|
368
|
+
const content = resultText(part.content ?? part.error)
|
|
369
|
+
const call = step.message.toolCalls?.find((tc) => tc.id === toolCallId)
|
|
370
|
+
if (call && call.result === undefined) call.result = content
|
|
371
|
+
step.results.push({
|
|
372
|
+
id: `${agent.id}:result-${toolCallId || step.results.length}`,
|
|
373
|
+
role: 'tool',
|
|
374
|
+
content,
|
|
375
|
+
timestamp: 1,
|
|
376
|
+
})
|
|
377
|
+
step.hasResult = true
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
close()
|
|
382
|
+
|
|
383
|
+
const last = iterations.at(-1)
|
|
384
|
+
if (last && agent.status === 'running') delete last.completedAt
|
|
385
|
+
if (last && agent.status === 'error') last.finishReason = 'error'
|
|
386
|
+
return { iterations, messages }
|
|
387
|
+
}
|