@intelligo-dev/chat 1.0.0-beta.13
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/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +116 -0
- package/dist/artifact-writer.d.ts +45 -0
- package/dist/artifact-writer.d.ts.map +1 -0
- package/dist/artifact-writer.js +77 -0
- package/dist/artifact-writer.js.map +1 -0
- package/dist/attachments.d.ts +56 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +204 -0
- package/dist/attachments.js.map +1 -0
- package/dist/body.d.ts +72 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +174 -0
- package/dist/body.js.map +1 -0
- package/dist/client.d.ts +65 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +61 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +322 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/feedback.d.ts +22 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +46 -0
- package/dist/feedback.js.map +1 -0
- package/dist/generation.d.ts +104 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +85 -0
- package/dist/generation.js.map +1 -0
- package/dist/handler.d.ts +30 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +913 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +19 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +34 -0
- package/dist/messages.js.map +1 -0
- package/dist/parts.d.ts +117 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +13 -0
- package/dist/parts.js.map +1 -0
- package/dist/quota.d.ts +32 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +81 -0
- package/dist/quota.js.map +1 -0
- package/dist/share.d.ts +24 -0
- package/dist/share.d.ts.map +1 -0
- package/dist/share.js +78 -0
- package/dist/share.js.map +1 -0
- package/dist/testing.d.ts +36 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +82 -0
- package/dist/testing.js.map +1 -0
- package/dist/title.d.ts +7 -0
- package/dist/title.d.ts.map +1 -0
- package/dist/title.js +15 -0
- package/dist/title.js.map +1 -0
- package/dist/usage.d.ts +21 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +35 -0
- package/dist/usage.js.map +1 -0
- package/dist/windowing.d.ts +38 -0
- package/dist/windowing.d.ts.map +1 -0
- package/dist/windowing.js +82 -0
- package/dist/windowing.js.map +1 -0
- package/package.json +78 -0
- package/src/artifact-writer.ts +114 -0
- package/src/attachments.ts +262 -0
- package/src/body.ts +236 -0
- package/src/client.ts +133 -0
- package/src/config.ts +376 -0
- package/src/errors.ts +80 -0
- package/src/feedback.ts +62 -0
- package/src/generation.ts +150 -0
- package/src/handler.ts +1164 -0
- package/src/index.ts +93 -0
- package/src/messages.ts +39 -0
- package/src/parts.ts +143 -0
- package/src/quota.ts +105 -0
- package/src/share.ts +103 -0
- package/src/testing.ts +150 -0
- package/src/title.ts +15 -0
- package/src/usage.ts +46 -0
- package/src/windowing.ts +110 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an application tells the chat transport. Two things are required,
|
|
3
|
+
* the execution boundary and a way to run the model; everything else has a
|
|
4
|
+
* default that gives a clean install a working chat with no API keys.
|
|
5
|
+
* Optional seams close over the caller's tenancy, so the model is never told
|
|
6
|
+
* which workspace it is in. `streamTurn` replaces the model call and nothing
|
|
7
|
+
* else: auth, rate limit, gate, admission, persistence and settlement stay
|
|
8
|
+
* the transport's.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
LanguageModel,
|
|
13
|
+
StopCondition,
|
|
14
|
+
ToolSet,
|
|
15
|
+
UIMessage,
|
|
16
|
+
UIMessageChunk,
|
|
17
|
+
UIMessageStreamWriter,
|
|
18
|
+
} from "ai";
|
|
19
|
+
import type { streamText } from "ai";
|
|
20
|
+
|
|
21
|
+
/** The provider-specific options `streamText` accepts (`ai` does not export the type). */
|
|
22
|
+
export type ProviderOptions = NonNullable<
|
|
23
|
+
Parameters<typeof streamText>[0]["providerOptions"]
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
import type { Conversation } from "@intelligo-dev/core/conversations";
|
|
27
|
+
import type { Executions } from "@intelligo-dev/executions";
|
|
28
|
+
|
|
29
|
+
import type { ChatAttachmentPolicy } from "./body";
|
|
30
|
+
import type { ChatErrorCode, ChatModelOption } from "./client";
|
|
31
|
+
import type { ChatMessages } from "./errors";
|
|
32
|
+
import type { ChatGenerationOptions } from "./generation";
|
|
33
|
+
import type { ChatDataChunk, ChatUIMessage } from "./parts";
|
|
34
|
+
import type { TokenUsage } from "./usage";
|
|
35
|
+
import type { ConversationWindowOptions } from "./windowing";
|
|
36
|
+
|
|
37
|
+
export type { ChatAttachmentPolicy } from "./body";
|
|
38
|
+
export type { ChatMessages, ChatMessageKey, ChatMessageParams } from "./errors";
|
|
39
|
+
export type { ChatGenerationOptions } from "./generation";
|
|
40
|
+
|
|
41
|
+
/** The resolved caller. Every read and write is scoped to this pair. */
|
|
42
|
+
export interface ChatActor {
|
|
43
|
+
workspaceId: string;
|
|
44
|
+
userId: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What is known about a turn before the agent is resolved. */
|
|
48
|
+
export interface ChatTurnContext extends ChatActor {
|
|
49
|
+
request: Request;
|
|
50
|
+
conversationId: string;
|
|
51
|
+
/**
|
|
52
|
+
* Fields the client transport sent beyond the AI SDK's own — an
|
|
53
|
+
* `agentId`, a `modelId`. Opaque to the transport; `resolveAgent`
|
|
54
|
+
* reads them. `modelId` is also read by the default resolution when
|
|
55
|
+
* `models` is configured.
|
|
56
|
+
*/
|
|
57
|
+
body: Record<string, unknown>;
|
|
58
|
+
/** The existing row, or null on a conversation's first turn. */
|
|
59
|
+
conversation: Conversation | null;
|
|
60
|
+
trigger: "submit-message" | "regenerate-message" | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Write a part to the client mid-turn — a status line, a task plan,
|
|
63
|
+
* a document streaming into the canvas (`createArtifactWriter`). A
|
|
64
|
+
* no-op before the stream opens and after it closes, so a tool bound
|
|
65
|
+
* through `agent.tools` may hold on to it.
|
|
66
|
+
*/
|
|
67
|
+
write: (chunk: ChatDataChunk) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Merge a patch into the conversation's `metadata` — a runtime's
|
|
70
|
+
* session id, a summary of pruned history. Shallow: top-level keys
|
|
71
|
+
* are replaced, other keys kept. Rejects before the row exists.
|
|
72
|
+
*/
|
|
73
|
+
updateMetadata: (patch: Record<string, unknown>) => Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Add the tokens a tool spent on its own model call — a search that
|
|
76
|
+
* asks a model, a sub-agent — to what this turn settles. They are
|
|
77
|
+
* priced as the turn's model, so a call on another model is its own
|
|
78
|
+
* execution rather than usage added here. Usage added after the run
|
|
79
|
+
* settles is not charged.
|
|
80
|
+
*/
|
|
81
|
+
addUsage: (usage: TokenUsage) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The agent this turn runs as. */
|
|
85
|
+
export interface ResolvedAgent {
|
|
86
|
+
/** Stored on the conversation row when the transport creates it. */
|
|
87
|
+
id: string;
|
|
88
|
+
systemPrompt: string;
|
|
89
|
+
tools?: ToolSet;
|
|
90
|
+
/** Names the model may call this turn; every tool when omitted. */
|
|
91
|
+
activeTools?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* Added after `stepCountIs(maxSteps)`, e.g. `hasToolCall("askUser")`.
|
|
94
|
+
*
|
|
95
|
+
* Consulted only on a turn that has tools: the SDK stops a run on
|
|
96
|
+
* this condition "when there are tool results in the last step", and
|
|
97
|
+
* a turn with no tools never has any, so it is a single step either
|
|
98
|
+
* way. The step cap itself is not removable — an uncapped step count
|
|
99
|
+
* is an uncapped bill.
|
|
100
|
+
*/
|
|
101
|
+
stopWhen?: StopCondition<ToolSet> | StopCondition<ToolSet>[];
|
|
102
|
+
/** Overrides the request's and the config's model. Must be a registered model id. */
|
|
103
|
+
modelId?: string;
|
|
104
|
+
maxSteps?: number;
|
|
105
|
+
/** Overrides the config's; `null` disables the gate for this agent. */
|
|
106
|
+
featureKey?: string | null;
|
|
107
|
+
capability?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Passed to `streamText` as-is — the provider's own knobs, e.g. a
|
|
110
|
+
* thinking budget (`{ google: { thinkingConfig: { includeThoughts: true } } }`,
|
|
111
|
+
* `{ anthropic: { thinking: { type: "enabled", budgetTokens: 2048 } } }`).
|
|
112
|
+
* With `reasoning: true` this is what makes a model's thoughts reach
|
|
113
|
+
* the transcript at all; the transport never names a provider.
|
|
114
|
+
*/
|
|
115
|
+
providerOptions?: ProviderOptions;
|
|
116
|
+
/**
|
|
117
|
+
* How the model samples this turn — temperature, a token ceiling, a
|
|
118
|
+
* tool choice, a seed. An allowlist of the `streamText` options that
|
|
119
|
+
* do not touch settlement; see `ChatGenerationOptions` for what the
|
|
120
|
+
* transport keeps and why.
|
|
121
|
+
*/
|
|
122
|
+
generation?: ChatGenerationOptions;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The one-agent shorthand, derived from `ResolvedAgent` so the two
|
|
127
|
+
* cannot drift: everything a `resolveAgent` function can return is
|
|
128
|
+
* settable here too, and the three fields below are the only ones that
|
|
129
|
+
* differ — an id and a prompt because the transport has defaults for
|
|
130
|
+
* them, and tools because the shorthand may close over the turn.
|
|
131
|
+
*/
|
|
132
|
+
export interface ChatAgentConfig extends Omit<
|
|
133
|
+
ResolvedAgent,
|
|
134
|
+
"id" | "systemPrompt" | "tools"
|
|
135
|
+
> {
|
|
136
|
+
/** Default `"assistant"`. */
|
|
137
|
+
id?: string;
|
|
138
|
+
systemPrompt?: string;
|
|
139
|
+
tools?: ToolSet | ((turn: ChatTurnContext) => ToolSet | Promise<ToolSet>);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A turn with its agent resolved — what the hooks below receive. */
|
|
143
|
+
export interface ChatTurn extends ChatTurnContext {
|
|
144
|
+
agent: ResolvedAgent;
|
|
145
|
+
/** The persisted history, read lazily: not every `prepareMessages` needs it. */
|
|
146
|
+
history: () => Promise<UIMessage[]>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What the model is shown. */
|
|
150
|
+
export interface PreparedTurn {
|
|
151
|
+
messages: UIMessage[];
|
|
152
|
+
/** Replaces the agent's system prompt when set — a summary prefix, injected context. */
|
|
153
|
+
system?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A turn produced by something other than `streamText`.
|
|
158
|
+
*
|
|
159
|
+
* `stream` carries the AI SDK's UI message chunks. The transport writes
|
|
160
|
+
* `start` and `finish` itself and drops any the stream emits, so a
|
|
161
|
+
* runtime whose adapter already frames the message needs no stripping.
|
|
162
|
+
* `usage` settles the execution: it resolves once the run is over,
|
|
163
|
+
* with the whole run's tokens. On a client abort the transport settles
|
|
164
|
+
* with whatever `usage` resolves to; reject it and the turn is failed.
|
|
165
|
+
*/
|
|
166
|
+
export interface TurnStream {
|
|
167
|
+
stream: ReadableStream<UIMessageChunk>;
|
|
168
|
+
usage: Promise<
|
|
169
|
+
TokenUsage & {
|
|
170
|
+
modelId?: string;
|
|
171
|
+
finishReason?: string;
|
|
172
|
+
}
|
|
173
|
+
>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type StreamTurn = (
|
|
177
|
+
turn: ChatTurn,
|
|
178
|
+
prepared: PreparedTurn,
|
|
179
|
+
context: {
|
|
180
|
+
modelId: string;
|
|
181
|
+
abortSignal: AbortSignal;
|
|
182
|
+
writer: UIMessageStreamWriter<ChatUIMessage>;
|
|
183
|
+
}
|
|
184
|
+
) => TurnStream | Promise<TurnStream>;
|
|
185
|
+
|
|
186
|
+
export interface RateLimitDecision {
|
|
187
|
+
allowed: boolean;
|
|
188
|
+
retryAfterSeconds?: number;
|
|
189
|
+
limit?: number;
|
|
190
|
+
remaining?: number;
|
|
191
|
+
resetAt?: Date;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ChatTurnEvents {
|
|
195
|
+
/** The execution is open and the model is about to run. */
|
|
196
|
+
start?: (event: {
|
|
197
|
+
turn: ChatTurn;
|
|
198
|
+
executionId: string;
|
|
199
|
+
requestId: string;
|
|
200
|
+
modelId: string;
|
|
201
|
+
}) => void | Promise<void>;
|
|
202
|
+
/** The run settled — after a normal finish, or after the client aborted. */
|
|
203
|
+
complete?: (event: {
|
|
204
|
+
turn: ChatTurn;
|
|
205
|
+
executionId: string;
|
|
206
|
+
modelId: string;
|
|
207
|
+
usage: TokenUsage;
|
|
208
|
+
aborted: boolean;
|
|
209
|
+
finishReason?: string;
|
|
210
|
+
rawFinishReason?: string;
|
|
211
|
+
providerMetadata?: unknown;
|
|
212
|
+
warnings?: unknown[];
|
|
213
|
+
}) => void | Promise<void>;
|
|
214
|
+
/** Something threw. `phase` says where; the turn may or may not have an agent yet. */
|
|
215
|
+
fail?: (event: {
|
|
216
|
+
turn: ChatTurnContext;
|
|
217
|
+
error: unknown;
|
|
218
|
+
phase: "stream" | "settlement" | "persistence" | "title" | "unhandled";
|
|
219
|
+
}) => void | Promise<void>;
|
|
220
|
+
/** The transport answered with a refusal rather than a stream. */
|
|
221
|
+
refuse?: (event: {
|
|
222
|
+
actor: ChatActor | null;
|
|
223
|
+
conversationId: string | null;
|
|
224
|
+
code: ChatErrorCode;
|
|
225
|
+
status: number;
|
|
226
|
+
reasonCode?: string;
|
|
227
|
+
}) => void | Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* The user answered a tool's approval request. Fired once per
|
|
230
|
+
* response, from the continuation turn that carries it — the audit
|
|
231
|
+
* trail a product needs for a gated action.
|
|
232
|
+
*/
|
|
233
|
+
approval?: (event: {
|
|
234
|
+
turn: ChatTurnContext;
|
|
235
|
+
toolName: string;
|
|
236
|
+
toolCallId: string;
|
|
237
|
+
approvalId: string;
|
|
238
|
+
approved: boolean;
|
|
239
|
+
reason?: string;
|
|
240
|
+
}) => void | Promise<void>;
|
|
241
|
+
/** The reader voted on a reply, or cleared a vote. */
|
|
242
|
+
feedback?: (event: {
|
|
243
|
+
actor: ChatActor;
|
|
244
|
+
conversationId: string;
|
|
245
|
+
messageId: string;
|
|
246
|
+
vote: "up" | "down" | null;
|
|
247
|
+
}) => void | Promise<void>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface ChatModelsConfig {
|
|
251
|
+
/**
|
|
252
|
+
* Models the request may ask for by `modelId`. A list, or a function
|
|
253
|
+
* of the caller for a list that depends on the plan. A request naming
|
|
254
|
+
* a model outside it, or one whose `featureKey` the workspace lacks,
|
|
255
|
+
* is refused as `FEATURE_GATED` with reason `model_not_allowed`.
|
|
256
|
+
*/
|
|
257
|
+
options:
|
|
258
|
+
| ChatModelOption[]
|
|
259
|
+
| ((actor: ChatActor) => ChatModelOption[] | Promise<ChatModelOption[]>);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface ChatServerConfig {
|
|
263
|
+
/** The execution boundary, with its ports bound by the composition root. */
|
|
264
|
+
executions: Executions;
|
|
265
|
+
model: {
|
|
266
|
+
/** Must be registered in `@intelligo-dev/executions/pricing`. */
|
|
267
|
+
defaultId: string;
|
|
268
|
+
/**
|
|
269
|
+
* Turns a model id into a model for `streamText`. Required unless
|
|
270
|
+
* `streamTurn` is set, and unused when it is.
|
|
271
|
+
*/
|
|
272
|
+
resolve?: (
|
|
273
|
+
modelId: string,
|
|
274
|
+
turn: ChatTurnContext
|
|
275
|
+
) => LanguageModel | Promise<LanguageModel>;
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Runs the turn instead of `streamText` — a Mastra agent, an eve
|
|
279
|
+
* session, a workflow. Everything around the model call stays the
|
|
280
|
+
* transport's. See `TurnStream`.
|
|
281
|
+
*/
|
|
282
|
+
streamTurn?: StreamTurn;
|
|
283
|
+
|
|
284
|
+
/** Runs first on every request — the place to call `composeIntelligo()`. */
|
|
285
|
+
onRequest?: () => void | Promise<void>;
|
|
286
|
+
/** Plan feature key gating the surface. `null` disables the gate. Default `"chat"`. */
|
|
287
|
+
featureKey?: string | null;
|
|
288
|
+
/** Capability recorded on each execution. Default `"chat.message"`. */
|
|
289
|
+
capability?: string;
|
|
290
|
+
/** Longest user message accepted, in characters. Default 8000. */
|
|
291
|
+
maxMessageLength?: number;
|
|
292
|
+
/** Model steps one turn may take (a tool call and the reply using it are two). Default 5. */
|
|
293
|
+
maxSteps?: number;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The one-agent shorthand. Ignored when `resolveAgent` is set.
|
|
297
|
+
* `tools` may close over the turn's tenancy.
|
|
298
|
+
*/
|
|
299
|
+
agent?: ChatAgentConfig;
|
|
300
|
+
/**
|
|
301
|
+
* Which agent runs this turn — from the body, the row, a table. The
|
|
302
|
+
* default resolves `agent`, keeping the id the conversation was
|
|
303
|
+
* created with.
|
|
304
|
+
*/
|
|
305
|
+
resolveAgent?: (
|
|
306
|
+
turn: ChatTurnContext
|
|
307
|
+
) => ResolvedAgent | Promise<ResolvedAgent>;
|
|
308
|
+
/** The models a request may pick from. Unset: every request runs the default. */
|
|
309
|
+
models?: ChatModelsConfig;
|
|
310
|
+
/**
|
|
311
|
+
* What the model is shown. The default windows the incoming
|
|
312
|
+
* transcript by `windowing`. An application that prunes, summarises
|
|
313
|
+
* or injects context binds its own; `turn.history()` reads the
|
|
314
|
+
* persisted messages when the client's copy is not to be trusted.
|
|
315
|
+
*/
|
|
316
|
+
prepareMessages?: (
|
|
317
|
+
turn: ChatTurn,
|
|
318
|
+
incoming: UIMessage[]
|
|
319
|
+
) => PreparedTurn | Promise<PreparedTurn>;
|
|
320
|
+
/** Default `{ maxMessages: 40 }`; `false` shows the model everything. */
|
|
321
|
+
windowing?: ConversationWindowOptions | false;
|
|
322
|
+
/** File parts the transport accepts. Default `false`: any file part is a 400. */
|
|
323
|
+
attachments?: ChatAttachmentPolicy | false;
|
|
324
|
+
/** Stream the model's reasoning parts to the client. Default false. */
|
|
325
|
+
reasoning?: boolean;
|
|
326
|
+
/** Stream the model's source parts (citations) to the client. Default false. */
|
|
327
|
+
sources?: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Attach `{ modelId, usage, finishedAt }` to the assistant message as
|
|
330
|
+
* its metadata when the turn finishes. Default true.
|
|
331
|
+
*/
|
|
332
|
+
messageMetadata?: boolean;
|
|
333
|
+
/**
|
|
334
|
+
* Answer cross-origin requests from these origins — an embedded
|
|
335
|
+
* widget on another site. Unset: no CORS headers, same-origin only.
|
|
336
|
+
*/
|
|
337
|
+
cors?: { origins: readonly string[] };
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Titles a conversation from its first user message when the row is
|
|
341
|
+
* created. Sync: stored on create. Async: the row is created untitled,
|
|
342
|
+
* renamed when the promise resolves, and the client is sent a
|
|
343
|
+
* transient `data-chat-title` part. Default: first line, truncated.
|
|
344
|
+
*/
|
|
345
|
+
deriveTitle?: (
|
|
346
|
+
firstUserText: string,
|
|
347
|
+
turn: ChatTurnContext
|
|
348
|
+
) => string | null | Promise<string | null>;
|
|
349
|
+
/**
|
|
350
|
+
* Where the turn's messages go. Default: the user message that
|
|
351
|
+
* opened the turn and the assistant reply, upserted through core.
|
|
352
|
+
* `false` persists nothing.
|
|
353
|
+
*/
|
|
354
|
+
persist?:
|
|
355
|
+
| false
|
|
356
|
+
| ((
|
|
357
|
+
turn: ChatTurn,
|
|
358
|
+
result: {
|
|
359
|
+
userMessage: UIMessage | null;
|
|
360
|
+
responseMessage: UIMessage;
|
|
361
|
+
isContinuation: boolean;
|
|
362
|
+
}
|
|
363
|
+
) => Promise<void>);
|
|
364
|
+
/** Adjusts what settlement records — a floor for providers that report nothing. */
|
|
365
|
+
normalizeUsage?: (usage: TokenUsage) => TokenUsage;
|
|
366
|
+
/** Merged into the execution's metadata on begin and complete. */
|
|
367
|
+
metadata?: (turn: ChatTurn) => Record<string, unknown>;
|
|
368
|
+
onTurn?: ChatTurnEvents;
|
|
369
|
+
|
|
370
|
+
/** Localised refusal copy. Default: English. */
|
|
371
|
+
messages?: (request: Request) => ChatMessages | Promise<ChatMessages>;
|
|
372
|
+
/** Default: `requireWorkspace()`. Throw to answer 401. */
|
|
373
|
+
authenticate?: (request: Request) => Promise<ChatActor>;
|
|
374
|
+
/** Default: the workspace plan's per-minute limit. `false` disables. */
|
|
375
|
+
rateLimit?: false | ((actor: ChatActor) => Promise<RateLimitDecision>);
|
|
376
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refusals: one status per code, one message key per situation.
|
|
3
|
+
*
|
|
4
|
+
* The transport never invents copy. It asks a translator for a key,
|
|
5
|
+
* and the application binds that translator to its own message files
|
|
6
|
+
* (`messages(request)` in the config). The English table below is the
|
|
7
|
+
* default a deployment gets when it binds nothing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChatErrorBody, ChatErrorCode } from "./client";
|
|
11
|
+
|
|
12
|
+
export const CHAT_ERROR_STATUS: Readonly<Record<ChatErrorCode, number>> = {
|
|
13
|
+
BAD_REQUEST: 400,
|
|
14
|
+
UNAUTHORIZED: 401,
|
|
15
|
+
QUOTA_EXCEEDED: 402,
|
|
16
|
+
FEATURE_GATED: 403,
|
|
17
|
+
NOT_FOUND: 404,
|
|
18
|
+
RATE_LIMITED: 429,
|
|
19
|
+
INTERNAL: 500,
|
|
20
|
+
BILLING_NOT_CONFIGURED: 503,
|
|
21
|
+
MODEL_UNAVAILABLE: 503,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every message the transport can answer with. One key per situation,
|
|
26
|
+
* not per code: `BAD_REQUEST` has three.
|
|
27
|
+
*/
|
|
28
|
+
export type ChatMessageKey =
|
|
29
|
+
| "invalidBody"
|
|
30
|
+
| "messageTooLong"
|
|
31
|
+
| "attachmentRejected"
|
|
32
|
+
| "unauthorized"
|
|
33
|
+
| "rateLimited"
|
|
34
|
+
| "featureGated"
|
|
35
|
+
| "notFound"
|
|
36
|
+
| "quotaExceeded"
|
|
37
|
+
| "billingNotConfigured"
|
|
38
|
+
| "modelUnavailable"
|
|
39
|
+
| "internalError"
|
|
40
|
+
| "streamError";
|
|
41
|
+
|
|
42
|
+
export type ChatMessageParams = Record<string, string | number>;
|
|
43
|
+
|
|
44
|
+
/** Resolves a message key in the caller's locale. */
|
|
45
|
+
export type ChatMessages = (
|
|
46
|
+
key: ChatMessageKey,
|
|
47
|
+
params?: ChatMessageParams
|
|
48
|
+
) => string;
|
|
49
|
+
|
|
50
|
+
const ENGLISH: Readonly<Record<ChatMessageKey, string>> = {
|
|
51
|
+
invalidBody: "Invalid request body.",
|
|
52
|
+
messageTooLong: "Message is too long (max {max} characters).",
|
|
53
|
+
attachmentRejected: "That attachment type isn't accepted.",
|
|
54
|
+
unauthorized: "Unauthorized.",
|
|
55
|
+
rateLimited: "Rate limit exceeded. Try again in {seconds}s.",
|
|
56
|
+
featureGated: "Chat isn't included in your current plan.",
|
|
57
|
+
notFound: "That conversation no longer exists.",
|
|
58
|
+
quotaExceeded: "Usage quota exceeded. Upgrade your plan or purchase credits.",
|
|
59
|
+
billingNotConfigured: "Billing is not configured for this deployment.",
|
|
60
|
+
modelUnavailable: "Chat is temporarily unavailable. Please try again later.",
|
|
61
|
+
internalError: "Something went wrong.",
|
|
62
|
+
streamError: "Something went wrong while generating a response.",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The default translator: English, with `{param}` interpolation. */
|
|
66
|
+
export const DEFAULT_CHAT_MESSAGES: ChatMessages = (key, params = {}) =>
|
|
67
|
+
ENGLISH[key].replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
68
|
+
name in params ? String(params[name]) : match
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/** A JSON refusal with the status its code fixes. */
|
|
72
|
+
export function refuse(
|
|
73
|
+
code: ChatErrorCode,
|
|
74
|
+
error: string,
|
|
75
|
+
extra: Partial<Omit<ChatErrorBody, "error" | "code">> = {},
|
|
76
|
+
headers: Record<string, string> = {}
|
|
77
|
+
): Response {
|
|
78
|
+
const body: ChatErrorBody = { error, code, ...extra };
|
|
79
|
+
return Response.json(body, { status: CHAT_ERROR_STATUS[code], headers });
|
|
80
|
+
}
|
package/src/feedback.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reader's verdict on a reply, recorded once and reported once.
|
|
3
|
+
*
|
|
4
|
+
* The registry's `voteMessage` action is two lines over this: the
|
|
5
|
+
* write goes through core's votes table, and the `feedback` hook is
|
|
6
|
+
* the one place telemetry learns about it — the same hook whether the
|
|
7
|
+
* vote came from the page, a panel or a widget.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
clearVote,
|
|
12
|
+
isConversationServiceError,
|
|
13
|
+
voteMessage,
|
|
14
|
+
} from "@intelligo-dev/core/conversations";
|
|
15
|
+
|
|
16
|
+
import type { ChatActor, ChatServerConfig } from "./config";
|
|
17
|
+
|
|
18
|
+
export type ChatFeedback = "up" | "down" | null;
|
|
19
|
+
|
|
20
|
+
export type RecordChatFeedbackResult =
|
|
21
|
+
{ ok: true } | { ok: false; code: "not_found" | "database_error" };
|
|
22
|
+
|
|
23
|
+
export async function recordChatFeedback(
|
|
24
|
+
config: Pick<ChatServerConfig, "onTurn">,
|
|
25
|
+
actor: ChatActor,
|
|
26
|
+
params: { conversationId: string; messageId: string; vote: ChatFeedback }
|
|
27
|
+
): Promise<RecordChatFeedbackResult> {
|
|
28
|
+
try {
|
|
29
|
+
if (params.vote === null) {
|
|
30
|
+
await clearVote(actor, {
|
|
31
|
+
chatId: params.conversationId,
|
|
32
|
+
messageId: params.messageId,
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
await voteMessage(actor, {
|
|
36
|
+
chatId: params.conversationId,
|
|
37
|
+
messageId: params.messageId,
|
|
38
|
+
type: params.vote,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (
|
|
43
|
+
isConversationServiceError(error) &&
|
|
44
|
+
(error.code === "not_found" || error.code === "forbidden")
|
|
45
|
+
) {
|
|
46
|
+
return { ok: false, code: "not_found" };
|
|
47
|
+
}
|
|
48
|
+
return { ok: false, code: "database_error" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await config.onTurn?.feedback?.({
|
|
53
|
+
actor,
|
|
54
|
+
conversationId: params.conversationId,
|
|
55
|
+
messageId: params.messageId,
|
|
56
|
+
vote: params.vote,
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// Telemetry must never fail a vote.
|
|
60
|
+
}
|
|
61
|
+
return { ok: true };
|
|
62
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `streamText` options a product may set per turn: how the model
|
|
3
|
+
* samples (temperature, a token ceiling, a tool choice, a seed), none of
|
|
4
|
+
* which changes what settlement reads back. Written out as an allowlist
|
|
5
|
+
* rather than an `Omit<>` over the SDK's type, so an option a later SDK adds
|
|
6
|
+
* is not admitted silently.
|
|
7
|
+
*
|
|
8
|
+
* What the transport keeps, and why each one is not negotiable:
|
|
9
|
+
*
|
|
10
|
+
* `abortSignal` without `request.signal` an abandoned run bills in
|
|
11
|
+
* full and `onAbort` never fires.
|
|
12
|
+
* `timeout` a timeout lands on the error path, which releases
|
|
13
|
+
* the hold without charging tokens the provider has
|
|
14
|
+
* already produced. The route's deadline is
|
|
15
|
+
* `maxDuration`.
|
|
16
|
+
* `onFinish`, the transport captures `totalUsage` there; replacing
|
|
17
|
+
* `onEnd` it means every turn ends "stream ended without
|
|
18
|
+
* usage".
|
|
19
|
+
* `onAbort` settles from `sumStepUsage(steps)`.
|
|
20
|
+
* `onError` the failure path is `run.fail()` plus localised copy.
|
|
21
|
+
* `model` admission already priced the resolved `modelId`;
|
|
22
|
+
* another model bills one admission never saw.
|
|
23
|
+
* `system`, `prepareMessages` owns what the model is shown.
|
|
24
|
+
* `messages`,
|
|
25
|
+
* `prompt`,
|
|
26
|
+
* `instructions`
|
|
27
|
+
* `tools`, already seams on the agent, and the same object
|
|
28
|
+
* `activeTools` feeds `convertToModelMessages`; a second source
|
|
29
|
+
* desynchronises the two.
|
|
30
|
+
* `stopWhen` `stepCountIs(maxSteps)` must stay first and
|
|
31
|
+
* unremovable — an uncapped step count is an uncapped
|
|
32
|
+
* bill. `ResolvedAgent.stopWhen` appends to it.
|
|
33
|
+
* `_internal` the SDK's own test seam, not a product knob.
|
|
34
|
+
*
|
|
35
|
+
* Enforced three ways, because a cast defeats the type: the type itself,
|
|
36
|
+
* `pickGenerationOptions` copying only `GENERATION_KEYS` at runtime, and the
|
|
37
|
+
* handler spreading the result *first* so the transport's own keys win.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type {
|
|
41
|
+
LanguageModelCallOptions,
|
|
42
|
+
PrepareStepFunction,
|
|
43
|
+
RequestOptions,
|
|
44
|
+
StreamTextTransform,
|
|
45
|
+
TelemetryOptions,
|
|
46
|
+
ToolChoice,
|
|
47
|
+
ToolSet,
|
|
48
|
+
} from "ai";
|
|
49
|
+
import type { streamText } from "ai";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Settlement-neutral `streamText` options, sourced from the SDK's own
|
|
53
|
+
* types by indexed access so that a signature change upstream is a
|
|
54
|
+
* compile error here rather than a silently dropped field.
|
|
55
|
+
*
|
|
56
|
+
* Nested under `generation` rather than flattened onto `ResolvedAgent`
|
|
57
|
+
* for a concrete reason: `ai` has its own `reasoning` (an effort level
|
|
58
|
+
* for the model) and `ChatServerConfig.reasoning` already means "stream
|
|
59
|
+
* reasoning parts to the client". Flattening would put two different
|
|
60
|
+
* `reasoning` in one namespace, and every future SDK option would be
|
|
61
|
+
* one name collision away from a framework field.
|
|
62
|
+
*/
|
|
63
|
+
export interface ChatGenerationOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Ceiling on the tokens one step may produce. Unset, the transport
|
|
66
|
+
* uses the registered model's `maxOutputTokens` — the figure admission
|
|
67
|
+
* sized its hold with. A larger value here lets a step cost more than
|
|
68
|
+
* was held.
|
|
69
|
+
*/
|
|
70
|
+
maxOutputTokens?: LanguageModelCallOptions["maxOutputTokens"];
|
|
71
|
+
temperature?: LanguageModelCallOptions["temperature"];
|
|
72
|
+
topP?: LanguageModelCallOptions["topP"];
|
|
73
|
+
topK?: LanguageModelCallOptions["topK"];
|
|
74
|
+
presencePenalty?: LanguageModelCallOptions["presencePenalty"];
|
|
75
|
+
frequencyPenalty?: LanguageModelCallOptions["frequencyPenalty"];
|
|
76
|
+
stopSequences?: LanguageModelCallOptions["stopSequences"];
|
|
77
|
+
seed?: LanguageModelCallOptions["seed"];
|
|
78
|
+
/**
|
|
79
|
+
* The model's reasoning effort — not `ChatServerConfig.reasoning`,
|
|
80
|
+
* which decides whether reasoning parts reach the client.
|
|
81
|
+
*/
|
|
82
|
+
reasoning?: LanguageModelCallOptions["reasoning"];
|
|
83
|
+
/** Retries on a failed provider call. A retried call still reports one `totalUsage`. */
|
|
84
|
+
maxRetries?: RequestOptions["maxRetries"];
|
|
85
|
+
headers?: RequestOptions["headers"];
|
|
86
|
+
/** Picks among the tools the agent already declared; cannot introduce one. */
|
|
87
|
+
toolChoice?: ToolChoice<ToolSet>;
|
|
88
|
+
/** Per-step adjustment inside a loop the transport still caps with `maxSteps`. */
|
|
89
|
+
prepareStep?: PrepareStepFunction<ToolSet>;
|
|
90
|
+
/** Observation only. The current name; `experimental_telemetry` is the SDK's deprecated alias. */
|
|
91
|
+
telemetry?: TelemetryOptions;
|
|
92
|
+
/** Transforms the text stream. Usage is computed from steps, not from the transformed text. */
|
|
93
|
+
experimental_transform?: StreamTextTransform<ToolSet>;
|
|
94
|
+
/** Per-step observation `onTurn` has no equivalent for. */
|
|
95
|
+
onStepEnd?: Parameters<typeof streamText>[0]["onStepEnd"];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every key of `ChatGenerationOptions`, as values.
|
|
100
|
+
*
|
|
101
|
+
* `satisfies` proves the array holds only real keys; `Exhaustive` below
|
|
102
|
+
* proves it holds all of them, so a field added to the interface
|
|
103
|
+
* without a line here is a compile error rather than an option that
|
|
104
|
+
* silently never reaches the model.
|
|
105
|
+
*/
|
|
106
|
+
export const GENERATION_KEYS = [
|
|
107
|
+
"maxOutputTokens",
|
|
108
|
+
"temperature",
|
|
109
|
+
"topP",
|
|
110
|
+
"topK",
|
|
111
|
+
"presencePenalty",
|
|
112
|
+
"frequencyPenalty",
|
|
113
|
+
"stopSequences",
|
|
114
|
+
"seed",
|
|
115
|
+
"reasoning",
|
|
116
|
+
"maxRetries",
|
|
117
|
+
"headers",
|
|
118
|
+
"toolChoice",
|
|
119
|
+
"prepareStep",
|
|
120
|
+
"telemetry",
|
|
121
|
+
"experimental_transform",
|
|
122
|
+
"onStepEnd",
|
|
123
|
+
] as const satisfies ReadonlyArray<keyof ChatGenerationOptions>;
|
|
124
|
+
|
|
125
|
+
/** Fails to compile if `GENERATION_KEYS` misses a key of the interface. */
|
|
126
|
+
type Missing = Exclude<
|
|
127
|
+
keyof ChatGenerationOptions,
|
|
128
|
+
(typeof GENERATION_KEYS)[number]
|
|
129
|
+
>;
|
|
130
|
+
// Stryker disable next-line BooleanLiteral: equivalent — the assertion is the type annotation, which `tsc` checks and the test runner's transform strips; the value itself is never read, so `false` changes nothing a test could observe.
|
|
131
|
+
const _exhaustive: Missing extends never ? true : never = true;
|
|
132
|
+
void _exhaustive;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The allowlisted options, copied by name.
|
|
136
|
+
*
|
|
137
|
+
* Absent keys produce no own property rather than an explicit
|
|
138
|
+
* `undefined`: the handler spreads the result into a call where a
|
|
139
|
+
* present-but-undefined key would override the SDK's own default.
|
|
140
|
+
*/
|
|
141
|
+
export function pickGenerationOptions(
|
|
142
|
+
options: ChatGenerationOptions | undefined
|
|
143
|
+
): Partial<ChatGenerationOptions> {
|
|
144
|
+
if (!options) return {};
|
|
145
|
+
const picked: Record<string, unknown> = {};
|
|
146
|
+
for (const key of GENERATION_KEYS) {
|
|
147
|
+
if (options[key] !== undefined) picked[key] = options[key];
|
|
148
|
+
}
|
|
149
|
+
return picked as Partial<ChatGenerationOptions>;
|
|
150
|
+
}
|