@tanstack/ai-client 0.29.2 → 0.31.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/byok/client.d.ts +6 -1
- package/dist/esm/byok/client.js +15 -6
- package/dist/esm/byok/client.js.map +1 -1
- package/dist/esm/chat-client.d.ts +0 -2
- package/dist/esm/chat-client.js +2 -6
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -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/dist/esm/web-mcp-tools.d.ts +49 -0
- package/dist/esm/web-mcp-tools.js +80 -0
- package/dist/esm/web-mcp-tools.js.map +1 -0
- package/package.json +6 -2
- package/src/byok/client.ts +28 -7
- package/src/chat-client.ts +2 -10
- package/src/index.ts +7 -0
- package/src/ui/selectors.ts +178 -0
- package/src/ui/types.ts +243 -0
- package/src/ui.ts +37 -0
- package/src/web-mcp-tools.ts +211 -0
package/dist/esm/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { InterruptManager } from './interrupt-manager.js';
|
|
|
5
5
|
export type { InterruptManagerHydration, InterruptManagerOptions, InterruptManagerSubmission, } from './interrupt-manager.js';
|
|
6
6
|
export { createMcpAppBridge } from './mcp-app-bridge.js';
|
|
7
7
|
export type { McpAppBridge, CreateMcpAppBridgeOptions } from './mcp-app-bridge.js';
|
|
8
|
+
export { registerWebMCPTools } from './web-mcp-tools.js';
|
|
9
|
+
export type { RegisterWebMCPToolsOptions, WebMCPToolAnnotations, WebMCPToolOptions, WebMCPToolOptionsByName, } from './web-mcp-tools.js';
|
|
8
10
|
export { RealtimeClient } from './realtime-client.js';
|
|
9
11
|
export { GenerationClient } from './generation-client.js';
|
|
10
12
|
export { VideoGenerationClient } from './video-generation-client.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DurableStreamIncompleteError, StreamReconnectLimitError, StreamTruncate
|
|
|
4
4
|
import { InterruptManager } from "./interrupt-manager.js";
|
|
5
5
|
import { ChatClient } from "./chat-client.js";
|
|
6
6
|
import { createMcpAppBridge } from "./mcp-app-bridge.js";
|
|
7
|
+
import { registerWebMCPTools } from "./web-mcp-tools.js";
|
|
7
8
|
import { RealtimeClient } from "./realtime-client.js";
|
|
8
9
|
import { GENERATION_EVENTS } from "./generation-types.js";
|
|
9
10
|
import { GenerationClient } from "./generation-client.js";
|
|
@@ -13,4 +14,4 @@ import { clientTools, createChatClientOptions } from "./types.js";
|
|
|
13
14
|
import { StorageUnavailableError, indexedDBPersistence, localStoragePersistence, sessionStoragePersistence } from "./storage-adapters.js";
|
|
14
15
|
import { createAIDevtoolsGenerationPreview } from "./devtools.js";
|
|
15
16
|
import { BatchStrategy, CompositeStrategy, ImmediateStrategy, PartialJSONParser, PunctuationStrategy, StreamProcessor, WordBoundaryStrategy, convertMessagesToModelMessages, defaultJSONParser, generateMessageId, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, uiMessageToModelMessages } from "@tanstack/ai/client";
|
|
16
|
-
export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents };
|
|
17
|
+
export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { UIMessage } from '../types.js';
|
|
2
|
+
import { ChatUIInterrupt, ChatUIPartKey, ChatUISelectInput, ChatUISelectedMessage, ChatUISelectedPart, ChatUISelection } from './types.js';
|
|
3
|
+
export declare function partTypeToKey(type: string): ChatUIPartKey | string;
|
|
4
|
+
export declare function collectInlineToolNames(toolInterrupts?: Record<string, unknown>, mappedToolNames?: ReadonlyArray<string>): Array<string>;
|
|
5
|
+
export declare function resolveInterruptComponent(interrupt: ChatUIInterrupt, interruptsMap: {
|
|
6
|
+
tools?: Record<string, unknown>;
|
|
7
|
+
generic?: Record<string, unknown>;
|
|
8
|
+
} | undefined): unknown;
|
|
9
|
+
export declare function selectChatUI(input: ChatUISelectInput): ChatUISelection;
|
|
10
|
+
export declare function automaticPartsForMessage(selected: ChatUISelectedMessage): Array<ChatUISelectedPart>;
|
|
11
|
+
export declare function selectMessageUI(message: UIMessage, input: Omit<ChatUISelectInput, 'messages'>): ChatUISelectedMessage;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/ui/selectors.ts
|
|
2
|
+
var PART_KEY_BY_TYPE = {
|
|
3
|
+
text: "text",
|
|
4
|
+
image: "image",
|
|
5
|
+
audio: "audio",
|
|
6
|
+
video: "video",
|
|
7
|
+
document: "document",
|
|
8
|
+
thinking: "thinking",
|
|
9
|
+
"tool-call": "toolCall",
|
|
10
|
+
"tool-result": "toolResult",
|
|
11
|
+
"structured-output": "structuredOutput",
|
|
12
|
+
"ui-resource": "uiResource"
|
|
13
|
+
};
|
|
14
|
+
function partTypeToKey(type) {
|
|
15
|
+
const mapped = PART_KEY_BY_TYPE[type];
|
|
16
|
+
if (mapped) return mapped;
|
|
17
|
+
return type.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
18
|
+
}
|
|
19
|
+
function collectInlineToolNames(toolInterrupts, mappedToolNames) {
|
|
20
|
+
const listPlaced = new Set(Object.keys(toolInterrupts ?? {}));
|
|
21
|
+
return (mappedToolNames ?? []).filter((name) => !listPlaced.has(name));
|
|
22
|
+
}
|
|
23
|
+
function resolveInterruptComponent(interrupt, interruptsMap) {
|
|
24
|
+
if (!interruptsMap) return void 0;
|
|
25
|
+
if (interrupt.kind === "tool-approval") return interruptsMap.tools?.[interrupt.toolName];
|
|
26
|
+
const generic = interruptsMap.generic;
|
|
27
|
+
if (!generic) return void 0;
|
|
28
|
+
const definitionId = "definitionId" in interrupt && typeof interrupt.definitionId === "string" ? interrupt.definitionId : void 0;
|
|
29
|
+
if (definitionId && definitionId !== "fallback") {
|
|
30
|
+
const registered = generic[definitionId];
|
|
31
|
+
if (registered) return registered;
|
|
32
|
+
}
|
|
33
|
+
return generic.fallback;
|
|
34
|
+
}
|
|
35
|
+
function isToolCallPart(part) {
|
|
36
|
+
return part.type === "tool-call";
|
|
37
|
+
}
|
|
38
|
+
function isToolResultPart(part) {
|
|
39
|
+
return part.type === "tool-result";
|
|
40
|
+
}
|
|
41
|
+
function isToolApproval(interrupt) {
|
|
42
|
+
return interrupt.kind === "tool-approval";
|
|
43
|
+
}
|
|
44
|
+
function selectChatUI(input) {
|
|
45
|
+
const messages = input.messages;
|
|
46
|
+
const interrupts = input.interrupts ?? [];
|
|
47
|
+
const inlineToolNames = new Set(input.inlineToolNames ?? []);
|
|
48
|
+
const resultsByCallId = /* @__PURE__ */ new Map();
|
|
49
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const message of messages) for (const part of message.parts) {
|
|
51
|
+
if (isToolCallPart(part)) callIds.add(part.id);
|
|
52
|
+
if (isToolResultPart(part)) resultsByCallId.set(part.toolCallId, part);
|
|
53
|
+
}
|
|
54
|
+
const approvalByCallId = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const interrupt of interrupts) if (isToolApproval(interrupt)) approvalByCallId.set(interrupt.toolCallId, interrupt);
|
|
56
|
+
return {
|
|
57
|
+
messages: messages.map((message) => ({
|
|
58
|
+
message,
|
|
59
|
+
parts: message.parts.map((part) => toSelectedPart(part, resultsByCallId, approvalByCallId, callIds))
|
|
60
|
+
})),
|
|
61
|
+
interrupts: interrupts.filter((interrupt) => {
|
|
62
|
+
if (!isToolApproval(interrupt)) return true;
|
|
63
|
+
if (!inlineToolNames.has(interrupt.toolName)) return true;
|
|
64
|
+
return !callIds.has(interrupt.toolCallId);
|
|
65
|
+
})
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function toSelectedPart(part, resultsByCallId, approvalByCallId, callIds) {
|
|
69
|
+
if (isToolCallPart(part)) {
|
|
70
|
+
const result = resultsByCallId.get(part.id);
|
|
71
|
+
const interrupt = approvalByCallId.get(part.id);
|
|
72
|
+
return {
|
|
73
|
+
key: "toolCall",
|
|
74
|
+
part,
|
|
75
|
+
...result ? { result } : {},
|
|
76
|
+
...interrupt ? { interrupt } : {},
|
|
77
|
+
input: part.input,
|
|
78
|
+
output: part.output
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (isToolResultPart(part)) return {
|
|
82
|
+
key: "toolResult",
|
|
83
|
+
part,
|
|
84
|
+
matched: callIds.has(part.toolCallId)
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
key: partTypeToKey(part.type),
|
|
88
|
+
part
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function automaticPartsForMessage(selected) {
|
|
92
|
+
return selected.parts.filter((part) => !(part.key === "toolResult" && part.matched));
|
|
93
|
+
}
|
|
94
|
+
function selectMessageUI(message, input) {
|
|
95
|
+
return selectChatUI({
|
|
96
|
+
...input,
|
|
97
|
+
messages: [message]
|
|
98
|
+
}).messages[0] ?? {
|
|
99
|
+
message,
|
|
100
|
+
parts: []
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { automaticPartsForMessage, collectInlineToolNames, partTypeToKey, resolveInterruptComponent, selectChatUI, selectMessageUI };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=selectors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.js","names":[],"sources":["../../../src/ui/selectors.ts"],"sourcesContent":["import type {\n MessagePart,\n ToolApprovalInterrupt,\n ToolCallPart,\n ToolResultPart,\n UIMessage,\n} from '../types'\nimport type {\n ChatUIInterrupt,\n ChatUIPartKey,\n ChatUISelectInput,\n ChatUISelectedMessage,\n ChatUISelectedPart,\n ChatUISelection,\n} from './types'\n\nconst PART_KEY_BY_TYPE: Record<string, ChatUIPartKey> = {\n text: 'text',\n image: 'image',\n audio: 'audio',\n video: 'video',\n document: 'document',\n thinking: 'thinking',\n 'tool-call': 'toolCall',\n 'tool-result': 'toolResult',\n 'structured-output': 'structuredOutput',\n 'ui-resource': 'uiResource',\n}\n\nexport function partTypeToKey(type: string): ChatUIPartKey | string {\n const mapped = PART_KEY_BY_TYPE[type]\n if (mapped) return mapped\n return type.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())\n}\n\nexport function collectInlineToolNames(\n toolInterrupts?: Record<string, unknown>,\n mappedToolNames?: ReadonlyArray<string>,\n): Array<string> {\n const listPlaced = new Set(Object.keys(toolInterrupts ?? {}))\n return (mappedToolNames ?? []).filter((name) => !listPlaced.has(name))\n}\n\nexport function resolveInterruptComponent(\n interrupt: ChatUIInterrupt,\n interruptsMap:\n | {\n tools?: Record<string, unknown>\n generic?: Record<string, unknown>\n }\n | undefined,\n): unknown {\n if (!interruptsMap) return undefined\n if (interrupt.kind === 'tool-approval') {\n return interruptsMap.tools?.[interrupt.toolName]\n }\n const generic = interruptsMap.generic\n if (!generic) return undefined\n const definitionId =\n 'definitionId' in interrupt && typeof interrupt.definitionId === 'string'\n ? interrupt.definitionId\n : undefined\n if (definitionId && definitionId !== 'fallback') {\n const registered = generic[definitionId]\n if (registered) return registered\n }\n return generic.fallback\n}\n\nfunction isToolCallPart(part: MessagePart): part is ToolCallPart {\n return part.type === 'tool-call'\n}\n\nfunction isToolResultPart(part: MessagePart): part is ToolResultPart {\n return part.type === 'tool-result'\n}\n\nfunction isToolApproval(\n interrupt: ChatUIInterrupt,\n): interrupt is ToolApprovalInterrupt {\n return interrupt.kind === 'tool-approval'\n}\n\nexport function selectChatUI(input: ChatUISelectInput): ChatUISelection {\n const messages = input.messages\n const interrupts = input.interrupts ?? []\n const inlineToolNames = new Set(input.inlineToolNames ?? [])\n\n const resultsByCallId = new Map<string, ToolResultPart>()\n const callIds = new Set<string>()\n for (const message of messages) {\n for (const part of message.parts) {\n if (isToolCallPart(part)) callIds.add(part.id)\n if (isToolResultPart(part)) resultsByCallId.set(part.toolCallId, part)\n }\n }\n\n const approvalByCallId = new Map<string, ToolApprovalInterrupt>()\n for (const interrupt of interrupts) {\n if (isToolApproval(interrupt)) {\n approvalByCallId.set(interrupt.toolCallId, interrupt)\n }\n }\n\n const selectedMessages: Array<ChatUISelectedMessage> = messages.map(\n (message) => ({\n message,\n parts: message.parts.map((part) =>\n toSelectedPart(part, resultsByCallId, approvalByCallId, callIds),\n ),\n }),\n )\n\n return {\n messages: selectedMessages,\n interrupts: interrupts.filter((interrupt) => {\n if (!isToolApproval(interrupt)) return true\n if (!inlineToolNames.has(interrupt.toolName)) return true\n return !callIds.has(interrupt.toolCallId)\n }),\n }\n}\n\nfunction toSelectedPart(\n part: MessagePart,\n resultsByCallId: Map<string, ToolResultPart>,\n approvalByCallId: Map<string, ToolApprovalInterrupt>,\n callIds: Set<string>,\n): ChatUISelectedPart {\n if (isToolCallPart(part)) {\n const result = resultsByCallId.get(part.id)\n const interrupt = approvalByCallId.get(part.id)\n return {\n key: 'toolCall',\n part,\n ...(result ? { result } : {}),\n ...(interrupt ? { interrupt } : {}),\n input: part.input,\n output: part.output,\n }\n }\n\n if (isToolResultPart(part)) {\n return {\n key: 'toolResult',\n part,\n matched: callIds.has(part.toolCallId),\n }\n }\n\n return {\n key: partTypeToKey(part.type) as Exclude<\n ChatUIPartKey,\n 'toolCall' | 'toolResult'\n >,\n part,\n }\n}\n\nexport function automaticPartsForMessage(\n selected: ChatUISelectedMessage,\n): Array<ChatUISelectedPart> {\n return selected.parts.filter(\n (part) => !(part.key === 'toolResult' && part.matched),\n )\n}\n\nexport function selectMessageUI(\n message: UIMessage,\n input: Omit<ChatUISelectInput, 'messages'>,\n): ChatUISelectedMessage {\n return (\n selectChatUI({ ...input, messages: [message] }).messages[0] ?? {\n message,\n parts: [],\n }\n )\n}\n"],"mappings":"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"}
|
|
@@ -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 };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AnyClientTool } from '@tanstack/ai/client';
|
|
2
|
+
import { ClientContextOptionFromTools, InferredClientContext } from './types.js';
|
|
3
|
+
/** WebMCP behavior hints for one registered tool. */
|
|
4
|
+
export interface WebMCPToolAnnotations {
|
|
5
|
+
/** Indicates that the tool does not modify state. */
|
|
6
|
+
readOnlyHint?: boolean;
|
|
7
|
+
/** Indicates that the tool can return content that the application does not trust. */
|
|
8
|
+
untrustedContentHint?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Display and behavior options for one WebMCP tool. */
|
|
11
|
+
export interface WebMCPToolOptions {
|
|
12
|
+
/** A human-readable title for browser user interfaces. */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Optional behavior hints for browser agents. */
|
|
15
|
+
annotations?: WebMCPToolAnnotations;
|
|
16
|
+
}
|
|
17
|
+
/** WebMCP options keyed by the inferred names in a client tool list. */
|
|
18
|
+
export type WebMCPToolOptionsByName<TTools extends ReadonlyArray<AnyClientTool>> = Partial<{
|
|
19
|
+
[TName in TTools[number]['name']]: WebMCPToolOptions;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Options for {@link registerWebMCPTools}.
|
|
23
|
+
*
|
|
24
|
+
* The signal controls the registration lifetime. Context is required when a
|
|
25
|
+
* client tool declares a required runtime context.
|
|
26
|
+
*/
|
|
27
|
+
export type RegisterWebMCPToolsOptions<TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>> = {
|
|
28
|
+
/** Removes all tools from this call when the signal aborts. */
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
/** Per-tool display and behavior options. */
|
|
31
|
+
toolOptions?: WebMCPToolOptionsByName<TTools>;
|
|
32
|
+
} & ClientContextOptionFromTools<TTools, TContext>;
|
|
33
|
+
/**
|
|
34
|
+
* Registers executable TanStack client tools with the browser WebMCP API.
|
|
35
|
+
*
|
|
36
|
+
* Unsupported browsers and server environments resolve without registration.
|
|
37
|
+
* Abort `options.signal` to remove every tool registered by this call.
|
|
38
|
+
*
|
|
39
|
+
* @param tools - The executable client tools to expose through WebMCP.
|
|
40
|
+
* @param options - The registration signal, runtime context, and per-tool options.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const controller = new AbortController()
|
|
45
|
+
* await registerWebMCPTools(tools, { signal: controller.signal })
|
|
46
|
+
* controller.abort()
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function registerWebMCPTools<const TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>>(tools: TTools, options: RegisterWebMCPToolsOptions<TTools, TContext>): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { convertSchemaToJsonSchema, validateWithStandardSchema } from "@tanstack/ai/client";
|
|
2
|
+
//#region src/web-mcp-tools.ts
|
|
3
|
+
function isWebMCPModelContext(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && "registerTool" in value && typeof value.registerTool === "function";
|
|
5
|
+
}
|
|
6
|
+
function getToolOptions(toolOptions, name) {
|
|
7
|
+
return toolOptions?.[name];
|
|
8
|
+
}
|
|
9
|
+
async function validateSchemaValue(schema, value) {
|
|
10
|
+
const result = await validateWithStandardSchema(schema, value);
|
|
11
|
+
if (result.success) return result.data;
|
|
12
|
+
throw new Error(`Validation failed: ${result.issues.map((issue) => issue.message).join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Registers executable TanStack client tools with the browser WebMCP API.
|
|
16
|
+
*
|
|
17
|
+
* Unsupported browsers and server environments resolve without registration.
|
|
18
|
+
* Abort `options.signal` to remove every tool registered by this call.
|
|
19
|
+
*
|
|
20
|
+
* @param tools - The executable client tools to expose through WebMCP.
|
|
21
|
+
* @param options - The registration signal, runtime context, and per-tool options.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const controller = new AbortController()
|
|
26
|
+
* await registerWebMCPTools(tools, { signal: controller.signal })
|
|
27
|
+
* controller.abort()
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
async function registerWebMCPTools(tools, options) {
|
|
31
|
+
if (typeof document === "undefined" || typeof isSecureContext !== "undefined" && !isSecureContext || !("modelContext" in document) || !isWebMCPModelContext(document.modelContext)) return;
|
|
32
|
+
if (tools.length === 0) return;
|
|
33
|
+
const names = /* @__PURE__ */ new Set();
|
|
34
|
+
const webMCPTools = tools.map((tool) => {
|
|
35
|
+
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(tool.name)) throw new Error(`WebMCP tool name "${tool.name}" must contain 1 to 128 ASCII letters, numbers, underscores, hyphens, or periods.`);
|
|
36
|
+
if (names.has(tool.name)) throw new Error(`Duplicate WebMCP tool name "${tool.name}".`);
|
|
37
|
+
if (tool.description.trim() === "") throw new Error(`WebMCP tool "${tool.name}" must have a description.`);
|
|
38
|
+
if (typeof tool.execute !== "function") throw new Error(`WebMCP tool "${tool.name}" must have an execute handler.`);
|
|
39
|
+
if (tool.needsApproval === true) throw new Error(`WebMCP tool "${tool.name}" cannot use needsApproval: true.`);
|
|
40
|
+
names.add(tool.name);
|
|
41
|
+
const toolOptions = getToolOptions(options.toolOptions, tool.name);
|
|
42
|
+
const inputSchema = tool.inputSchema;
|
|
43
|
+
const outputSchema = tool.outputSchema;
|
|
44
|
+
const convertedInputSchema = convertSchemaToJsonSchema(inputSchema);
|
|
45
|
+
const inputSchemaType = convertedInputSchema?.type;
|
|
46
|
+
if (typeof inputSchemaType === "string" && inputSchemaType !== "object" || Array.isArray(inputSchemaType) && !inputSchemaType.includes("object")) throw new Error(`WebMCP tool "${tool.name}" input schema must accept an object.`);
|
|
47
|
+
const execute = tool.execute;
|
|
48
|
+
return {
|
|
49
|
+
name: tool.name,
|
|
50
|
+
description: tool.description,
|
|
51
|
+
...toolOptions?.title !== void 0 ? { title: toolOptions.title } : {},
|
|
52
|
+
...convertedInputSchema !== void 0 ? { inputSchema: convertedInputSchema } : {},
|
|
53
|
+
...toolOptions?.annotations !== void 0 ? { annotations: toolOptions.annotations } : {},
|
|
54
|
+
async execute(input, executionOptions) {
|
|
55
|
+
const validatedInput = await validateSchemaValue(inputSchema, input);
|
|
56
|
+
const output = await execute(validatedInput, {
|
|
57
|
+
abortSignal: executionOptions?.signal,
|
|
58
|
+
context: options.context,
|
|
59
|
+
emitCustomEvent() {}
|
|
60
|
+
});
|
|
61
|
+
return validateSchemaValue(outputSchema, output);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
const registrationController = new AbortController();
|
|
66
|
+
const abortRegistration = () => registrationController.abort(options.signal.reason);
|
|
67
|
+
if (options.signal.aborted) abortRegistration();
|
|
68
|
+
else options.signal.addEventListener("abort", abortRegistration, { once: true });
|
|
69
|
+
try {
|
|
70
|
+
for (const tool of webMCPTools) await document.modelContext.registerTool(tool, { signal: registrationController.signal });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
registrationController.abort(error);
|
|
73
|
+
options.signal.removeEventListener("abort", abortRegistration);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { registerWebMCPTools };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=web-mcp-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-mcp-tools.js","names":[],"sources":["../../src/web-mcp-tools.ts"],"sourcesContent":["import {\n convertSchemaToJsonSchema,\n validateWithStandardSchema,\n} from '@tanstack/ai/client'\nimport type { AnyClientTool } from '@tanstack/ai/client'\nimport type {\n ClientContextOptionFromTools,\n InferredClientContext,\n} from './types'\n\ninterface WebMCPTool {\n name: string\n title?: string\n description: string\n inputSchema?: object\n annotations?: WebMCPToolAnnotations\n execute: (input: object, options: { signal: AbortSignal }) => Promise<unknown>\n}\n\ninterface WebMCPModelContext {\n registerTool: (\n tool: WebMCPTool,\n options: { signal: AbortSignal },\n ) => Promise<void>\n}\n\n/** WebMCP behavior hints for one registered tool. */\nexport interface WebMCPToolAnnotations {\n /** Indicates that the tool does not modify state. */\n readOnlyHint?: boolean\n /** Indicates that the tool can return content that the application does not trust. */\n untrustedContentHint?: boolean\n}\n\n/** Display and behavior options for one WebMCP tool. */\nexport interface WebMCPToolOptions {\n /** A human-readable title for browser user interfaces. */\n title?: string\n /** Optional behavior hints for browser agents. */\n annotations?: WebMCPToolAnnotations\n}\n\n/** WebMCP options keyed by the inferred names in a client tool list. */\nexport type WebMCPToolOptionsByName<\n TTools extends ReadonlyArray<AnyClientTool>,\n> = Partial<{\n [TName in TTools[number]['name']]: WebMCPToolOptions\n}>\n\n/**\n * Options for {@link registerWebMCPTools}.\n *\n * The signal controls the registration lifetime. Context is required when a\n * client tool declares a required runtime context.\n */\nexport type RegisterWebMCPToolsOptions<\n TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n> = {\n /** Removes all tools from this call when the signal aborts. */\n signal: AbortSignal\n /** Per-tool display and behavior options. */\n toolOptions?: WebMCPToolOptionsByName<TTools>\n} & ClientContextOptionFromTools<TTools, TContext>\n\nfunction isWebMCPModelContext(value: unknown): value is WebMCPModelContext {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'registerTool' in value &&\n typeof value.registerTool === 'function'\n )\n}\n\nfunction getToolOptions<TName extends string>(\n toolOptions: Partial<Record<TName, WebMCPToolOptions>> | undefined,\n name: TName,\n) {\n return toolOptions?.[name]\n}\n\nasync function validateSchemaValue(schema: unknown, value: unknown) {\n const result = await validateWithStandardSchema(schema, value)\n if (result.success) {\n return result.data\n }\n\n throw new Error(\n `Validation failed: ${result.issues.map((issue) => issue.message).join(', ')}`,\n )\n}\n\n/**\n * Registers executable TanStack client tools with the browser WebMCP API.\n *\n * Unsupported browsers and server environments resolve without registration.\n * Abort `options.signal` to remove every tool registered by this call.\n *\n * @param tools - The executable client tools to expose through WebMCP.\n * @param options - The registration signal, runtime context, and per-tool options.\n *\n * @example\n * ```ts\n * const controller = new AbortController()\n * await registerWebMCPTools(tools, { signal: controller.signal })\n * controller.abort()\n * ```\n */\nexport async function registerWebMCPTools<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n>(tools: TTools, options: RegisterWebMCPToolsOptions<TTools, TContext>) {\n if (\n typeof document === 'undefined' ||\n (typeof isSecureContext !== 'undefined' && !isSecureContext) ||\n !('modelContext' in document) ||\n !isWebMCPModelContext(document.modelContext)\n ) {\n return\n }\n if (tools.length === 0) {\n return\n }\n\n const names = new Set<string>()\n const webMCPTools = tools.map((tool) => {\n if (!/^[A-Za-z0-9_.-]{1,128}$/.test(tool.name)) {\n throw new Error(\n `WebMCP tool name \"${tool.name}\" must contain 1 to 128 ASCII letters, numbers, underscores, hyphens, or periods.`,\n )\n }\n if (names.has(tool.name)) {\n throw new Error(`Duplicate WebMCP tool name \"${tool.name}\".`)\n }\n if (tool.description.trim() === '') {\n throw new Error(`WebMCP tool \"${tool.name}\" must have a description.`)\n }\n if (typeof tool.execute !== 'function') {\n throw new Error(\n `WebMCP tool \"${tool.name}\" must have an execute handler.`,\n )\n }\n if (tool.needsApproval === true) {\n throw new Error(\n `WebMCP tool \"${tool.name}\" cannot use needsApproval: true.`,\n )\n }\n\n names.add(tool.name)\n const toolOptions = getToolOptions(options.toolOptions, tool.name)\n const inputSchema = tool.inputSchema\n const outputSchema = tool.outputSchema\n const convertedInputSchema = convertSchemaToJsonSchema(inputSchema)\n const inputSchemaType = convertedInputSchema?.type\n const requiresNonObjectInput =\n (typeof inputSchemaType === 'string' && inputSchemaType !== 'object') ||\n (Array.isArray(inputSchemaType) && !inputSchemaType.includes('object'))\n if (requiresNonObjectInput) {\n throw new Error(\n `WebMCP tool \"${tool.name}\" input schema must accept an object.`,\n )\n }\n const execute = tool.execute\n\n return {\n name: tool.name,\n description: tool.description,\n ...(toolOptions?.title !== undefined ? { title: toolOptions.title } : {}),\n ...(convertedInputSchema !== undefined\n ? { inputSchema: convertedInputSchema }\n : {}),\n ...(toolOptions?.annotations !== undefined\n ? { annotations: toolOptions.annotations }\n : {}),\n async execute(\n input: object,\n executionOptions?: { signal?: AbortSignal },\n ) {\n const validatedInput = await validateSchemaValue(inputSchema, input)\n const output = await execute(validatedInput, {\n abortSignal: executionOptions?.signal,\n context: options.context,\n emitCustomEvent() {},\n })\n return validateSchemaValue(outputSchema, output)\n },\n }\n })\n\n const registrationController = new AbortController()\n const abortRegistration = () =>\n registrationController.abort(options.signal.reason)\n\n if (options.signal.aborted) {\n abortRegistration()\n } else {\n options.signal.addEventListener('abort', abortRegistration, { once: true })\n }\n\n try {\n for (const tool of webMCPTools) {\n await document.modelContext.registerTool(tool, {\n signal: registrationController.signal,\n })\n }\n } catch (error) {\n registrationController.abort(error)\n options.signal.removeEventListener('abort', abortRegistration)\n throw error\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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,7 +57,7 @@
|
|
|
53
57
|
"src"
|
|
54
58
|
],
|
|
55
59
|
"dependencies": {
|
|
56
|
-
"@tanstack/ai": "^0.
|
|
60
|
+
"@tanstack/ai": "^0.53.0",
|
|
57
61
|
"@tanstack/ai-event-client": "^0.11.2",
|
|
58
62
|
"@tanstack/ai-utils": "^0.4.0"
|
|
59
63
|
},
|
package/src/byok/client.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
maskKey,
|
|
6
6
|
} from '@tanstack/ai/byok'
|
|
7
7
|
import { memoryStorage } from './storage'
|
|
8
|
-
import type { ProviderId } from '@tanstack/ai/byok'
|
|
8
|
+
import type { ByokProvider, ProviderId } from '@tanstack/ai/byok'
|
|
9
9
|
import type { Keyring, KeyringStorage } from './storage'
|
|
10
10
|
|
|
11
11
|
export type KeyStatus =
|
|
@@ -28,6 +28,11 @@ export type ByokSnapshot = {
|
|
|
28
28
|
|
|
29
29
|
export interface DefineByokOptions {
|
|
30
30
|
storage?: KeyringStorage
|
|
31
|
+
/**
|
|
32
|
+
* Descriptors whose `with` companions the store expands: `headers()` and
|
|
33
|
+
* `prepare()` for such a provider cover its companions too.
|
|
34
|
+
*/
|
|
35
|
+
providers?: ReadonlyArray<ByokProvider>
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export const EMPTY_BYOK_SNAPSHOT: ByokSnapshot = {
|
|
@@ -85,6 +90,7 @@ export class ByokClient {
|
|
|
85
90
|
#prompt: ByokPrompt | null = null
|
|
86
91
|
#coverageAll = false
|
|
87
92
|
#coverage: Record<string, boolean> = {}
|
|
93
|
+
readonly #companions: Record<string, Array<ProviderId>> = {}
|
|
88
94
|
readonly #listeners = new Set<() => void>()
|
|
89
95
|
#snapshot: ByokSnapshot
|
|
90
96
|
#storageError: string | null = null
|
|
@@ -92,6 +98,13 @@ export class ByokClient {
|
|
|
92
98
|
|
|
93
99
|
constructor(options: DefineByokOptions = {}) {
|
|
94
100
|
this.storage = options.storage ?? memoryStorage()
|
|
101
|
+
for (const provider of options.providers ?? []) {
|
|
102
|
+
if (provider.with?.length) {
|
|
103
|
+
this.#companions[requireProviderId(provider.id)] = provider.with.map(
|
|
104
|
+
(companion) => requireProviderId(companion.id),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
95
108
|
this.#locked = Boolean(this.storage.unlockable)
|
|
96
109
|
this.#snapshot = this.#buildSnapshot()
|
|
97
110
|
this.#ready = this.#hydrate()
|
|
@@ -146,12 +159,19 @@ export class ByokClient {
|
|
|
146
159
|
return this.#coverageAll || this.#coverage[provider] === true
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
/** The slug plus every companion declared on its descriptor. */
|
|
163
|
+
#expand(provider: ProviderId): Array<ProviderId> {
|
|
164
|
+
return [provider, ...(this.#companions[provider] ?? [])]
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
headers(provider?: ProviderId): Record<string, string> {
|
|
150
168
|
const headers: Record<string, string> = {}
|
|
151
169
|
if (provider) {
|
|
152
170
|
requireProviderId(provider)
|
|
153
|
-
const
|
|
154
|
-
|
|
171
|
+
for (const id of this.#expand(provider)) {
|
|
172
|
+
const key = this.#keys[id]
|
|
173
|
+
if (key) headers[byokHeaderName(id)] = key
|
|
174
|
+
}
|
|
155
175
|
return headers
|
|
156
176
|
}
|
|
157
177
|
for (const [id, key] of Object.entries(this.#keys)) {
|
|
@@ -167,10 +187,11 @@ export class ByokClient {
|
|
|
167
187
|
}
|
|
168
188
|
if (!provider) return
|
|
169
189
|
requireProviderId(provider)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
for (const id of this.#expand(provider)) {
|
|
191
|
+
if (this.#keys[id] || this.#hasCoverage(id)) continue
|
|
192
|
+
this.request(id, 'missing')
|
|
193
|
+
throw new ByokBlockedError(id, 'missing')
|
|
194
|
+
}
|
|
174
195
|
}
|
|
175
196
|
|
|
176
197
|
async update(
|
package/src/chat-client.ts
CHANGED
|
@@ -2023,7 +2023,7 @@ export class ChatClient<
|
|
|
2023
2023
|
if (emptyMessage) {
|
|
2024
2024
|
return
|
|
2025
2025
|
}
|
|
2026
|
-
if (this.
|
|
2026
|
+
if (this.hasPendingInterrupts()) {
|
|
2027
2027
|
throw new Error(
|
|
2028
2028
|
'ChatClient: cannot send normal input while pending interrupts exist. Use resumeInterrupts() instead.',
|
|
2029
2029
|
)
|
|
@@ -2065,14 +2065,6 @@ export class ChatClient<
|
|
|
2065
2065
|
return this.interruptManager.getDescriptors().length > 0
|
|
2066
2066
|
}
|
|
2067
2067
|
|
|
2068
|
-
/** True while an interrupt batch owns the next user turn. */
|
|
2069
|
-
private hasBlockingInterrupts(): boolean {
|
|
2070
|
-
return (
|
|
2071
|
-
this.activeInterruptSubmission !== undefined ||
|
|
2072
|
-
this.hasPendingInterrupts()
|
|
2073
|
-
)
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
2068
|
/** True while a stream is active, a send is claiming the client, or the queue is draining. */
|
|
2077
2069
|
private isSendBusy(): boolean {
|
|
2078
2070
|
return this.isLoading || this.sendInFlight || this.messageQueueDraining
|
|
@@ -2188,7 +2180,7 @@ export class ChatClient<
|
|
|
2188
2180
|
*/
|
|
2189
2181
|
async append(message: UIMessage | ModelMessage): Promise<void> {
|
|
2190
2182
|
this.mountDevtools()
|
|
2191
|
-
if (this.
|
|
2183
|
+
if (this.hasPendingInterrupts()) {
|
|
2192
2184
|
throw new Error(
|
|
2193
2185
|
'ChatClient: cannot append normal input while pending interrupts exist. Use resumeInterrupts() instead.',
|
|
2194
2186
|
)
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,13 @@ export type {
|
|
|
14
14
|
} from './interrupt-manager'
|
|
15
15
|
export { createMcpAppBridge } from './mcp-app-bridge'
|
|
16
16
|
export type { McpAppBridge, CreateMcpAppBridgeOptions } from './mcp-app-bridge'
|
|
17
|
+
export { registerWebMCPTools } from './web-mcp-tools'
|
|
18
|
+
export type {
|
|
19
|
+
RegisterWebMCPToolsOptions,
|
|
20
|
+
WebMCPToolAnnotations,
|
|
21
|
+
WebMCPToolOptions,
|
|
22
|
+
WebMCPToolOptionsByName,
|
|
23
|
+
} from './web-mcp-tools'
|
|
17
24
|
export { RealtimeClient } from './realtime-client'
|
|
18
25
|
export { GenerationClient } from './generation-client'
|
|
19
26
|
export { VideoGenerationClient } from './video-generation-client'
|