basalt-ui 1.10.0 → 1.11.0
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/AGENTS.md +2 -2
- package/CHANGELOG.md +10 -0
- package/dist/agent/ai-sdk-transport.d.ts +3 -52
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +108 -24
- package/dist/agent/coalesce.d.ts +26 -0
- package/dist/agent/coalesce.d.ts.map +1 -0
- package/dist/agent/coalesce.js +69 -0
- package/dist/agent/foreign.d.ts +97 -0
- package/dist/agent/foreign.d.ts.map +1 -0
- package/dist/agent/foreign.js +18 -0
- package/dist/agent/history.d.ts +2 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/id.d.ts +35 -0
- package/dist/agent/id.d.ts.map +1 -0
- package/dist/agent/id.js +14 -0
- package/dist/agent/index.d.ts +9 -4
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +15 -3
- package/dist/agent/merge.d.ts +8 -0
- package/dist/agent/merge.d.ts.map +1 -0
- package/dist/agent/merge.js +23 -0
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +16 -6
- package/dist/agent/parts.d.ts +100 -21
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +128 -11
- package/dist/agent/transport.d.ts +39 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +20 -17
- package/dist/agent/use-agent-stream.d.ts +2 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +5 -2
- package/dist/agent/use-agent-thread-runs.d.ts +9 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +75 -23
- package/dist/agent-chat/index.d.ts +2 -0
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/thread-message.d.ts +24 -7
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +102 -43
- package/dist/agent-chat/tool-chip.d.ts +21 -0
- package/dist/agent-chat/tool-chip.d.ts.map +1 -0
- package/dist/agent-chat/tool-chip.js +120 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/surfaces.d.ts +2 -2
- package/dist/surfaces.js +2 -2
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/src/agent/ai-sdk-transport.test.ts +338 -40
- package/src/agent/ai-sdk-transport.ts +206 -53
- package/src/agent/coalesce.test.ts +194 -0
- package/src/agent/coalesce.ts +118 -0
- package/src/agent/foreign.test.ts +47 -0
- package/src/agent/foreign.ts +140 -0
- package/src/agent/history.ts +2 -0
- package/src/agent/id.test.ts +71 -0
- package/src/agent/id.ts +43 -0
- package/src/agent/index.ts +26 -3
- package/src/agent/merge.test.ts +119 -0
- package/src/agent/merge.ts +67 -0
- package/src/agent/part-list.test.tsx +82 -0
- package/src/agent/part-list.tsx +31 -6
- package/src/agent/parts.test.ts +401 -59
- package/src/agent/parts.ts +262 -32
- package/src/agent/transport.test.ts +70 -0
- package/src/agent/transport.ts +72 -20
- package/src/agent/use-agent-stream.test.tsx +47 -17
- package/src/agent/use-agent-stream.ts +13 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +133 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +34 -6
- package/src/agent/use-agent-thread-runs.test.tsx +397 -14
- package/src/agent/use-agent-thread-runs.ts +170 -26
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +4 -2
- package/src/agent-chat/index.ts +4 -0
- package/src/agent-chat/thread-message.test.tsx +211 -0
- package/src/agent-chat/thread-message.tsx +235 -62
- package/src/agent-chat/tool-chip.test.tsx +139 -0
- package/src/agent-chat/tool-chip.tsx +219 -0
- package/src/guard/check-source.test.ts +8 -4
- package/src/guard/index.ts +1 -3
- package/src/index.ts +2 -0
- package/src/surfaces.ts +2 -2
package/src/agent/parts.test.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* parseAgentPart / isStartPart — the runtime narrowing guard over the
|
|
2
|
+
* parseAgentPart / isStartPart / isToolCallSettled — the runtime narrowing guard over the
|
|
3
|
+
* AgentPart wire format, post-1.11.0 part identity + the seven-state ToolCallPart union.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Every AgentPart variant below is validated against parts.ts's `parseAgentPart`, `isStartPart`,
|
|
6
|
+
* and `isToolCallSettled` as they read right now: `id` is required on every variant, and `tool`
|
|
7
|
+
* parts are validated against the seven-state discriminated union mirroring AI SDK v7's
|
|
8
|
+
* `UIToolInvocation` (see parts.ts's ToolCallPart doc for the ground-truth corrections against the
|
|
9
|
+
* design spec).
|
|
9
10
|
*/
|
|
10
11
|
import { describe, expect, test } from 'bun:test'
|
|
11
|
-
import { isStartPart, parseAgentPart } from './parts'
|
|
12
|
-
import type { AgentPart } from './parts'
|
|
12
|
+
import { isStartPart, isToolCallSettled, parseAgentPart } from './parts'
|
|
13
|
+
import type { AgentPart, ToolCallPart } from './parts'
|
|
13
14
|
|
|
14
15
|
/** JSON round-trip a value through JSON.stringify → JSON.parse, mirroring wire transport. */
|
|
15
16
|
function roundTrip(value: unknown): unknown {
|
|
@@ -19,13 +20,14 @@ function roundTrip(value: unknown): unknown {
|
|
|
19
20
|
describe('parseAgentPart', () => {
|
|
20
21
|
describe('start', () => {
|
|
21
22
|
test('parses a minimal start part (no resumeToken)', () => {
|
|
22
|
-
const raw = { type: 'start', runId: 'run-1' }
|
|
23
|
-
expect(parseAgentPart(raw)).toEqual({ type: 'start', runId: 'run-1' })
|
|
23
|
+
const raw = { id: 'p1', type: 'start', runId: 'run-1' }
|
|
24
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'start', runId: 'run-1' })
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
test('parses a start part with resumeToken', () => {
|
|
27
|
-
const raw = { type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
28
|
+
const raw = { id: 'p1', type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
28
29
|
expect(parseAgentPart(raw)).toEqual({
|
|
30
|
+
id: 'p1',
|
|
29
31
|
type: 'start',
|
|
30
32
|
runId: 'run-1',
|
|
31
33
|
resumeToken: 'tok-1',
|
|
@@ -33,120 +35,397 @@ describe('parseAgentPart', () => {
|
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
test('drops a non-string resumeToken rather than passing it through', () => {
|
|
36
|
-
const raw = { type: 'start', runId: 'run-1', resumeToken: 42 }
|
|
37
|
-
expect(parseAgentPart(raw)).toEqual({ type: 'start', runId: 'run-1' })
|
|
38
|
+
const raw = { id: 'p1', type: 'start', runId: 'run-1', resumeToken: 42 }
|
|
39
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'start', runId: 'run-1' })
|
|
38
40
|
})
|
|
39
41
|
|
|
40
42
|
test('rejects a missing runId', () => {
|
|
41
|
-
expect(parseAgentPart({ type: 'start' })).toBeNull()
|
|
43
|
+
expect(parseAgentPart({ id: 'p1', type: 'start' })).toBeNull()
|
|
42
44
|
})
|
|
43
45
|
|
|
44
46
|
test('rejects a non-string runId', () => {
|
|
45
|
-
expect(parseAgentPart({ type: 'start', runId: 42 })).toBeNull()
|
|
47
|
+
expect(parseAgentPart({ id: 'p1', type: 'start', runId: 42 })).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('rejects a missing id', () => {
|
|
51
|
+
expect(parseAgentPart({ type: 'start', runId: 'run-1' })).toBeNull()
|
|
46
52
|
})
|
|
47
53
|
|
|
48
54
|
test('round-trips through JSON', () => {
|
|
49
|
-
const part: AgentPart = { type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
55
|
+
const part: AgentPart = { id: 'p1', type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
50
56
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
51
57
|
})
|
|
52
58
|
})
|
|
53
59
|
|
|
54
60
|
describe('text', () => {
|
|
55
61
|
test('parses a text part', () => {
|
|
56
|
-
expect(parseAgentPart({ type: 'text', text: 'hello' })).toEqual({
|
|
62
|
+
expect(parseAgentPart({ id: 'p1', type: 'text', text: 'hello' })).toEqual({
|
|
63
|
+
id: 'p1',
|
|
57
64
|
type: 'text',
|
|
58
65
|
text: 'hello',
|
|
59
66
|
})
|
|
60
67
|
})
|
|
61
68
|
|
|
69
|
+
test('parses a text part with offset', () => {
|
|
70
|
+
const raw = { id: 'p1', type: 'text', text: 'llo', offset: 2 }
|
|
71
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'text', text: 'llo', offset: 2 })
|
|
72
|
+
})
|
|
73
|
+
|
|
62
74
|
test('rejects a missing text field', () => {
|
|
63
|
-
expect(parseAgentPart({ type: 'text' })).toBeNull()
|
|
75
|
+
expect(parseAgentPart({ id: 'p1', type: 'text' })).toBeNull()
|
|
64
76
|
})
|
|
65
77
|
|
|
66
78
|
test('rejects a non-string text field', () => {
|
|
67
|
-
expect(parseAgentPart({ type: 'text', text: 1 })).toBeNull()
|
|
79
|
+
expect(parseAgentPart({ id: 'p1', type: 'text', text: 1 })).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('rejects a missing id', () => {
|
|
83
|
+
expect(parseAgentPart({ type: 'text', text: 'hello' })).toBeNull()
|
|
68
84
|
})
|
|
69
85
|
|
|
70
86
|
test('round-trips through JSON', () => {
|
|
71
|
-
const part: AgentPart = { type: 'text', text: 'hello' }
|
|
87
|
+
const part: AgentPart = { id: 'p1', type: 'text', text: 'hello', offset: 0 }
|
|
72
88
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
73
89
|
})
|
|
74
90
|
})
|
|
75
91
|
|
|
76
92
|
describe('reasoning', () => {
|
|
77
93
|
test('parses a reasoning part', () => {
|
|
78
|
-
expect(parseAgentPart({ type: 'reasoning', text: 'thinking...' })).toEqual({
|
|
94
|
+
expect(parseAgentPart({ id: 'p1', type: 'reasoning', text: 'thinking...' })).toEqual({
|
|
95
|
+
id: 'p1',
|
|
79
96
|
type: 'reasoning',
|
|
80
97
|
text: 'thinking...',
|
|
81
98
|
})
|
|
82
99
|
})
|
|
83
100
|
|
|
84
101
|
test('rejects a missing text field', () => {
|
|
85
|
-
expect(parseAgentPart({ type: 'reasoning' })).toBeNull()
|
|
102
|
+
expect(parseAgentPart({ id: 'p1', type: 'reasoning' })).toBeNull()
|
|
86
103
|
})
|
|
87
104
|
|
|
88
105
|
test('round-trips through JSON', () => {
|
|
89
|
-
const part: AgentPart = { type: 'reasoning', text: 'thinking...' }
|
|
106
|
+
const part: AgentPart = { id: 'p1', type: 'reasoning', text: 'thinking...' }
|
|
90
107
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
91
108
|
})
|
|
92
109
|
})
|
|
93
110
|
|
|
94
111
|
describe('tool', () => {
|
|
95
|
-
test('
|
|
96
|
-
const raw = {
|
|
97
|
-
|
|
112
|
+
test('rejects a missing toolCallId', () => {
|
|
113
|
+
const raw = {
|
|
114
|
+
id: 'p1',
|
|
98
115
|
type: 'tool',
|
|
99
116
|
toolName: 'search',
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
state: 'input-available',
|
|
118
|
+
input: {},
|
|
119
|
+
}
|
|
120
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('rejects a missing toolName', () => {
|
|
124
|
+
const raw = {
|
|
125
|
+
id: 'p1',
|
|
126
|
+
type: 'tool',
|
|
127
|
+
toolCallId: 'call-1',
|
|
128
|
+
state: 'input-available',
|
|
129
|
+
input: {},
|
|
130
|
+
}
|
|
131
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('rejects a missing state', () => {
|
|
135
|
+
const raw = { id: 'p1', type: 'tool', toolCallId: 'call-1', toolName: 'search', input: {} }
|
|
136
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('rejects an unknown state', () => {
|
|
140
|
+
const raw = {
|
|
141
|
+
id: 'p1',
|
|
142
|
+
type: 'tool',
|
|
143
|
+
toolCallId: 'call-1',
|
|
144
|
+
toolName: 'search',
|
|
145
|
+
state: 'running',
|
|
146
|
+
input: {},
|
|
147
|
+
}
|
|
148
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
102
149
|
})
|
|
103
150
|
|
|
104
|
-
test('
|
|
151
|
+
test('rejects the pre-1.11.0 flat shape (no `state` field at all)', () => {
|
|
105
152
|
const raw = {
|
|
153
|
+
id: 'p1',
|
|
106
154
|
type: 'tool',
|
|
107
155
|
toolName: 'search',
|
|
156
|
+
toolCallId: 'call-1',
|
|
108
157
|
input: { q: 'x' },
|
|
109
158
|
output: { hits: 3 },
|
|
159
|
+
}
|
|
160
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// ── the seven states ────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
test('input-streaming: parses with no input', () => {
|
|
166
|
+
const raw = {
|
|
167
|
+
id: 'p1',
|
|
168
|
+
type: 'tool',
|
|
110
169
|
toolCallId: 'call-1',
|
|
170
|
+
toolName: 'search',
|
|
171
|
+
state: 'input-streaming',
|
|
111
172
|
}
|
|
112
173
|
expect(parseAgentPart(raw)).toEqual({
|
|
174
|
+
id: 'p1',
|
|
113
175
|
type: 'tool',
|
|
176
|
+
toolCallId: 'call-1',
|
|
114
177
|
toolName: 'search',
|
|
178
|
+
state: 'input-streaming',
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('input-streaming: parses with a partial input', () => {
|
|
183
|
+
const raw = {
|
|
184
|
+
id: 'p1',
|
|
185
|
+
type: 'tool',
|
|
186
|
+
toolCallId: 'call-1',
|
|
187
|
+
toolName: 'search',
|
|
188
|
+
state: 'input-streaming',
|
|
115
189
|
input: { q: 'x' },
|
|
116
|
-
|
|
190
|
+
}
|
|
191
|
+
expect(parseAgentPart(raw)).toEqual({
|
|
192
|
+
id: 'p1',
|
|
193
|
+
type: 'tool',
|
|
117
194
|
toolCallId: 'call-1',
|
|
195
|
+
toolName: 'search',
|
|
196
|
+
state: 'input-streaming',
|
|
197
|
+
input: { q: 'x' },
|
|
118
198
|
})
|
|
119
199
|
})
|
|
120
200
|
|
|
121
|
-
test('
|
|
122
|
-
|
|
201
|
+
test('input-available: requires input', () => {
|
|
202
|
+
const raw = {
|
|
203
|
+
id: 'p1',
|
|
204
|
+
type: 'tool',
|
|
205
|
+
toolCallId: 'call-1',
|
|
206
|
+
toolName: 'search',
|
|
207
|
+
state: 'input-available',
|
|
208
|
+
}
|
|
209
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
123
210
|
})
|
|
124
211
|
|
|
125
|
-
test('
|
|
126
|
-
|
|
212
|
+
test('input-available: round-trips', () => {
|
|
213
|
+
const part: ToolCallPart = {
|
|
214
|
+
id: 'p1',
|
|
215
|
+
type: 'tool',
|
|
216
|
+
toolCallId: 'call-1',
|
|
217
|
+
toolName: 'search',
|
|
218
|
+
state: 'input-available',
|
|
219
|
+
input: { q: 'x' },
|
|
220
|
+
}
|
|
221
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
127
222
|
})
|
|
128
223
|
|
|
129
|
-
test('
|
|
130
|
-
|
|
224
|
+
test('approval-requested: round-trips', () => {
|
|
225
|
+
const part: ToolCallPart = {
|
|
226
|
+
id: 'p1',
|
|
227
|
+
type: 'tool',
|
|
228
|
+
toolCallId: 'call-1',
|
|
229
|
+
toolName: 'search',
|
|
230
|
+
state: 'approval-requested',
|
|
231
|
+
input: { q: 'x' },
|
|
232
|
+
approval: { id: 'appr-1' },
|
|
233
|
+
}
|
|
234
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
131
235
|
})
|
|
132
236
|
|
|
133
|
-
test('
|
|
134
|
-
const raw = {
|
|
135
|
-
|
|
237
|
+
test('approval-requested: rejects a missing approval', () => {
|
|
238
|
+
const raw = {
|
|
239
|
+
id: 'p1',
|
|
240
|
+
type: 'tool',
|
|
241
|
+
toolCallId: 'call-1',
|
|
242
|
+
toolName: 'search',
|
|
243
|
+
state: 'approval-requested',
|
|
244
|
+
input: { q: 'x' },
|
|
245
|
+
}
|
|
246
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
136
247
|
})
|
|
137
248
|
|
|
138
|
-
test('
|
|
139
|
-
const raw = {
|
|
140
|
-
|
|
249
|
+
test('approval-requested: rejects an approval that already carries approved (a verdict, not a pending request)', () => {
|
|
250
|
+
const raw = {
|
|
251
|
+
id: 'p1',
|
|
252
|
+
type: 'tool',
|
|
253
|
+
toolCallId: 'call-1',
|
|
254
|
+
toolName: 'search',
|
|
255
|
+
state: 'approval-requested',
|
|
256
|
+
input: { q: 'x' },
|
|
257
|
+
approval: { id: 'appr-1', approved: true },
|
|
258
|
+
}
|
|
259
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
141
260
|
})
|
|
142
261
|
|
|
143
|
-
test('
|
|
144
|
-
const
|
|
262
|
+
test('approval-requested: rejects an approval that already carries a reason', () => {
|
|
263
|
+
const raw = {
|
|
264
|
+
id: 'p1',
|
|
265
|
+
type: 'tool',
|
|
266
|
+
toolCallId: 'call-1',
|
|
267
|
+
toolName: 'search',
|
|
268
|
+
state: 'approval-requested',
|
|
269
|
+
input: { q: 'x' },
|
|
270
|
+
approval: { id: 'appr-1', reason: 'too risky' },
|
|
271
|
+
}
|
|
272
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('approval-responded: round-trips with approved + reason', () => {
|
|
276
|
+
const part: ToolCallPart = {
|
|
277
|
+
id: 'p1',
|
|
278
|
+
type: 'tool',
|
|
279
|
+
toolCallId: 'call-1',
|
|
280
|
+
toolName: 'search',
|
|
281
|
+
state: 'approval-responded',
|
|
282
|
+
input: { q: 'x' },
|
|
283
|
+
approval: { id: 'appr-1', approved: true, reason: 'looks safe' },
|
|
284
|
+
}
|
|
285
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('approval-responded: rejects a non-boolean approved', () => {
|
|
289
|
+
const raw = {
|
|
290
|
+
id: 'p1',
|
|
291
|
+
type: 'tool',
|
|
292
|
+
toolCallId: 'call-1',
|
|
293
|
+
toolName: 'search',
|
|
294
|
+
state: 'approval-responded',
|
|
295
|
+
input: { q: 'x' },
|
|
296
|
+
approval: { id: 'appr-1' },
|
|
297
|
+
}
|
|
298
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('output-denied: round-trips (approved: false)', () => {
|
|
302
|
+
const part: ToolCallPart = {
|
|
303
|
+
id: 'p1',
|
|
145
304
|
type: 'tool',
|
|
305
|
+
toolCallId: 'call-1',
|
|
146
306
|
toolName: 'search',
|
|
307
|
+
state: 'output-denied',
|
|
308
|
+
input: { q: 'x' },
|
|
309
|
+
approval: { id: 'appr-1', approved: false, reason: 'too risky' },
|
|
310
|
+
}
|
|
311
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('output-denied: rejects approved !== false', () => {
|
|
315
|
+
const raw = {
|
|
316
|
+
id: 'p1',
|
|
317
|
+
type: 'tool',
|
|
318
|
+
toolCallId: 'call-1',
|
|
319
|
+
toolName: 'search',
|
|
320
|
+
state: 'output-denied',
|
|
321
|
+
input: { q: 'x' },
|
|
322
|
+
approval: { id: 'appr-1', approved: true },
|
|
323
|
+
}
|
|
324
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('output-available: round-trips with output + preliminary', () => {
|
|
328
|
+
const part: ToolCallPart = {
|
|
329
|
+
id: 'p1',
|
|
330
|
+
type: 'tool',
|
|
331
|
+
toolCallId: 'call-1',
|
|
332
|
+
toolName: 'search',
|
|
333
|
+
state: 'output-available',
|
|
147
334
|
input: { q: 'x' },
|
|
148
335
|
output: { hits: 3 },
|
|
336
|
+
preliminary: true,
|
|
337
|
+
}
|
|
338
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('output-available: an optional approval carries through', () => {
|
|
342
|
+
const part: ToolCallPart = {
|
|
343
|
+
id: 'p1',
|
|
344
|
+
type: 'tool',
|
|
149
345
|
toolCallId: 'call-1',
|
|
346
|
+
toolName: 'search',
|
|
347
|
+
state: 'output-available',
|
|
348
|
+
input: { q: 'x' },
|
|
349
|
+
output: { hits: 3 },
|
|
350
|
+
approval: { id: 'appr-1', approved: true },
|
|
351
|
+
}
|
|
352
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('output-available: rejects a missing output', () => {
|
|
356
|
+
const raw = {
|
|
357
|
+
id: 'p1',
|
|
358
|
+
type: 'tool',
|
|
359
|
+
toolCallId: 'call-1',
|
|
360
|
+
toolName: 'search',
|
|
361
|
+
state: 'output-available',
|
|
362
|
+
input: { q: 'x' },
|
|
363
|
+
}
|
|
364
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('output-available: rejects approval.approved === false (a denial masquerading as output)', () => {
|
|
368
|
+
const raw = {
|
|
369
|
+
id: 'p1',
|
|
370
|
+
type: 'tool',
|
|
371
|
+
toolCallId: 'call-1',
|
|
372
|
+
toolName: 'search',
|
|
373
|
+
state: 'output-available',
|
|
374
|
+
input: { q: 'x' },
|
|
375
|
+
output: { hits: 3 },
|
|
376
|
+
approval: { id: 'appr-1', approved: false },
|
|
377
|
+
}
|
|
378
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('output-error: uses errorText, not error, and round-trips with rawInput', () => {
|
|
382
|
+
const part: ToolCallPart = {
|
|
383
|
+
id: 'p1',
|
|
384
|
+
type: 'tool',
|
|
385
|
+
toolCallId: 'call-1',
|
|
386
|
+
toolName: 'search',
|
|
387
|
+
state: 'output-error',
|
|
388
|
+
errorText: 'boom',
|
|
389
|
+
rawInput: '{"q": invalid json',
|
|
390
|
+
}
|
|
391
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('output-error: rejects a missing errorText', () => {
|
|
395
|
+
const raw = {
|
|
396
|
+
id: 'p1',
|
|
397
|
+
type: 'tool',
|
|
398
|
+
toolCallId: 'call-1',
|
|
399
|
+
toolName: 'search',
|
|
400
|
+
state: 'output-error',
|
|
401
|
+
}
|
|
402
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('output-error: rejects approval.approved === false (a denial masquerading as an error)', () => {
|
|
406
|
+
const raw = {
|
|
407
|
+
id: 'p1',
|
|
408
|
+
type: 'tool',
|
|
409
|
+
toolCallId: 'call-1',
|
|
410
|
+
toolName: 'search',
|
|
411
|
+
state: 'output-error',
|
|
412
|
+
errorText: 'boom',
|
|
413
|
+
approval: { id: 'appr-1', approved: false },
|
|
414
|
+
}
|
|
415
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('durationMs and providerExecuted carry through when present', () => {
|
|
419
|
+
const part: ToolCallPart = {
|
|
420
|
+
id: 'p1',
|
|
421
|
+
type: 'tool',
|
|
422
|
+
toolCallId: 'call-1',
|
|
423
|
+
toolName: 'search',
|
|
424
|
+
state: 'output-available',
|
|
425
|
+
input: { q: 'x' },
|
|
426
|
+
output: { hits: 3 },
|
|
427
|
+
durationMs: 42,
|
|
428
|
+
providerExecuted: true,
|
|
150
429
|
}
|
|
151
430
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
152
431
|
})
|
|
@@ -154,15 +433,17 @@ describe('parseAgentPart', () => {
|
|
|
154
433
|
|
|
155
434
|
describe('source', () => {
|
|
156
435
|
test('parses a minimal source part (no title)', () => {
|
|
157
|
-
expect(parseAgentPart({ type: 'source', url: 'https://example.com' })).toEqual({
|
|
436
|
+
expect(parseAgentPart({ id: 'p1', type: 'source', url: 'https://example.com' })).toEqual({
|
|
437
|
+
id: 'p1',
|
|
158
438
|
type: 'source',
|
|
159
439
|
url: 'https://example.com',
|
|
160
440
|
})
|
|
161
441
|
})
|
|
162
442
|
|
|
163
443
|
test('parses a source part with title', () => {
|
|
164
|
-
const raw = { type: 'source', url: 'https://example.com', title: 'Example' }
|
|
444
|
+
const raw = { id: 'p1', type: 'source', url: 'https://example.com', title: 'Example' }
|
|
165
445
|
expect(parseAgentPart(raw)).toEqual({
|
|
446
|
+
id: 'p1',
|
|
166
447
|
type: 'source',
|
|
167
448
|
url: 'https://example.com',
|
|
168
449
|
title: 'Example',
|
|
@@ -170,38 +451,44 @@ describe('parseAgentPart', () => {
|
|
|
170
451
|
})
|
|
171
452
|
|
|
172
453
|
test('rejects a missing url', () => {
|
|
173
|
-
expect(parseAgentPart({ type: 'source' })).toBeNull()
|
|
454
|
+
expect(parseAgentPart({ id: 'p1', type: 'source' })).toBeNull()
|
|
174
455
|
})
|
|
175
456
|
|
|
176
457
|
test('drops a non-string title rather than passing it through', () => {
|
|
177
|
-
const raw = { type: 'source', url: 'https://example.com', title: 7 }
|
|
178
|
-
expect(parseAgentPart(raw)).toEqual({ type: 'source', url: 'https://example.com' })
|
|
458
|
+
const raw = { id: 'p1', type: 'source', url: 'https://example.com', title: 7 }
|
|
459
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'source', url: 'https://example.com' })
|
|
179
460
|
})
|
|
180
461
|
|
|
181
462
|
test('round-trips through JSON', () => {
|
|
182
|
-
const part: AgentPart = {
|
|
463
|
+
const part: AgentPart = {
|
|
464
|
+
id: 'p1',
|
|
465
|
+
type: 'source',
|
|
466
|
+
url: 'https://example.com',
|
|
467
|
+
title: 'Example',
|
|
468
|
+
}
|
|
183
469
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
184
470
|
})
|
|
185
471
|
})
|
|
186
472
|
|
|
187
473
|
describe('error', () => {
|
|
188
474
|
test('parses an error part', () => {
|
|
189
|
-
expect(parseAgentPart({ type: 'error', message: 'boom' })).toEqual({
|
|
475
|
+
expect(parseAgentPart({ id: 'p1', type: 'error', message: 'boom' })).toEqual({
|
|
476
|
+
id: 'p1',
|
|
190
477
|
type: 'error',
|
|
191
478
|
message: 'boom',
|
|
192
479
|
})
|
|
193
480
|
})
|
|
194
481
|
|
|
195
482
|
test('rejects a missing message', () => {
|
|
196
|
-
expect(parseAgentPart({ type: 'error' })).toBeNull()
|
|
483
|
+
expect(parseAgentPart({ id: 'p1', type: 'error' })).toBeNull()
|
|
197
484
|
})
|
|
198
485
|
|
|
199
486
|
test('rejects a non-string message', () => {
|
|
200
|
-
expect(parseAgentPart({ type: 'error', message: 500 })).toBeNull()
|
|
487
|
+
expect(parseAgentPart({ id: 'p1', type: 'error', message: 500 })).toBeNull()
|
|
201
488
|
})
|
|
202
489
|
|
|
203
490
|
test('round-trips through JSON', () => {
|
|
204
|
-
const part: AgentPart = { type: 'error', message: 'boom' }
|
|
491
|
+
const part: AgentPart = { id: 'p1', type: 'error', message: 'boom' }
|
|
205
492
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
206
493
|
})
|
|
207
494
|
})
|
|
@@ -225,26 +512,26 @@ describe('parseAgentPart', () => {
|
|
|
225
512
|
})
|
|
226
513
|
|
|
227
514
|
test('rejects an object with no type field', () => {
|
|
228
|
-
expect(parseAgentPart({ text: 'hello' })).toBeNull()
|
|
515
|
+
expect(parseAgentPart({ id: 'p1', text: 'hello' })).toBeNull()
|
|
229
516
|
})
|
|
230
517
|
|
|
231
518
|
test('rejects an object with a non-string type field', () => {
|
|
232
|
-
expect(parseAgentPart({ type: 1 })).toBeNull()
|
|
519
|
+
expect(parseAgentPart({ id: 'p1', type: 1 })).toBeNull()
|
|
233
520
|
})
|
|
234
521
|
|
|
235
522
|
test('rejects an unknown type discriminant', () => {
|
|
236
|
-
expect(parseAgentPart({ type: 'unknown-variant', text: 'hi' })).toBeNull()
|
|
523
|
+
expect(parseAgentPart({ id: 'p1', type: 'unknown-variant', text: 'hi' })).toBeNull()
|
|
237
524
|
})
|
|
238
525
|
})
|
|
239
526
|
})
|
|
240
527
|
|
|
241
528
|
describe('isStartPart', () => {
|
|
242
529
|
test('returns true for a StartPart', () => {
|
|
243
|
-
expect(isStartPart({ type: 'start', runId: 'run-1' })).toBe(true)
|
|
530
|
+
expect(isStartPart({ id: 'p1', type: 'start', runId: 'run-1' })).toBe(true)
|
|
244
531
|
})
|
|
245
532
|
|
|
246
533
|
test('returns false for a non-start AgentPart', () => {
|
|
247
|
-
expect(isStartPart({ type: 'text', text: 'hi' })).toBe(false)
|
|
534
|
+
expect(isStartPart({ id: 'p1', type: 'text', text: 'hi' })).toBe(false)
|
|
248
535
|
})
|
|
249
536
|
|
|
250
537
|
test('returns false for null', () => {
|
|
@@ -259,3 +546,58 @@ describe('isStartPart', () => {
|
|
|
259
546
|
expect(isStartPart({ runId: 'run-1' })).toBe(false)
|
|
260
547
|
})
|
|
261
548
|
})
|
|
549
|
+
|
|
550
|
+
describe('isToolCallSettled', () => {
|
|
551
|
+
const base = { id: 'p1', type: 'tool', toolCallId: 'call-1', toolName: 'search' } as const
|
|
552
|
+
|
|
553
|
+
test('false for input-streaming', () => {
|
|
554
|
+
expect(isToolCallSettled({ ...base, state: 'input-streaming' })).toBe(false)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
test('false for input-available', () => {
|
|
558
|
+
expect(isToolCallSettled({ ...base, state: 'input-available', input: {} })).toBe(false)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('false for approval-requested', () => {
|
|
562
|
+
expect(
|
|
563
|
+
isToolCallSettled({
|
|
564
|
+
...base,
|
|
565
|
+
state: 'approval-requested',
|
|
566
|
+
input: {},
|
|
567
|
+
approval: { id: 'appr-1' },
|
|
568
|
+
}),
|
|
569
|
+
).toBe(false)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
test('false for approval-responded', () => {
|
|
573
|
+
expect(
|
|
574
|
+
isToolCallSettled({
|
|
575
|
+
...base,
|
|
576
|
+
state: 'approval-responded',
|
|
577
|
+
input: {},
|
|
578
|
+
approval: { id: 'appr-1', approved: true },
|
|
579
|
+
}),
|
|
580
|
+
).toBe(false)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
test('true for output-available', () => {
|
|
584
|
+
expect(isToolCallSettled({ ...base, state: 'output-available', input: {}, output: {} })).toBe(
|
|
585
|
+
true,
|
|
586
|
+
)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
test('true for output-error', () => {
|
|
590
|
+
expect(isToolCallSettled({ ...base, state: 'output-error', errorText: 'boom' })).toBe(true)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test('true for output-denied', () => {
|
|
594
|
+
expect(
|
|
595
|
+
isToolCallSettled({
|
|
596
|
+
...base,
|
|
597
|
+
state: 'output-denied',
|
|
598
|
+
input: {},
|
|
599
|
+
approval: { id: 'appr-1', approved: false },
|
|
600
|
+
}),
|
|
601
|
+
).toBe(true)
|
|
602
|
+
})
|
|
603
|
+
})
|