@tanstack/ai-client 0.29.1 → 0.30.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/README.md +11 -15
- package/dist/esm/chat-client.js +1 -0
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +2 -0
- package/dist/esm/devtools-noop.js +2 -0
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +11 -1
- package/dist/esm/devtools.js +95 -0
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/ui/selectors.d.ts +11 -0
- package/dist/esm/ui/selectors.js +106 -0
- package/dist/esm/ui/selectors.js.map +1 -0
- package/dist/esm/ui/types.d.ts +103 -0
- package/dist/esm/ui.d.ts +3 -0
- package/dist/esm/ui.js +2 -0
- package/package.json +7 -3
- package/src/chat-client.ts +7 -0
- package/src/devtools-noop.ts +2 -0
- package/src/devtools.ts +182 -0
- package/src/ui/selectors.ts +178 -0
- package/src/ui/types.ts +243 -0
- package/src/ui.ts +37 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AnyClientTool, InferSchemaType, InferToolInput, InferToolOutput, InterruptDefinition, SchemaInput } from '@tanstack/ai/client';
|
|
2
|
+
import { ChatInterrupt, MessagePart, RegisteredGenericInterrupt, StructuredOutputPart, ToolApprovalInterrupt, ToolCallPart, ToolResultPart, UIMessage } from '../types.js';
|
|
3
|
+
export type ChatUIPartKey = 'text' | 'image' | 'audio' | 'video' | 'document' | 'thinking' | 'toolCall' | 'toolResult' | 'structuredOutput' | 'uiResource';
|
|
4
|
+
export type ChatUIPartTypeByKey = {
|
|
5
|
+
text: 'text';
|
|
6
|
+
image: 'image';
|
|
7
|
+
audio: 'audio';
|
|
8
|
+
video: 'video';
|
|
9
|
+
document: 'document';
|
|
10
|
+
thinking: 'thinking';
|
|
11
|
+
toolCall: 'tool-call';
|
|
12
|
+
toolResult: 'tool-result';
|
|
13
|
+
structuredOutput: 'structured-output';
|
|
14
|
+
uiResource: 'ui-resource';
|
|
15
|
+
};
|
|
16
|
+
export type ChatUIPartOf<TOptions, TKey extends ChatUIPartKey = ChatUIPartKey> = TKey extends ChatUIPartKey ? Extract<MessagePart<ChatUIToolsOf<TOptions>, ChatUIData<TOptions>>, {
|
|
17
|
+
type: ChatUIPartTypeByKey[TKey];
|
|
18
|
+
}> : never;
|
|
19
|
+
export type ChatUIToolsOf<TOptions> = TOptions extends {
|
|
20
|
+
tools: infer TTools;
|
|
21
|
+
} ? TTools extends ReadonlyArray<AnyClientTool> ? TTools : any : any;
|
|
22
|
+
export type ChatUIInterruptsOf<TOptions> = TOptions extends {
|
|
23
|
+
interrupts: infer TInterrupts;
|
|
24
|
+
} ? TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> ? TInterrupts : readonly [] : readonly [];
|
|
25
|
+
export type ChatUISchemaOf<TOptions> = TOptions extends {
|
|
26
|
+
outputSchema: infer TSchema;
|
|
27
|
+
} ? TSchema extends SchemaInput ? TSchema : undefined : undefined;
|
|
28
|
+
export type ChatUIData<TOptions> = TOptions extends {
|
|
29
|
+
outputSchema: infer TSchema;
|
|
30
|
+
} ? TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown : unknown;
|
|
31
|
+
export type ChatUIToolName<TOptions> = ChatUIToolsOf<TOptions>[number] extends infer TTool ? TTool extends AnyClientTool ? TTool['name'] : string : string;
|
|
32
|
+
export type ChatUIRegisteredInterruptId<TOptions> = ChatUIInterruptsOf<TOptions>[number] extends infer TDefinition ? TDefinition extends InterruptDefinition<infer TId, any, any, any> ? TId : string : string;
|
|
33
|
+
export type ChatUIHasNamedTools<TOptions> = [ChatUIToolName<TOptions>] extends [
|
|
34
|
+
never
|
|
35
|
+
] ? false : [string] extends [ChatUIToolName<TOptions>] ? false : true;
|
|
36
|
+
export type ChatUIHasNamedInterrupts<TOptions> = [
|
|
37
|
+
ChatUIRegisteredInterruptId<TOptions>
|
|
38
|
+
] extends [never] ? false : [string] extends [ChatUIRegisteredInterruptId<TOptions>] ? false : true;
|
|
39
|
+
export type ChatUINamedInterruptId<TOptions> = Exclude<ChatUIRegisteredInterruptId<TOptions>, 'fallback'>;
|
|
40
|
+
export type ChatUIInterruptName<TOptions> = (ChatUIHasNamedTools<TOptions> extends true ? ChatUIToolName<TOptions> : never) | (ChatUIHasNamedInterrupts<TOptions> extends true ? ChatUINamedInterruptId<TOptions> : never);
|
|
41
|
+
type ToolByName<TOptions, TName> = Extract<ChatUIToolsOf<TOptions> extends ReadonlyArray<infer TTool> ? TTool : never, {
|
|
42
|
+
name: TName;
|
|
43
|
+
}>;
|
|
44
|
+
export type ChatUIInterrupt = ChatInterrupt | ToolApprovalInterrupt;
|
|
45
|
+
export type ChatUIToolApproval<TOptions, TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>> = Extract<ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>, {
|
|
46
|
+
kind: 'tool-approval';
|
|
47
|
+
toolName: TName;
|
|
48
|
+
}>;
|
|
49
|
+
export type ChatUIToolPart<TOptions, TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>> = {
|
|
50
|
+
key: 'toolCall';
|
|
51
|
+
part: Extract<ToolCallPart<ChatUIToolsOf<TOptions>>, {
|
|
52
|
+
name: TName;
|
|
53
|
+
}>;
|
|
54
|
+
result?: ToolResultPart;
|
|
55
|
+
interrupt?: Extract<ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>, {
|
|
56
|
+
kind: 'tool-approval';
|
|
57
|
+
toolName: TName;
|
|
58
|
+
}>;
|
|
59
|
+
input?: InferToolInput<ToolByName<TOptions, TName>>;
|
|
60
|
+
output?: InferToolOutput<ToolByName<TOptions, TName>>;
|
|
61
|
+
};
|
|
62
|
+
export type RegisteredUIInterrupt<TOptions, TId extends ChatUIRegisteredInterruptId<TOptions> = ChatUIRegisteredInterruptId<TOptions>> = Extract<RegisteredGenericInterrupt<ChatUIInterruptsOf<TOptions>>, {
|
|
63
|
+
definitionId: TId;
|
|
64
|
+
}>;
|
|
65
|
+
export type ChatUIInterruptOf<TOptions, TName extends ChatUIInterruptName<TOptions> = never> = [TName] extends [never] ? ChatUIInterrupt : TName extends (ChatUIHasNamedTools<TOptions> extends true ? ChatUIToolName<TOptions> : never) ? ChatUIToolApproval<TOptions, TName> : TName extends ChatUINamedInterruptId<TOptions> ? RegisteredUIInterrupt<TOptions, TName> : ChatUIInterrupt;
|
|
66
|
+
export type ChatUISelectedToolPart = {
|
|
67
|
+
key: 'toolCall';
|
|
68
|
+
part: ToolCallPart;
|
|
69
|
+
result?: ToolResultPart;
|
|
70
|
+
interrupt?: ChatUIInterrupt;
|
|
71
|
+
input?: unknown;
|
|
72
|
+
output?: unknown;
|
|
73
|
+
};
|
|
74
|
+
export type ChatUISelectedResultPart = {
|
|
75
|
+
key: 'toolResult';
|
|
76
|
+
part: ToolResultPart;
|
|
77
|
+
matched: boolean;
|
|
78
|
+
};
|
|
79
|
+
export type ChatUISelectedGenericPart = {
|
|
80
|
+
key: Exclude<ChatUIPartKey, 'toolCall' | 'toolResult'>;
|
|
81
|
+
part: MessagePart;
|
|
82
|
+
};
|
|
83
|
+
export type ChatUISelectedPart = ChatUISelectedToolPart | ChatUISelectedResultPart | ChatUISelectedGenericPart;
|
|
84
|
+
export type ChatUISelectedPartOf<TOptions, TKey extends ChatUIPartKey = ChatUIPartKey> = TKey extends 'toolCall' ? ChatUIToolPart<TOptions> : TKey extends 'toolResult' ? ChatUISelectedResultPart : {
|
|
85
|
+
key: TKey;
|
|
86
|
+
part: ChatUIPartOf<TOptions, TKey>;
|
|
87
|
+
};
|
|
88
|
+
export type ChatUISelectedMessage = {
|
|
89
|
+
message: UIMessage;
|
|
90
|
+
parts: Array<ChatUISelectedPart>;
|
|
91
|
+
};
|
|
92
|
+
export type ChatUISelection = {
|
|
93
|
+
messages: Array<ChatUISelectedMessage>;
|
|
94
|
+
interrupts: Array<ChatUIInterrupt>;
|
|
95
|
+
};
|
|
96
|
+
export type ChatUISelectInput = {
|
|
97
|
+
messages: ReadonlyArray<UIMessage>;
|
|
98
|
+
interrupts?: ReadonlyArray<ChatUIInterrupt>;
|
|
99
|
+
inlineToolNames?: ReadonlyArray<string>;
|
|
100
|
+
};
|
|
101
|
+
export type ChatUIMessages<TOptions> = Array<UIMessage<ChatUIToolsOf<TOptions>, ChatUIData<TOptions>>>;
|
|
102
|
+
export type ChatUIStructuredPart<TOptions> = StructuredOutputPart<ChatUIData<TOptions>>;
|
|
103
|
+
export {};
|
package/dist/esm/ui.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { partTypeToKey, selectChatUI } from './ui/selectors.js';
|
|
2
|
+
export { automaticPartsForMessage, collectInlineToolNames, resolveInterruptComponent, selectMessageUI, } from './ui/selectors.js';
|
|
3
|
+
export type { ChatUIData, ChatUIHasNamedInterrupts, ChatUIHasNamedTools, ChatUIInterrupt, ChatUIInterruptName, ChatUIInterruptOf, ChatUIInterruptsOf, ChatUINamedInterruptId, ChatUIMessages, ChatUIPartKey, ChatUIPartOf, ChatUIPartTypeByKey, ChatUIRegisteredInterruptId, ChatUISchemaOf, ChatUISelectInput, ChatUISelectedGenericPart, ChatUISelectedMessage, ChatUISelectedPart, ChatUISelectedPartOf, ChatUISelectedResultPart, ChatUISelectedToolPart, ChatUISelection, ChatUIStructuredPart, ChatUIToolApproval, ChatUIToolName, ChatUIToolPart, ChatUIToolsOf, RegisteredUIInterrupt, } from './ui/types.js';
|
package/dist/esm/ui.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { automaticPartsForMessage, collectInlineToolNames, partTypeToKey, resolveInterruptComponent, selectChatUI, selectMessageUI } from "./ui/selectors.js";
|
|
2
|
+
export { automaticPartsForMessage, collectInlineToolNames, partTypeToKey, resolveInterruptComponent, selectChatUI, selectMessageUI };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI chat, realtime sessions, streaming transports, and media generations.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"./byok": {
|
|
47
47
|
"types": "./dist/esm/byok.d.ts",
|
|
48
48
|
"import": "./dist/esm/byok.js"
|
|
49
|
+
},
|
|
50
|
+
"./ui": {
|
|
51
|
+
"types": "./dist/esm/ui.d.ts",
|
|
52
|
+
"import": "./dist/esm/ui.js"
|
|
49
53
|
}
|
|
50
54
|
},
|
|
51
55
|
"files": [
|
|
@@ -53,8 +57,8 @@
|
|
|
53
57
|
"src"
|
|
54
58
|
],
|
|
55
59
|
"dependencies": {
|
|
56
|
-
"@tanstack/ai": "^0.
|
|
57
|
-
"@tanstack/ai-event-client": "^0.11.
|
|
60
|
+
"@tanstack/ai": "^0.52.1",
|
|
61
|
+
"@tanstack/ai-event-client": "^0.11.2",
|
|
58
62
|
"@tanstack/ai-utils": "^0.4.0"
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
package/src/chat-client.ts
CHANGED
|
@@ -851,6 +851,13 @@ export class ChatClient<
|
|
|
851
851
|
if (eventType === 'memory:state') {
|
|
852
852
|
this.devtoolsBridge.recordMemoryState(data)
|
|
853
853
|
}
|
|
854
|
+
if (
|
|
855
|
+
eventType === 'compaction:started' ||
|
|
856
|
+
eventType === 'compaction:state' ||
|
|
857
|
+
eventType === 'compaction:ended'
|
|
858
|
+
) {
|
|
859
|
+
this.devtoolsBridge.recordCompactionEvent(eventType, data)
|
|
860
|
+
}
|
|
854
861
|
if (eventType === 'skills:state') {
|
|
855
862
|
this.devtoolsBridge.recordSkillsState(data)
|
|
856
863
|
}
|
package/src/devtools-noop.ts
CHANGED
|
@@ -87,6 +87,8 @@ export class NoOpChatDevtoolsBridge {
|
|
|
87
87
|
}
|
|
88
88
|
observeChunk(_chunk: StreamChunk): void {}
|
|
89
89
|
recordMemoryState(_value: unknown): void {}
|
|
90
|
+
recordCompactionEvent(_eventType: string, _value: unknown): void {}
|
|
91
|
+
recordCompactionState(_value: unknown): void {}
|
|
90
92
|
recordSkillsState(_value: unknown): void {}
|
|
91
93
|
beginRun(_runId: string, _threadId: string): void {}
|
|
92
94
|
getCurrentRunEventContext(): ChatClientRunEventContext | undefined {
|
package/src/devtools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { DefaultChatClientEventEmitter } from './events'
|
|
|
8
8
|
import type { AnyClientTool, StreamChunk } from '@tanstack/ai/client'
|
|
9
9
|
import type {
|
|
10
10
|
AIDevtoolsEventVisibility,
|
|
11
|
+
CompactionMessagePreview,
|
|
11
12
|
MemoryScopeLite,
|
|
12
13
|
} from '@tanstack/ai-event-client'
|
|
13
14
|
import type {
|
|
@@ -60,6 +61,127 @@ interface MemoryStateEventValue {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function readCompactionBoundaryValue(rawValue: unknown): {
|
|
65
|
+
before?: number
|
|
66
|
+
after?: number
|
|
67
|
+
messagesBefore?: number
|
|
68
|
+
messagesAfter?: number
|
|
69
|
+
reusedCheckpoint?: boolean
|
|
70
|
+
maxTokens?: number
|
|
71
|
+
strategyKey?: string
|
|
72
|
+
durationMs?: number
|
|
73
|
+
} {
|
|
74
|
+
if (!rawValue || typeof rawValue !== 'object') return {}
|
|
75
|
+
return {
|
|
76
|
+
...('before' in rawValue && typeof rawValue.before === 'number'
|
|
77
|
+
? { before: rawValue.before }
|
|
78
|
+
: {}),
|
|
79
|
+
...('after' in rawValue && typeof rawValue.after === 'number'
|
|
80
|
+
? { after: rawValue.after }
|
|
81
|
+
: {}),
|
|
82
|
+
...('messagesBefore' in rawValue &&
|
|
83
|
+
typeof rawValue.messagesBefore === 'number'
|
|
84
|
+
? { messagesBefore: rawValue.messagesBefore }
|
|
85
|
+
: {}),
|
|
86
|
+
...('messagesAfter' in rawValue &&
|
|
87
|
+
typeof rawValue.messagesAfter === 'number'
|
|
88
|
+
? { messagesAfter: rawValue.messagesAfter }
|
|
89
|
+
: {}),
|
|
90
|
+
...('reusedCheckpoint' in rawValue &&
|
|
91
|
+
typeof rawValue.reusedCheckpoint === 'boolean'
|
|
92
|
+
? { reusedCheckpoint: rawValue.reusedCheckpoint }
|
|
93
|
+
: {}),
|
|
94
|
+
...('maxTokens' in rawValue && typeof rawValue.maxTokens === 'number'
|
|
95
|
+
? { maxTokens: rawValue.maxTokens }
|
|
96
|
+
: {}),
|
|
97
|
+
...('strategyKey' in rawValue && typeof rawValue.strategyKey === 'string'
|
|
98
|
+
? { strategyKey: rawValue.strategyKey }
|
|
99
|
+
: {}),
|
|
100
|
+
...('durationMs' in rawValue && typeof rawValue.durationMs === 'number'
|
|
101
|
+
? { durationMs: rawValue.durationMs }
|
|
102
|
+
: {}),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readPreviewList(
|
|
107
|
+
value: unknown,
|
|
108
|
+
): Array<CompactionMessagePreview> | undefined {
|
|
109
|
+
if (!Array.isArray(value)) return undefined
|
|
110
|
+
const previews: Array<CompactionMessagePreview> = []
|
|
111
|
+
for (const item of value) {
|
|
112
|
+
if (!item || typeof item !== 'object') continue
|
|
113
|
+
if (!('role' in item) || !('tokens' in item) || !('text' in item)) continue
|
|
114
|
+
if (
|
|
115
|
+
typeof item.role !== 'string' ||
|
|
116
|
+
typeof item.tokens !== 'number' ||
|
|
117
|
+
typeof item.text !== 'string'
|
|
118
|
+
) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
previews.push({
|
|
122
|
+
role: item.role,
|
|
123
|
+
tokens: item.tokens,
|
|
124
|
+
text: item.text,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
return previews
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readCompactionStateValue(rawValue: unknown): {
|
|
131
|
+
before: number
|
|
132
|
+
after: number
|
|
133
|
+
messagesBefore: number
|
|
134
|
+
messagesAfter: number
|
|
135
|
+
reusedCheckpoint: boolean
|
|
136
|
+
maxTokens?: number
|
|
137
|
+
strategyKey?: string
|
|
138
|
+
dropped?: Array<CompactionMessagePreview>
|
|
139
|
+
result?: Array<CompactionMessagePreview>
|
|
140
|
+
} | null {
|
|
141
|
+
if (!rawValue || typeof rawValue !== 'object') return null
|
|
142
|
+
if (
|
|
143
|
+
!('before' in rawValue) ||
|
|
144
|
+
!('after' in rawValue) ||
|
|
145
|
+
!('messagesBefore' in rawValue) ||
|
|
146
|
+
!('messagesAfter' in rawValue)
|
|
147
|
+
) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
typeof rawValue.before !== 'number' ||
|
|
152
|
+
typeof rawValue.after !== 'number' ||
|
|
153
|
+
typeof rawValue.messagesBefore !== 'number' ||
|
|
154
|
+
typeof rawValue.messagesAfter !== 'number'
|
|
155
|
+
) {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
const reusedCheckpoint =
|
|
159
|
+
'reusedCheckpoint' in rawValue && rawValue.reusedCheckpoint === true
|
|
160
|
+
const maxTokens =
|
|
161
|
+
'maxTokens' in rawValue && typeof rawValue.maxTokens === 'number'
|
|
162
|
+
? rawValue.maxTokens
|
|
163
|
+
: undefined
|
|
164
|
+
const strategyKey =
|
|
165
|
+
'strategyKey' in rawValue && typeof rawValue.strategyKey === 'string'
|
|
166
|
+
? rawValue.strategyKey
|
|
167
|
+
: undefined
|
|
168
|
+
const dropped =
|
|
169
|
+
'dropped' in rawValue ? readPreviewList(rawValue.dropped) : undefined
|
|
170
|
+
const result =
|
|
171
|
+
'result' in rawValue ? readPreviewList(rawValue.result) : undefined
|
|
172
|
+
return {
|
|
173
|
+
before: rawValue.before,
|
|
174
|
+
after: rawValue.after,
|
|
175
|
+
messagesBefore: rawValue.messagesBefore,
|
|
176
|
+
messagesAfter: rawValue.messagesAfter,
|
|
177
|
+
reusedCheckpoint,
|
|
178
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
179
|
+
...(strategyKey ? { strategyKey } : {}),
|
|
180
|
+
...(dropped ? { dropped } : {}),
|
|
181
|
+
...(result ? { result } : {}),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
63
185
|
export interface AIDevtoolsClientMetadata extends AIDevtoolsDisplayOptions {
|
|
64
186
|
framework?: string
|
|
65
187
|
hookName: string
|
|
@@ -760,6 +882,9 @@ export class ClientDevtoolsBridge<TSnapshot extends object> {
|
|
|
760
882
|
| 'memory:retrieve:started'
|
|
761
883
|
| 'memory:retrieve:completed'
|
|
762
884
|
| 'memory:snapshot'
|
|
885
|
+
| 'compaction:started'
|
|
886
|
+
| 'compaction:state'
|
|
887
|
+
| 'compaction:ended'
|
|
763
888
|
| 'skills:snapshot'
|
|
764
889
|
| AIDevtoolsRunEventType,
|
|
765
890
|
visibility: AIDevtoolsEventVisibility = 'client-state',
|
|
@@ -819,6 +944,11 @@ export class ChatDevtoolsBridge extends ClientDevtoolsBridge<AIDevtoolsChatSnaps
|
|
|
819
944
|
private lastRunEventContext: ChatClientRunEventContext | undefined
|
|
820
945
|
/** Last transported `memory:state` value, replayed when a panel opens. */
|
|
821
946
|
private lastMemoryStateValue: unknown = null
|
|
947
|
+
/** Transported compaction CUSTOM events, replayed when a panel opens. */
|
|
948
|
+
private readonly lastCompactionEvents: Array<{
|
|
949
|
+
eventType: string
|
|
950
|
+
value: unknown
|
|
951
|
+
}> = []
|
|
822
952
|
/** Last transported `skills:state` value, replayed when a panel opens. */
|
|
823
953
|
private lastSkillsStateValue: unknown = null
|
|
824
954
|
|
|
@@ -987,6 +1117,55 @@ export class ChatDevtoolsBridge extends ClientDevtoolsBridge<AIDevtoolsChatSnaps
|
|
|
987
1117
|
}
|
|
988
1118
|
}
|
|
989
1119
|
|
|
1120
|
+
/**
|
|
1121
|
+
* Record a transported compaction CUSTOM event. Called from the chat
|
|
1122
|
+
* client's `onCustomEvent` handler so server-side compaction reaches the
|
|
1123
|
+
* browser DevTools panel.
|
|
1124
|
+
*/
|
|
1125
|
+
recordCompactionEvent(eventType: string, rawValue: unknown): void {
|
|
1126
|
+
this.lastCompactionEvents.push({ eventType, value: rawValue })
|
|
1127
|
+
if (this.lastCompactionEvents.length > 60) {
|
|
1128
|
+
this.lastCompactionEvents.splice(0, this.lastCompactionEvents.length - 60)
|
|
1129
|
+
}
|
|
1130
|
+
this.emitCompactionEvent(eventType, rawValue)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
recordCompactionState(rawValue: unknown): void {
|
|
1134
|
+
this.recordCompactionEvent('compaction:state', rawValue)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private emitCompactionEvent(eventType: string, rawValue: unknown): void {
|
|
1138
|
+
const runContext = this.currentRunId ? { runId: this.currentRunId } : {}
|
|
1139
|
+
if (eventType === 'compaction:started') {
|
|
1140
|
+
const value = readCompactionBoundaryValue(rawValue)
|
|
1141
|
+
emitAIDevtoolsEvent('compaction:started', {
|
|
1142
|
+
...this.createEnvelope(
|
|
1143
|
+
'compaction:started',
|
|
1144
|
+
'client-state',
|
|
1145
|
+
runContext,
|
|
1146
|
+
),
|
|
1147
|
+
...value,
|
|
1148
|
+
})
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1151
|
+
if (eventType === 'compaction:ended') {
|
|
1152
|
+
const value = readCompactionBoundaryValue(rawValue)
|
|
1153
|
+
emitAIDevtoolsEvent('compaction:ended', {
|
|
1154
|
+
...this.createEnvelope('compaction:ended', 'client-state', runContext),
|
|
1155
|
+
...value,
|
|
1156
|
+
})
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
if (eventType === 'compaction:state') {
|
|
1160
|
+
const value = readCompactionStateValue(rawValue)
|
|
1161
|
+
if (!value) return
|
|
1162
|
+
emitAIDevtoolsEvent('compaction:state', {
|
|
1163
|
+
...this.createEnvelope('compaction:state', 'client-state', runContext),
|
|
1164
|
+
...value,
|
|
1165
|
+
})
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
990
1169
|
private emitSkillsState(rawValue: unknown): void {
|
|
991
1170
|
if (!rawValue || typeof rawValue !== 'object') return
|
|
992
1171
|
const value = rawValue as SkillsStateEventValue
|
|
@@ -1004,6 +1183,9 @@ export class ChatDevtoolsBridge extends ClientDevtoolsBridge<AIDevtoolsChatSnaps
|
|
|
1004
1183
|
if (this.lastMemoryStateValue != null) {
|
|
1005
1184
|
this.emitMemoryState(this.lastMemoryStateValue)
|
|
1006
1185
|
}
|
|
1186
|
+
for (const event of this.lastCompactionEvents) {
|
|
1187
|
+
this.emitCompactionEvent(event.eventType, event.value)
|
|
1188
|
+
}
|
|
1007
1189
|
if (this.lastSkillsStateValue != null) {
|
|
1008
1190
|
this.emitSkillsState(this.lastSkillsStateValue)
|
|
1009
1191
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessagePart,
|
|
3
|
+
ToolApprovalInterrupt,
|
|
4
|
+
ToolCallPart,
|
|
5
|
+
ToolResultPart,
|
|
6
|
+
UIMessage,
|
|
7
|
+
} from '../types'
|
|
8
|
+
import type {
|
|
9
|
+
ChatUIInterrupt,
|
|
10
|
+
ChatUIPartKey,
|
|
11
|
+
ChatUISelectInput,
|
|
12
|
+
ChatUISelectedMessage,
|
|
13
|
+
ChatUISelectedPart,
|
|
14
|
+
ChatUISelection,
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
const PART_KEY_BY_TYPE: Record<string, ChatUIPartKey> = {
|
|
18
|
+
text: 'text',
|
|
19
|
+
image: 'image',
|
|
20
|
+
audio: 'audio',
|
|
21
|
+
video: 'video',
|
|
22
|
+
document: 'document',
|
|
23
|
+
thinking: 'thinking',
|
|
24
|
+
'tool-call': 'toolCall',
|
|
25
|
+
'tool-result': 'toolResult',
|
|
26
|
+
'structured-output': 'structuredOutput',
|
|
27
|
+
'ui-resource': 'uiResource',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function partTypeToKey(type: string): ChatUIPartKey | string {
|
|
31
|
+
const mapped = PART_KEY_BY_TYPE[type]
|
|
32
|
+
if (mapped) return mapped
|
|
33
|
+
return type.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function collectInlineToolNames(
|
|
37
|
+
toolInterrupts?: Record<string, unknown>,
|
|
38
|
+
mappedToolNames?: ReadonlyArray<string>,
|
|
39
|
+
): Array<string> {
|
|
40
|
+
const listPlaced = new Set(Object.keys(toolInterrupts ?? {}))
|
|
41
|
+
return (mappedToolNames ?? []).filter((name) => !listPlaced.has(name))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveInterruptComponent(
|
|
45
|
+
interrupt: ChatUIInterrupt,
|
|
46
|
+
interruptsMap:
|
|
47
|
+
| {
|
|
48
|
+
tools?: Record<string, unknown>
|
|
49
|
+
generic?: Record<string, unknown>
|
|
50
|
+
}
|
|
51
|
+
| undefined,
|
|
52
|
+
): unknown {
|
|
53
|
+
if (!interruptsMap) return undefined
|
|
54
|
+
if (interrupt.kind === 'tool-approval') {
|
|
55
|
+
return interruptsMap.tools?.[interrupt.toolName]
|
|
56
|
+
}
|
|
57
|
+
const generic = interruptsMap.generic
|
|
58
|
+
if (!generic) return undefined
|
|
59
|
+
const definitionId =
|
|
60
|
+
'definitionId' in interrupt && typeof interrupt.definitionId === 'string'
|
|
61
|
+
? interrupt.definitionId
|
|
62
|
+
: undefined
|
|
63
|
+
if (definitionId && definitionId !== 'fallback') {
|
|
64
|
+
const registered = generic[definitionId]
|
|
65
|
+
if (registered) return registered
|
|
66
|
+
}
|
|
67
|
+
return generic.fallback
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isToolCallPart(part: MessagePart): part is ToolCallPart {
|
|
71
|
+
return part.type === 'tool-call'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isToolResultPart(part: MessagePart): part is ToolResultPart {
|
|
75
|
+
return part.type === 'tool-result'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isToolApproval(
|
|
79
|
+
interrupt: ChatUIInterrupt,
|
|
80
|
+
): interrupt is ToolApprovalInterrupt {
|
|
81
|
+
return interrupt.kind === 'tool-approval'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function selectChatUI(input: ChatUISelectInput): ChatUISelection {
|
|
85
|
+
const messages = input.messages
|
|
86
|
+
const interrupts = input.interrupts ?? []
|
|
87
|
+
const inlineToolNames = new Set(input.inlineToolNames ?? [])
|
|
88
|
+
|
|
89
|
+
const resultsByCallId = new Map<string, ToolResultPart>()
|
|
90
|
+
const callIds = new Set<string>()
|
|
91
|
+
for (const message of messages) {
|
|
92
|
+
for (const part of message.parts) {
|
|
93
|
+
if (isToolCallPart(part)) callIds.add(part.id)
|
|
94
|
+
if (isToolResultPart(part)) resultsByCallId.set(part.toolCallId, part)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const approvalByCallId = new Map<string, ToolApprovalInterrupt>()
|
|
99
|
+
for (const interrupt of interrupts) {
|
|
100
|
+
if (isToolApproval(interrupt)) {
|
|
101
|
+
approvalByCallId.set(interrupt.toolCallId, interrupt)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const selectedMessages: Array<ChatUISelectedMessage> = messages.map(
|
|
106
|
+
(message) => ({
|
|
107
|
+
message,
|
|
108
|
+
parts: message.parts.map((part) =>
|
|
109
|
+
toSelectedPart(part, resultsByCallId, approvalByCallId, callIds),
|
|
110
|
+
),
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
messages: selectedMessages,
|
|
116
|
+
interrupts: interrupts.filter((interrupt) => {
|
|
117
|
+
if (!isToolApproval(interrupt)) return true
|
|
118
|
+
if (!inlineToolNames.has(interrupt.toolName)) return true
|
|
119
|
+
return !callIds.has(interrupt.toolCallId)
|
|
120
|
+
}),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toSelectedPart(
|
|
125
|
+
part: MessagePart,
|
|
126
|
+
resultsByCallId: Map<string, ToolResultPart>,
|
|
127
|
+
approvalByCallId: Map<string, ToolApprovalInterrupt>,
|
|
128
|
+
callIds: Set<string>,
|
|
129
|
+
): ChatUISelectedPart {
|
|
130
|
+
if (isToolCallPart(part)) {
|
|
131
|
+
const result = resultsByCallId.get(part.id)
|
|
132
|
+
const interrupt = approvalByCallId.get(part.id)
|
|
133
|
+
return {
|
|
134
|
+
key: 'toolCall',
|
|
135
|
+
part,
|
|
136
|
+
...(result ? { result } : {}),
|
|
137
|
+
...(interrupt ? { interrupt } : {}),
|
|
138
|
+
input: part.input,
|
|
139
|
+
output: part.output,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isToolResultPart(part)) {
|
|
144
|
+
return {
|
|
145
|
+
key: 'toolResult',
|
|
146
|
+
part,
|
|
147
|
+
matched: callIds.has(part.toolCallId),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
key: partTypeToKey(part.type) as Exclude<
|
|
153
|
+
ChatUIPartKey,
|
|
154
|
+
'toolCall' | 'toolResult'
|
|
155
|
+
>,
|
|
156
|
+
part,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function automaticPartsForMessage(
|
|
161
|
+
selected: ChatUISelectedMessage,
|
|
162
|
+
): Array<ChatUISelectedPart> {
|
|
163
|
+
return selected.parts.filter(
|
|
164
|
+
(part) => !(part.key === 'toolResult' && part.matched),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function selectMessageUI(
|
|
169
|
+
message: UIMessage,
|
|
170
|
+
input: Omit<ChatUISelectInput, 'messages'>,
|
|
171
|
+
): ChatUISelectedMessage {
|
|
172
|
+
return (
|
|
173
|
+
selectChatUI({ ...input, messages: [message] }).messages[0] ?? {
|
|
174
|
+
message,
|
|
175
|
+
parts: [],
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
}
|