basalt-ui 1.10.0 → 1.12.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 +21 -21
- package/CHANGELOG.md +25 -0
- package/README.md +36 -27
- package/agent/rules/basalt-content.md +16 -3
- package/dist/agent/adapter.d.ts +146 -0
- package/dist/agent/adapter.d.ts.map +1 -0
- package/dist/agent/adapter.js +473 -0
- package/dist/agent/adapter.test.d.ts +2 -0
- package/dist/agent/adapter.test.d.ts.map +1 -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/ai-sdk-transport.test.d.ts +2 -0
- package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
- package/dist/agent/coalesce.d.ts +42 -0
- package/dist/agent/coalesce.d.ts.map +1 -0
- package/dist/agent/coalesce.js +137 -0
- package/dist/agent/coalesce.test.d.ts +2 -0
- package/dist/agent/coalesce.test.d.ts.map +1 -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/foreign.test.d.ts +2 -0
- package/dist/agent/foreign.test.d.ts.map +1 -0
- package/dist/agent/history.d.ts +2 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/id.d.ts +65 -0
- package/dist/agent/id.d.ts.map +1 -0
- package/dist/agent/id.js +30 -0
- package/dist/agent/id.test.d.ts +2 -0
- package/dist/agent/id.test.d.ts.map +1 -0
- package/dist/agent/index.d.ts +11 -4
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +18 -3
- package/dist/agent/merge.d.ts +8 -0
- package/dist/agent/merge.d.ts.map +1 -0
- package/dist/agent/merge.js +35 -0
- package/dist/agent/merge.test.d.ts +2 -0
- package/dist/agent/merge.test.d.ts.map +1 -0
- package/dist/agent/part-list.d.ts +49 -34
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +53 -7
- package/dist/agent/part-list.test.d.ts +2 -0
- package/dist/agent/part-list.test.d.ts.map +1 -0
- package/dist/agent/parts.d.ts +135 -20
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +138 -11
- package/dist/agent/parts.test.d.ts +2 -0
- package/dist/agent/parts.test.d.ts.map +1 -0
- package/dist/agent/thread.d.ts +14 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/thread.js +8 -2
- package/dist/agent/thread.test.d.ts +2 -0
- package/dist/agent/thread.test.d.ts.map +1 -0
- 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/transport.test.d.ts +2 -0
- package/dist/agent/transport.test.d.ts.map +1 -0
- 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-stream.test.d.ts +2 -0
- package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
- 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.foreign.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.js +75 -23
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
- package/dist/agent-chat/composer.d.ts +130 -8
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +189 -23
- package/dist/agent-chat/composer.test.d.ts +2 -0
- package/dist/agent-chat/composer.test.d.ts.map +1 -0
- package/dist/agent-chat/index.d.ts +3 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- 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 +115 -45
- package/dist/agent-chat/thread-message.test.d.ts +2 -0
- package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-workspace.js +1 -1
- 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/agent-chat/tool-chip.test.d.ts +2 -0
- package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
- package/dist/charts/utils/domain.test.d.ts +2 -0
- package/dist/charts/utils/domain.test.d.ts.map +1 -0
- package/dist/cli/check-theme.test.d.ts +2 -0
- package/dist/cli/check-theme.test.d.ts.map +1 -0
- package/dist/cli/help.test.d.ts +2 -0
- package/dist/cli/help.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-sync.test.d.ts +2 -0
- package/dist/cli/init-sync.test.d.ts.map +1 -0
- package/dist/cli/placement-engine.test.d.ts +2 -0
- package/dist/cli/placement-engine.test.d.ts.map +1 -0
- package/dist/cli/tarball-parity.test.d.ts +2 -0
- package/dist/cli/tarball-parity.test.d.ts.map +1 -0
- package/dist/cli/template-format.test.d.ts +2 -0
- package/dist/cli/template-format.test.d.ts.map +1 -0
- package/dist/cli/template-ledger.test.d.ts +2 -0
- package/dist/cli/template-ledger.test.d.ts.map +1 -0
- package/dist/cli/tokens-css.test.d.ts +2 -0
- package/dist/cli/tokens-css.test.d.ts.map +1 -0
- package/dist/content/article-actions.test.d.ts +2 -0
- package/dist/content/article-actions.test.d.ts.map +1 -0
- package/dist/content/article-model.test.d.ts +2 -0
- package/dist/content/article-model.test.d.ts.map +1 -0
- package/dist/content/block-split.test.d.ts +2 -0
- package/dist/content/block-split.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts +37 -10
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/content/fence-block.js +80 -14
- package/dist/content/heading-components.d.ts.map +1 -1
- package/dist/content/heading-components.js +7 -4
- package/dist/content/heading-components.test.d.ts +2 -0
- package/dist/content/heading-components.test.d.ts.map +1 -0
- package/dist/content/index.d.ts +6 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +5 -1
- package/dist/content/markdown.d.ts +101 -14
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +141 -28
- package/dist/content/markdown.test.d.ts +2 -0
- package/dist/content/markdown.test.d.ts.map +1 -0
- package/dist/content/prose.module.css.test.d.ts +2 -0
- package/dist/content/prose.module.css.test.d.ts.map +1 -0
- package/dist/content/sanitize.d.ts +170 -0
- package/dist/content/sanitize.d.ts.map +1 -0
- package/dist/content/sanitize.js +178 -0
- package/dist/content/sanitize.test.d.ts +2 -0
- package/dist/content/sanitize.test.d.ts.map +1 -0
- package/dist/content/slug.test.d.ts +2 -0
- package/dist/content/slug.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.test.d.ts +2 -0
- package/dist/dashboard/stat-card.test.d.ts.map +1 -0
- package/dist/guard/check-source.test.d.ts +2 -0
- package/dist/guard/check-source.test.d.ts.map +1 -0
- package/dist/guard/guard-hook.test.d.ts +2 -0
- package/dist/guard/guard-hook.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/provider/index.test.d.ts +2 -0
- package/dist/provider/index.test.d.ts.map +1 -0
- package/dist/provider/lab-theme.test.d.ts +2 -0
- package/dist/provider/lab-theme.test.d.ts.map +1 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
- package/dist/styles.floor.test.d.ts +2 -0
- package/dist/styles.floor.test.d.ts.map +1 -0
- package/dist/surfaces.d.ts +5 -5
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +13 -4
- package/dist/theme/border-coverage.test.d.ts +18 -0
- package/dist/theme/border-coverage.test.d.ts.map +1 -0
- package/dist/theme/contrast.test.d.ts +2 -0
- package/dist/theme/contrast.test.d.ts.map +1 -0
- package/dist/theme/density-option.test.d.ts +2 -0
- package/dist/theme/density-option.test.d.ts.map +1 -0
- package/dist/theme/density-relations.test.d.ts +2 -0
- package/dist/theme/density-relations.test.d.ts.map +1 -0
- package/dist/theme/derive-theme.test.d.ts +2 -0
- package/dist/theme/derive-theme.test.d.ts.map +1 -0
- package/dist/theme/fonts-option.test.d.ts +2 -0
- package/dist/theme/fonts-option.test.d.ts.map +1 -0
- package/dist/theme/radius-option.test.d.ts +2 -0
- package/dist/theme/radius-option.test.d.ts.map +1 -0
- package/dist/theme/radius.test.d.ts +2 -0
- package/dist/theme/radius.test.d.ts.map +1 -0
- package/dist/theme/shadow-surfaces.test.d.ts +26 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
- package/dist/theme/spacing.test.d.ts +2 -0
- package/dist/theme/spacing.test.d.ts.map +1 -0
- package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
- package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-controls.test.d.ts +2 -0
- package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-state.test.d.ts +2 -0
- package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
- package/dist/theme-lab/theme-lab.test.d.ts +2 -0
- package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
- package/dist/tokens/build-fonts-css.test.d.ts +2 -0
- package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
- package/dist/tokens/build-radius-css.test.d.ts +2 -0
- package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
- package/dist/tokens/density.test.d.ts +2 -0
- package/dist/tokens/density.test.d.ts.map +1 -0
- package/dist/tokens/derive-radius.test.d.ts +2 -0
- package/dist/tokens/derive-radius.test.d.ts.map +1 -0
- package/dist/tokens/derive.test.d.ts +2 -0
- package/dist/tokens/derive.test.d.ts.map +1 -0
- package/dist/tokens/palette.test.d.ts +2 -0
- package/dist/tokens/palette.test.d.ts.map +1 -0
- package/dist/vite-color-mix.test.d.ts +2 -0
- package/dist/vite-color-mix.test.d.ts.map +1 -0
- package/dist/vite.test.d.ts +2 -0
- package/dist/vite.test.d.ts.map +1 -0
- package/llms.txt +7 -7
- package/package.json +6 -1
- package/src/agent/adapter.test.ts +1403 -0
- package/src/agent/adapter.ts +887 -0
- package/src/agent/ai-sdk-transport.test.ts +380 -55
- package/src/agent/ai-sdk-transport.ts +206 -53
- package/src/agent/coalesce.test.ts +387 -0
- package/src/agent/coalesce.ts +229 -0
- package/src/agent/foreign.test.ts +51 -0
- package/src/agent/foreign.ts +140 -0
- package/src/agent/history.ts +2 -0
- package/src/agent/id.test.ts +75 -0
- package/src/agent/id.ts +91 -0
- package/src/agent/index.ts +34 -3
- package/src/agent/merge.test.ts +206 -0
- package/src/agent/merge.ts +114 -0
- package/src/agent/part-list.test.tsx +112 -0
- package/src/agent/part-list.tsx +108 -35
- package/src/agent/parts.test.ts +516 -57
- package/src/agent/parts.ts +323 -32
- package/src/agent/thread.test.ts +75 -2
- package/src/agent/thread.ts +21 -1
- package/src/agent/transport.test.ts +70 -0
- package/src/agent/transport.ts +72 -20
- package/src/agent/use-agent-stream.test.tsx +53 -19
- package/src/agent/use-agent-stream.ts +13 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +138 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +44 -11
- package/src/agent/use-agent-thread-runs.test.tsx +418 -22
- package/src/agent/use-agent-thread-runs.ts +170 -26
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +15 -7
- package/src/agent-chat/composer.test.tsx +924 -0
- package/src/agent-chat/composer.tsx +521 -52
- package/src/agent-chat/index.ts +5 -1
- package/src/agent-chat/thread-detail-panel.tsx +3 -1
- package/src/agent-chat/thread-message.test.tsx +351 -0
- package/src/agent-chat/thread-message.tsx +262 -64
- package/src/agent-chat/thread-workspace.tsx +1 -1
- package/src/agent-chat/tool-chip.test.tsx +139 -0
- package/src/agent-chat/tool-chip.tsx +219 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/template-format.test.ts +2 -2
- package/src/content/fence-block.tsx +242 -15
- package/src/content/heading-components.test.tsx +65 -0
- package/src/content/heading-components.tsx +26 -5
- package/src/content/index.ts +19 -3
- package/src/content/markdown.test.tsx +603 -0
- package/src/content/markdown.tsx +355 -35
- package/src/content/sanitize.test.ts +659 -0
- package/src/content/sanitize.ts +510 -0
- package/src/dashboard/stat-card.test.tsx +6 -1
- package/src/guard/check-source.test.ts +8 -4
- package/src/guard/index.ts +1 -3
- package/src/index.ts +15 -1
- package/src/surfaces.ts +13 -4
- package/src/theme/contrast.test.ts +1 -4
- package/src/theme/density-relations.test.ts +4 -2
- package/src/theme/fonts-option.test.ts +7 -1
- package/src/theme/shadow-surfaces.test.ts +12 -6
- package/src/theme/use-basalt-spacing.test.tsx +2 -1
package/src/agent/parts.test.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
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, `toolCallId`
|
|
7
|
+
* is required on the three in-flight tool states and optional on the four settled ones (see the
|
|
8
|
+
* per-state identity block below), and `tool` parts are validated against the seven-state
|
|
9
|
+
* discriminated union mirroring AI SDK v7's
|
|
10
|
+
* `UIToolInvocation` (see parts.ts's ToolCallPart doc for the ground-truth corrections against the
|
|
11
|
+
* design spec).
|
|
9
12
|
*/
|
|
10
13
|
import { describe, expect, test } from 'bun:test'
|
|
11
|
-
import { isStartPart, parseAgentPart } from './parts'
|
|
12
|
-
import type { AgentPart } from './parts'
|
|
14
|
+
import { isStartPart, isToolCallSettled, parseAgentPart } from './parts'
|
|
15
|
+
import type { AgentPart, ToolCallPart, ToolCallState } from './parts'
|
|
13
16
|
|
|
14
17
|
/** JSON round-trip a value through JSON.stringify → JSON.parse, mirroring wire transport. */
|
|
15
18
|
function roundTrip(value: unknown): unknown {
|
|
@@ -19,13 +22,14 @@ function roundTrip(value: unknown): unknown {
|
|
|
19
22
|
describe('parseAgentPart', () => {
|
|
20
23
|
describe('start', () => {
|
|
21
24
|
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' })
|
|
25
|
+
const raw = { id: 'p1', type: 'start', runId: 'run-1' }
|
|
26
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'start', runId: 'run-1' })
|
|
24
27
|
})
|
|
25
28
|
|
|
26
29
|
test('parses a start part with resumeToken', () => {
|
|
27
|
-
const raw = { type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
30
|
+
const raw = { id: 'p1', type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
28
31
|
expect(parseAgentPart(raw)).toEqual({
|
|
32
|
+
id: 'p1',
|
|
29
33
|
type: 'start',
|
|
30
34
|
runId: 'run-1',
|
|
31
35
|
resumeToken: 'tok-1',
|
|
@@ -33,120 +37,512 @@ describe('parseAgentPart', () => {
|
|
|
33
37
|
})
|
|
34
38
|
|
|
35
39
|
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' })
|
|
40
|
+
const raw = { id: 'p1', type: 'start', runId: 'run-1', resumeToken: 42 }
|
|
41
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'start', runId: 'run-1' })
|
|
38
42
|
})
|
|
39
43
|
|
|
40
44
|
test('rejects a missing runId', () => {
|
|
41
|
-
expect(parseAgentPart({ type: 'start' })).toBeNull()
|
|
45
|
+
expect(parseAgentPart({ id: 'p1', type: 'start' })).toBeNull()
|
|
42
46
|
})
|
|
43
47
|
|
|
44
48
|
test('rejects a non-string runId', () => {
|
|
45
|
-
expect(parseAgentPart({ type: 'start', runId: 42 })).toBeNull()
|
|
49
|
+
expect(parseAgentPart({ id: 'p1', type: 'start', runId: 42 })).toBeNull()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('rejects a missing id', () => {
|
|
53
|
+
expect(parseAgentPart({ type: 'start', runId: 'run-1' })).toBeNull()
|
|
46
54
|
})
|
|
47
55
|
|
|
48
56
|
test('round-trips through JSON', () => {
|
|
49
|
-
const part: AgentPart = { type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
57
|
+
const part: AgentPart = { id: 'p1', type: 'start', runId: 'run-1', resumeToken: 'tok-1' }
|
|
50
58
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
51
59
|
})
|
|
52
60
|
})
|
|
53
61
|
|
|
54
62
|
describe('text', () => {
|
|
55
63
|
test('parses a text part', () => {
|
|
56
|
-
expect(parseAgentPart({ type: 'text', text: 'hello' })).toEqual({
|
|
64
|
+
expect(parseAgentPart({ id: 'p1', type: 'text', text: 'hello' })).toEqual({
|
|
65
|
+
id: 'p1',
|
|
57
66
|
type: 'text',
|
|
58
67
|
text: 'hello',
|
|
59
68
|
})
|
|
60
69
|
})
|
|
61
70
|
|
|
71
|
+
test('parses a text part with offset', () => {
|
|
72
|
+
const raw = { id: 'p1', type: 'text', text: 'llo', offset: 2 }
|
|
73
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'text', text: 'llo', offset: 2 })
|
|
74
|
+
})
|
|
75
|
+
|
|
62
76
|
test('rejects a missing text field', () => {
|
|
63
|
-
expect(parseAgentPart({ type: 'text' })).toBeNull()
|
|
77
|
+
expect(parseAgentPart({ id: 'p1', type: 'text' })).toBeNull()
|
|
64
78
|
})
|
|
65
79
|
|
|
66
80
|
test('rejects a non-string text field', () => {
|
|
67
|
-
expect(parseAgentPart({ type: 'text', text: 1 })).toBeNull()
|
|
81
|
+
expect(parseAgentPart({ id: 'p1', type: 'text', text: 1 })).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('rejects a missing id', () => {
|
|
85
|
+
expect(parseAgentPart({ type: 'text', text: 'hello' })).toBeNull()
|
|
68
86
|
})
|
|
69
87
|
|
|
70
88
|
test('round-trips through JSON', () => {
|
|
71
|
-
const part: AgentPart = { type: 'text', text: 'hello' }
|
|
89
|
+
const part: AgentPart = { id: 'p1', type: 'text', text: 'hello', offset: 0 }
|
|
72
90
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
73
91
|
})
|
|
74
92
|
})
|
|
75
93
|
|
|
76
94
|
describe('reasoning', () => {
|
|
77
95
|
test('parses a reasoning part', () => {
|
|
78
|
-
expect(parseAgentPart({ type: 'reasoning', text: 'thinking...' })).toEqual({
|
|
96
|
+
expect(parseAgentPart({ id: 'p1', type: 'reasoning', text: 'thinking...' })).toEqual({
|
|
97
|
+
id: 'p1',
|
|
79
98
|
type: 'reasoning',
|
|
80
99
|
text: 'thinking...',
|
|
81
100
|
})
|
|
82
101
|
})
|
|
83
102
|
|
|
84
103
|
test('rejects a missing text field', () => {
|
|
85
|
-
expect(parseAgentPart({ type: 'reasoning' })).toBeNull()
|
|
104
|
+
expect(parseAgentPart({ id: 'p1', type: 'reasoning' })).toBeNull()
|
|
86
105
|
})
|
|
87
106
|
|
|
88
107
|
test('round-trips through JSON', () => {
|
|
89
|
-
const part: AgentPart = { type: 'reasoning', text: 'thinking...' }
|
|
108
|
+
const part: AgentPart = { id: 'p1', type: 'reasoning', text: 'thinking...' }
|
|
90
109
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
91
110
|
})
|
|
92
111
|
})
|
|
93
112
|
|
|
94
113
|
describe('tool', () => {
|
|
95
|
-
test('
|
|
96
|
-
const raw = {
|
|
97
|
-
|
|
114
|
+
test('rejects a non-string toolCallId', () => {
|
|
115
|
+
const raw = {
|
|
116
|
+
id: 'p1',
|
|
98
117
|
type: 'tool',
|
|
118
|
+
toolCallId: 42,
|
|
119
|
+
toolName: 'search',
|
|
120
|
+
state: 'input-available',
|
|
121
|
+
input: {},
|
|
122
|
+
}
|
|
123
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// ── toolCallId identity, decided per state ──────────────────────────────
|
|
127
|
+
// An empty string is not a value any state accepts: '' normalizes to absent right here, so the
|
|
128
|
+
// pre-1.12.0 sentinel can never reach the type or a consumer of it. What an ABSENT id then
|
|
129
|
+
// means is a per-state decision (see Identified / MaybeIdentified in parts.ts):
|
|
130
|
+
// in-flight (input-streaming, input-available, approval-requested) → REJECTED. The part
|
|
131
|
+
// exists only to be closed by a later state, which re-supplies its toolName and input;
|
|
132
|
+
// unidentified it could never be closed and would render as a forever-pending chip.
|
|
133
|
+
// settled (approval-responded + the three terminal states) → KEPT, unidentified.
|
|
134
|
+
// It carries the OUTCOME; dropping it would erase how the call ended (silent data loss),
|
|
135
|
+
// while keeping it costs only the fold — coalesceParts renders it as its own block.
|
|
136
|
+
//
|
|
137
|
+
// The `satisfies Record<ToolCallState, …>` below is an authoring-time exhaustiveness gate: add
|
|
138
|
+
// a state to the union and this fixture stops typechecking until its identity behaviour is
|
|
139
|
+
// decided here too. It is an EDITOR/tsc guard only — the package tsconfig excludes `*.test.ts`
|
|
140
|
+
// from `bun run typecheck`, and `bun test` type-strips rather than typechecks — so the
|
|
141
|
+
// "covers every state exactly once" test below backs it with a runtime check of the same claim.
|
|
142
|
+
|
|
143
|
+
const UNIDENTIFIED_RAW = {
|
|
144
|
+
'input-streaming': { toolName: 'search', state: 'input-streaming' },
|
|
145
|
+
'input-available': { toolName: 'search', state: 'input-available', input: { q: 'x' } },
|
|
146
|
+
'approval-requested': {
|
|
147
|
+
toolName: 'search',
|
|
148
|
+
state: 'approval-requested',
|
|
149
|
+
input: { q: 'x' },
|
|
150
|
+
approval: { id: 'appr-1' },
|
|
151
|
+
},
|
|
152
|
+
'approval-responded': {
|
|
153
|
+
toolName: 'search',
|
|
154
|
+
state: 'approval-responded',
|
|
155
|
+
input: { q: 'x' },
|
|
156
|
+
approval: { id: 'appr-1', approved: true },
|
|
157
|
+
},
|
|
158
|
+
'output-available': {
|
|
99
159
|
toolName: 'search',
|
|
160
|
+
state: 'output-available',
|
|
100
161
|
input: { q: 'x' },
|
|
162
|
+
output: { hits: 3 },
|
|
163
|
+
},
|
|
164
|
+
'output-error': { toolName: 'search', state: 'output-error', errorText: 'boom' },
|
|
165
|
+
'output-denied': {
|
|
166
|
+
toolName: 'search',
|
|
167
|
+
state: 'output-denied',
|
|
168
|
+
input: { q: 'x' },
|
|
169
|
+
approval: { id: 'appr-1', approved: false },
|
|
170
|
+
},
|
|
171
|
+
} satisfies Record<ToolCallState, Record<string, unknown>>
|
|
172
|
+
|
|
173
|
+
const IN_FLIGHT_STATES = [
|
|
174
|
+
'input-streaming',
|
|
175
|
+
'input-available',
|
|
176
|
+
'approval-requested',
|
|
177
|
+
] as const satisfies readonly ToolCallState[]
|
|
178
|
+
|
|
179
|
+
const SETTLED_STATES = [
|
|
180
|
+
'approval-responded',
|
|
181
|
+
'output-available',
|
|
182
|
+
'output-error',
|
|
183
|
+
'output-denied',
|
|
184
|
+
] as const satisfies readonly ToolCallState[]
|
|
185
|
+
|
|
186
|
+
test('the in-flight/settled split covers every tool state exactly once', () => {
|
|
187
|
+
// Object.keys() is always typed string[] regardless of the object's key type — cast back to
|
|
188
|
+
// ToolCallState[], which every key of UNIDENTIFIED_RAW genuinely is (used as
|
|
189
|
+
// UNIDENTIFIED_RAW[state] elsewhere in this file).
|
|
190
|
+
expect([...IN_FLIGHT_STATES, ...SETTLED_STATES].toSorted()).toEqual(
|
|
191
|
+
(Object.keys(UNIDENTIFIED_RAW) as ToolCallState[]).toSorted(),
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
for (const state of IN_FLIGHT_STATES) {
|
|
196
|
+
test(`${state}: rejects a missing toolCallId — an in-flight part that could never be closed`, () => {
|
|
197
|
+
expect(parseAgentPart({ id: 'p1', type: 'tool', ...UNIDENTIFIED_RAW[state] })).toBeNull()
|
|
101
198
|
})
|
|
199
|
+
|
|
200
|
+
test(`${state}: rejects an empty-string toolCallId identically (it means absent)`, () => {
|
|
201
|
+
const raw = { id: 'p1', type: 'tool', toolCallId: '', ...UNIDENTIFIED_RAW[state] }
|
|
202
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const state of SETTLED_STATES) {
|
|
207
|
+
test(`${state}: a missing toolCallId keeps the part UNIDENTIFIED — it is never dropped`, () => {
|
|
208
|
+
const parsed = parseAgentPart({ id: 'p1', type: 'tool', ...UNIDENTIFIED_RAW[state] })
|
|
209
|
+
expect(parsed).not.toBeNull()
|
|
210
|
+
expect((parsed as ToolCallPart).state).toBe(state)
|
|
211
|
+
expect(Object.hasOwn(parsed as object, 'toolCallId')).toBe(false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test(`${state}: an empty-string toolCallId normalizes to absent, never to ""`, () => {
|
|
215
|
+
const raw = { id: 'p1', type: 'tool', toolCallId: '', ...UNIDENTIFIED_RAW[state] }
|
|
216
|
+
const parsed = parseAgentPart(raw)
|
|
217
|
+
expect(parsed).not.toBeNull()
|
|
218
|
+
expect(Object.hasOwn(parsed as object, 'toolCallId')).toBe(false)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test(`${state}: a real toolCallId still carries through`, () => {
|
|
222
|
+
const raw = { id: 'p1', type: 'tool', toolCallId: 'call-1', ...UNIDENTIFIED_RAW[state] }
|
|
223
|
+
expect((parseAgentPart(raw) as ToolCallPart | null)?.toolCallId).toBe('call-1')
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test('approval-responded: a real toolCallId round-trips through JSON', () => {
|
|
228
|
+
const part: ToolCallPart = {
|
|
229
|
+
id: 'p1',
|
|
230
|
+
type: 'tool',
|
|
231
|
+
toolCallId: 'call-1',
|
|
232
|
+
toolName: 'delete_file',
|
|
233
|
+
state: 'approval-responded',
|
|
234
|
+
input: { path: '/tmp/x' },
|
|
235
|
+
approval: { id: 'appr-1', approved: true },
|
|
236
|
+
}
|
|
237
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('rejects a missing toolName', () => {
|
|
241
|
+
const raw = {
|
|
242
|
+
id: 'p1',
|
|
243
|
+
type: 'tool',
|
|
244
|
+
toolCallId: 'call-1',
|
|
245
|
+
state: 'input-available',
|
|
246
|
+
input: {},
|
|
247
|
+
}
|
|
248
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('rejects a missing state', () => {
|
|
252
|
+
const raw = { id: 'p1', type: 'tool', toolCallId: 'call-1', toolName: 'search', input: {} }
|
|
253
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
102
254
|
})
|
|
103
255
|
|
|
104
|
-
test('
|
|
256
|
+
test('rejects an unknown state', () => {
|
|
105
257
|
const raw = {
|
|
258
|
+
id: 'p1',
|
|
106
259
|
type: 'tool',
|
|
260
|
+
toolCallId: 'call-1',
|
|
107
261
|
toolName: 'search',
|
|
262
|
+
state: 'running',
|
|
263
|
+
input: {},
|
|
264
|
+
}
|
|
265
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('rejects the pre-1.11.0 flat shape (no `state` field at all)', () => {
|
|
269
|
+
const raw = {
|
|
270
|
+
id: 'p1',
|
|
271
|
+
type: 'tool',
|
|
272
|
+
toolName: 'search',
|
|
273
|
+
toolCallId: 'call-1',
|
|
108
274
|
input: { q: 'x' },
|
|
109
275
|
output: { hits: 3 },
|
|
276
|
+
}
|
|
277
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// ── the seven states ────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
test('input-streaming: parses with no input', () => {
|
|
283
|
+
const raw = {
|
|
284
|
+
id: 'p1',
|
|
285
|
+
type: 'tool',
|
|
110
286
|
toolCallId: 'call-1',
|
|
287
|
+
toolName: 'search',
|
|
288
|
+
state: 'input-streaming',
|
|
111
289
|
}
|
|
112
290
|
expect(parseAgentPart(raw)).toEqual({
|
|
291
|
+
id: 'p1',
|
|
292
|
+
type: 'tool',
|
|
293
|
+
toolCallId: 'call-1',
|
|
294
|
+
toolName: 'search',
|
|
295
|
+
state: 'input-streaming',
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('input-streaming: parses with a partial input', () => {
|
|
300
|
+
const raw = {
|
|
301
|
+
id: 'p1',
|
|
113
302
|
type: 'tool',
|
|
303
|
+
toolCallId: 'call-1',
|
|
114
304
|
toolName: 'search',
|
|
305
|
+
state: 'input-streaming',
|
|
115
306
|
input: { q: 'x' },
|
|
116
|
-
|
|
307
|
+
}
|
|
308
|
+
expect(parseAgentPart(raw)).toEqual({
|
|
309
|
+
id: 'p1',
|
|
310
|
+
type: 'tool',
|
|
117
311
|
toolCallId: 'call-1',
|
|
312
|
+
toolName: 'search',
|
|
313
|
+
state: 'input-streaming',
|
|
314
|
+
input: { q: 'x' },
|
|
118
315
|
})
|
|
119
316
|
})
|
|
120
317
|
|
|
121
|
-
test('
|
|
122
|
-
|
|
318
|
+
test('input-available: requires input', () => {
|
|
319
|
+
const raw = {
|
|
320
|
+
id: 'p1',
|
|
321
|
+
type: 'tool',
|
|
322
|
+
toolCallId: 'call-1',
|
|
323
|
+
toolName: 'search',
|
|
324
|
+
state: 'input-available',
|
|
325
|
+
}
|
|
326
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('input-available: round-trips', () => {
|
|
330
|
+
const part: ToolCallPart = {
|
|
331
|
+
id: 'p1',
|
|
332
|
+
type: 'tool',
|
|
333
|
+
toolCallId: 'call-1',
|
|
334
|
+
toolName: 'search',
|
|
335
|
+
state: 'input-available',
|
|
336
|
+
input: { q: 'x' },
|
|
337
|
+
}
|
|
338
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
123
339
|
})
|
|
124
340
|
|
|
125
|
-
test('
|
|
126
|
-
|
|
341
|
+
test('approval-requested: round-trips', () => {
|
|
342
|
+
const part: ToolCallPart = {
|
|
343
|
+
id: 'p1',
|
|
344
|
+
type: 'tool',
|
|
345
|
+
toolCallId: 'call-1',
|
|
346
|
+
toolName: 'search',
|
|
347
|
+
state: 'approval-requested',
|
|
348
|
+
input: { q: 'x' },
|
|
349
|
+
approval: { id: 'appr-1' },
|
|
350
|
+
}
|
|
351
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
127
352
|
})
|
|
128
353
|
|
|
129
|
-
test('rejects a missing
|
|
130
|
-
|
|
354
|
+
test('approval-requested: rejects a missing approval', () => {
|
|
355
|
+
const raw = {
|
|
356
|
+
id: 'p1',
|
|
357
|
+
type: 'tool',
|
|
358
|
+
toolCallId: 'call-1',
|
|
359
|
+
toolName: 'search',
|
|
360
|
+
state: 'approval-requested',
|
|
361
|
+
input: { q: 'x' },
|
|
362
|
+
}
|
|
363
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
131
364
|
})
|
|
132
365
|
|
|
133
|
-
test('
|
|
134
|
-
const raw = {
|
|
135
|
-
|
|
366
|
+
test('approval-requested: rejects an approval that already carries approved (a verdict, not a pending request)', () => {
|
|
367
|
+
const raw = {
|
|
368
|
+
id: 'p1',
|
|
369
|
+
type: 'tool',
|
|
370
|
+
toolCallId: 'call-1',
|
|
371
|
+
toolName: 'search',
|
|
372
|
+
state: 'approval-requested',
|
|
373
|
+
input: { q: 'x' },
|
|
374
|
+
approval: { id: 'appr-1', approved: true },
|
|
375
|
+
}
|
|
376
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
136
377
|
})
|
|
137
378
|
|
|
138
|
-
test('
|
|
139
|
-
const raw = {
|
|
140
|
-
|
|
379
|
+
test('approval-requested: rejects an approval that already carries a reason', () => {
|
|
380
|
+
const raw = {
|
|
381
|
+
id: 'p1',
|
|
382
|
+
type: 'tool',
|
|
383
|
+
toolCallId: 'call-1',
|
|
384
|
+
toolName: 'search',
|
|
385
|
+
state: 'approval-requested',
|
|
386
|
+
input: { q: 'x' },
|
|
387
|
+
approval: { id: 'appr-1', reason: 'too risky' },
|
|
388
|
+
}
|
|
389
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
141
390
|
})
|
|
142
391
|
|
|
143
|
-
test('round-trips
|
|
144
|
-
const part:
|
|
392
|
+
test('approval-responded: round-trips with approved + reason', () => {
|
|
393
|
+
const part: ToolCallPart = {
|
|
394
|
+
id: 'p1',
|
|
395
|
+
type: 'tool',
|
|
396
|
+
toolCallId: 'call-1',
|
|
397
|
+
toolName: 'search',
|
|
398
|
+
state: 'approval-responded',
|
|
399
|
+
input: { q: 'x' },
|
|
400
|
+
approval: { id: 'appr-1', approved: true, reason: 'looks safe' },
|
|
401
|
+
}
|
|
402
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('approval-responded: rejects a non-boolean approved', () => {
|
|
406
|
+
const raw = {
|
|
407
|
+
id: 'p1',
|
|
408
|
+
type: 'tool',
|
|
409
|
+
toolCallId: 'call-1',
|
|
410
|
+
toolName: 'search',
|
|
411
|
+
state: 'approval-responded',
|
|
412
|
+
input: { q: 'x' },
|
|
413
|
+
approval: { id: 'appr-1' },
|
|
414
|
+
}
|
|
415
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('output-denied: round-trips (approved: false)', () => {
|
|
419
|
+
const part: ToolCallPart = {
|
|
420
|
+
id: 'p1',
|
|
421
|
+
type: 'tool',
|
|
422
|
+
toolCallId: 'call-1',
|
|
423
|
+
toolName: 'search',
|
|
424
|
+
state: 'output-denied',
|
|
425
|
+
input: { q: 'x' },
|
|
426
|
+
approval: { id: 'appr-1', approved: false, reason: 'too risky' },
|
|
427
|
+
}
|
|
428
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('output-denied: rejects approved !== false', () => {
|
|
432
|
+
const raw = {
|
|
433
|
+
id: 'p1',
|
|
434
|
+
type: 'tool',
|
|
435
|
+
toolCallId: 'call-1',
|
|
436
|
+
toolName: 'search',
|
|
437
|
+
state: 'output-denied',
|
|
438
|
+
input: { q: 'x' },
|
|
439
|
+
approval: { id: 'appr-1', approved: true },
|
|
440
|
+
}
|
|
441
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('output-available: round-trips with output + preliminary', () => {
|
|
445
|
+
const part: ToolCallPart = {
|
|
446
|
+
id: 'p1',
|
|
447
|
+
type: 'tool',
|
|
448
|
+
toolCallId: 'call-1',
|
|
449
|
+
toolName: 'search',
|
|
450
|
+
state: 'output-available',
|
|
451
|
+
input: { q: 'x' },
|
|
452
|
+
output: { hits: 3 },
|
|
453
|
+
preliminary: true,
|
|
454
|
+
}
|
|
455
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('output-available: an optional approval carries through', () => {
|
|
459
|
+
const part: ToolCallPart = {
|
|
460
|
+
id: 'p1',
|
|
461
|
+
type: 'tool',
|
|
462
|
+
toolCallId: 'call-1',
|
|
463
|
+
toolName: 'search',
|
|
464
|
+
state: 'output-available',
|
|
465
|
+
input: { q: 'x' },
|
|
466
|
+
output: { hits: 3 },
|
|
467
|
+
approval: { id: 'appr-1', approved: true },
|
|
468
|
+
}
|
|
469
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
test('output-available: rejects a missing output', () => {
|
|
473
|
+
const raw = {
|
|
474
|
+
id: 'p1',
|
|
475
|
+
type: 'tool',
|
|
476
|
+
toolCallId: 'call-1',
|
|
477
|
+
toolName: 'search',
|
|
478
|
+
state: 'output-available',
|
|
479
|
+
input: { q: 'x' },
|
|
480
|
+
}
|
|
481
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('output-available: rejects approval.approved === false (a denial masquerading as output)', () => {
|
|
485
|
+
const raw = {
|
|
486
|
+
id: 'p1',
|
|
145
487
|
type: 'tool',
|
|
488
|
+
toolCallId: 'call-1',
|
|
146
489
|
toolName: 'search',
|
|
490
|
+
state: 'output-available',
|
|
147
491
|
input: { q: 'x' },
|
|
148
492
|
output: { hits: 3 },
|
|
493
|
+
approval: { id: 'appr-1', approved: false },
|
|
494
|
+
}
|
|
495
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test('output-error: uses errorText, not error, and round-trips with rawInput', () => {
|
|
499
|
+
const part: ToolCallPart = {
|
|
500
|
+
id: 'p1',
|
|
501
|
+
type: 'tool',
|
|
502
|
+
toolCallId: 'call-1',
|
|
503
|
+
toolName: 'search',
|
|
504
|
+
state: 'output-error',
|
|
505
|
+
errorText: 'boom',
|
|
506
|
+
rawInput: '{"q": invalid json',
|
|
507
|
+
}
|
|
508
|
+
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('output-error: rejects a missing errorText', () => {
|
|
512
|
+
const raw = {
|
|
513
|
+
id: 'p1',
|
|
514
|
+
type: 'tool',
|
|
515
|
+
toolCallId: 'call-1',
|
|
516
|
+
toolName: 'search',
|
|
517
|
+
state: 'output-error',
|
|
518
|
+
}
|
|
519
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('output-error: rejects approval.approved === false (a denial masquerading as an error)', () => {
|
|
523
|
+
const raw = {
|
|
524
|
+
id: 'p1',
|
|
525
|
+
type: 'tool',
|
|
526
|
+
toolCallId: 'call-1',
|
|
527
|
+
toolName: 'search',
|
|
528
|
+
state: 'output-error',
|
|
529
|
+
errorText: 'boom',
|
|
530
|
+
approval: { id: 'appr-1', approved: false },
|
|
531
|
+
}
|
|
532
|
+
expect(parseAgentPart(raw)).toBeNull()
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('durationMs and providerExecuted carry through when present', () => {
|
|
536
|
+
const part: ToolCallPart = {
|
|
537
|
+
id: 'p1',
|
|
538
|
+
type: 'tool',
|
|
149
539
|
toolCallId: 'call-1',
|
|
540
|
+
toolName: 'search',
|
|
541
|
+
state: 'output-available',
|
|
542
|
+
input: { q: 'x' },
|
|
543
|
+
output: { hits: 3 },
|
|
544
|
+
durationMs: 42,
|
|
545
|
+
providerExecuted: true,
|
|
150
546
|
}
|
|
151
547
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
152
548
|
})
|
|
@@ -154,15 +550,17 @@ describe('parseAgentPart', () => {
|
|
|
154
550
|
|
|
155
551
|
describe('source', () => {
|
|
156
552
|
test('parses a minimal source part (no title)', () => {
|
|
157
|
-
expect(parseAgentPart({ type: 'source', url: 'https://example.com' })).toEqual({
|
|
553
|
+
expect(parseAgentPart({ id: 'p1', type: 'source', url: 'https://example.com' })).toEqual({
|
|
554
|
+
id: 'p1',
|
|
158
555
|
type: 'source',
|
|
159
556
|
url: 'https://example.com',
|
|
160
557
|
})
|
|
161
558
|
})
|
|
162
559
|
|
|
163
560
|
test('parses a source part with title', () => {
|
|
164
|
-
const raw = { type: 'source', url: 'https://example.com', title: 'Example' }
|
|
561
|
+
const raw = { id: 'p1', type: 'source', url: 'https://example.com', title: 'Example' }
|
|
165
562
|
expect(parseAgentPart(raw)).toEqual({
|
|
563
|
+
id: 'p1',
|
|
166
564
|
type: 'source',
|
|
167
565
|
url: 'https://example.com',
|
|
168
566
|
title: 'Example',
|
|
@@ -170,38 +568,44 @@ describe('parseAgentPart', () => {
|
|
|
170
568
|
})
|
|
171
569
|
|
|
172
570
|
test('rejects a missing url', () => {
|
|
173
|
-
expect(parseAgentPart({ type: 'source' })).toBeNull()
|
|
571
|
+
expect(parseAgentPart({ id: 'p1', type: 'source' })).toBeNull()
|
|
174
572
|
})
|
|
175
573
|
|
|
176
574
|
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' })
|
|
575
|
+
const raw = { id: 'p1', type: 'source', url: 'https://example.com', title: 7 }
|
|
576
|
+
expect(parseAgentPart(raw)).toEqual({ id: 'p1', type: 'source', url: 'https://example.com' })
|
|
179
577
|
})
|
|
180
578
|
|
|
181
579
|
test('round-trips through JSON', () => {
|
|
182
|
-
const part: AgentPart = {
|
|
580
|
+
const part: AgentPart = {
|
|
581
|
+
id: 'p1',
|
|
582
|
+
type: 'source',
|
|
583
|
+
url: 'https://example.com',
|
|
584
|
+
title: 'Example',
|
|
585
|
+
}
|
|
183
586
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
184
587
|
})
|
|
185
588
|
})
|
|
186
589
|
|
|
187
590
|
describe('error', () => {
|
|
188
591
|
test('parses an error part', () => {
|
|
189
|
-
expect(parseAgentPart({ type: 'error', message: 'boom' })).toEqual({
|
|
592
|
+
expect(parseAgentPart({ id: 'p1', type: 'error', message: 'boom' })).toEqual({
|
|
593
|
+
id: 'p1',
|
|
190
594
|
type: 'error',
|
|
191
595
|
message: 'boom',
|
|
192
596
|
})
|
|
193
597
|
})
|
|
194
598
|
|
|
195
599
|
test('rejects a missing message', () => {
|
|
196
|
-
expect(parseAgentPart({ type: 'error' })).toBeNull()
|
|
600
|
+
expect(parseAgentPart({ id: 'p1', type: 'error' })).toBeNull()
|
|
197
601
|
})
|
|
198
602
|
|
|
199
603
|
test('rejects a non-string message', () => {
|
|
200
|
-
expect(parseAgentPart({ type: 'error', message: 500 })).toBeNull()
|
|
604
|
+
expect(parseAgentPart({ id: 'p1', type: 'error', message: 500 })).toBeNull()
|
|
201
605
|
})
|
|
202
606
|
|
|
203
607
|
test('round-trips through JSON', () => {
|
|
204
|
-
const part: AgentPart = { type: 'error', message: 'boom' }
|
|
608
|
+
const part: AgentPart = { id: 'p1', type: 'error', message: 'boom' }
|
|
205
609
|
expect(parseAgentPart(roundTrip(part))).toEqual(part)
|
|
206
610
|
})
|
|
207
611
|
})
|
|
@@ -225,26 +629,26 @@ describe('parseAgentPart', () => {
|
|
|
225
629
|
})
|
|
226
630
|
|
|
227
631
|
test('rejects an object with no type field', () => {
|
|
228
|
-
expect(parseAgentPart({ text: 'hello' })).toBeNull()
|
|
632
|
+
expect(parseAgentPart({ id: 'p1', text: 'hello' })).toBeNull()
|
|
229
633
|
})
|
|
230
634
|
|
|
231
635
|
test('rejects an object with a non-string type field', () => {
|
|
232
|
-
expect(parseAgentPart({ type: 1 })).toBeNull()
|
|
636
|
+
expect(parseAgentPart({ id: 'p1', type: 1 })).toBeNull()
|
|
233
637
|
})
|
|
234
638
|
|
|
235
639
|
test('rejects an unknown type discriminant', () => {
|
|
236
|
-
expect(parseAgentPart({ type: 'unknown-variant', text: 'hi' })).toBeNull()
|
|
640
|
+
expect(parseAgentPart({ id: 'p1', type: 'unknown-variant', text: 'hi' })).toBeNull()
|
|
237
641
|
})
|
|
238
642
|
})
|
|
239
643
|
})
|
|
240
644
|
|
|
241
645
|
describe('isStartPart', () => {
|
|
242
646
|
test('returns true for a StartPart', () => {
|
|
243
|
-
expect(isStartPart({ type: 'start', runId: 'run-1' })).toBe(true)
|
|
647
|
+
expect(isStartPart({ id: 'p1', type: 'start', runId: 'run-1' })).toBe(true)
|
|
244
648
|
})
|
|
245
649
|
|
|
246
650
|
test('returns false for a non-start AgentPart', () => {
|
|
247
|
-
expect(isStartPart({ type: 'text', text: 'hi' })).toBe(false)
|
|
651
|
+
expect(isStartPart({ id: 'p1', type: 'text', text: 'hi' })).toBe(false)
|
|
248
652
|
})
|
|
249
653
|
|
|
250
654
|
test('returns false for null', () => {
|
|
@@ -259,3 +663,58 @@ describe('isStartPart', () => {
|
|
|
259
663
|
expect(isStartPart({ runId: 'run-1' })).toBe(false)
|
|
260
664
|
})
|
|
261
665
|
})
|
|
666
|
+
|
|
667
|
+
describe('isToolCallSettled', () => {
|
|
668
|
+
const base = { id: 'p1', type: 'tool', toolCallId: 'call-1', toolName: 'search' } as const
|
|
669
|
+
|
|
670
|
+
test('false for input-streaming', () => {
|
|
671
|
+
expect(isToolCallSettled({ ...base, state: 'input-streaming' })).toBe(false)
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test('false for input-available', () => {
|
|
675
|
+
expect(isToolCallSettled({ ...base, state: 'input-available', input: {} })).toBe(false)
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
test('false for approval-requested', () => {
|
|
679
|
+
expect(
|
|
680
|
+
isToolCallSettled({
|
|
681
|
+
...base,
|
|
682
|
+
state: 'approval-requested',
|
|
683
|
+
input: {},
|
|
684
|
+
approval: { id: 'appr-1' },
|
|
685
|
+
}),
|
|
686
|
+
).toBe(false)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
test('false for approval-responded', () => {
|
|
690
|
+
expect(
|
|
691
|
+
isToolCallSettled({
|
|
692
|
+
...base,
|
|
693
|
+
state: 'approval-responded',
|
|
694
|
+
input: {},
|
|
695
|
+
approval: { id: 'appr-1', approved: true },
|
|
696
|
+
}),
|
|
697
|
+
).toBe(false)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
test('true for output-available', () => {
|
|
701
|
+
expect(isToolCallSettled({ ...base, state: 'output-available', input: {}, output: {} })).toBe(
|
|
702
|
+
true,
|
|
703
|
+
)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
test('true for output-error', () => {
|
|
707
|
+
expect(isToolCallSettled({ ...base, state: 'output-error', errorText: 'boom' })).toBe(true)
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
test('true for output-denied', () => {
|
|
711
|
+
expect(
|
|
712
|
+
isToolCallSettled({
|
|
713
|
+
...base,
|
|
714
|
+
state: 'output-denied',
|
|
715
|
+
input: {},
|
|
716
|
+
approval: { id: 'appr-1', approved: false },
|
|
717
|
+
}),
|
|
718
|
+
).toBe(true)
|
|
719
|
+
})
|
|
720
|
+
})
|