@frontera-sdk/chat 1.50.40
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 +202 -0
- package/README.md +215 -0
- package/package.json +110 -0
- package/src/auth.ts +27 -0
- package/src/callback-page.ts +133 -0
- package/src/chat-client.ts +472 -0
- package/src/connect-popup.ts +352 -0
- package/src/doctor.ts +176 -0
- package/src/hooks.ts +202 -0
- package/src/provider.tsx +25 -0
- package/src/stream.ts +61 -0
- package/src/token-session.ts +182 -0
- package/src/transport.ts +200 -0
- package/src/types.ts +370 -0
- package/src/ui-stream.ts +49 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request/response shapes for the chat surface, mirrored from the service's
|
|
3
|
+
* TypeBox schemas (chat-router.ts, session-router.ts). Mirrored by hand rather
|
|
4
|
+
* than imported: the SDK publishes source and must not depend on the service
|
|
5
|
+
* package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ChatRole = 'user' | 'assistant' | 'system'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A message as `POST /v1/chat` accepts it. `parts` is deliberately untyped:
|
|
12
|
+
* the server persists parts opaquely (a narrow schema on the wire stripped
|
|
13
|
+
* part fields once — see the comment in chat-router.ts), and the SDK follows.
|
|
14
|
+
*/
|
|
15
|
+
export interface ChatInputMessage {
|
|
16
|
+
id: string
|
|
17
|
+
role: ChatRole
|
|
18
|
+
content?: string
|
|
19
|
+
parts: unknown[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContextAttachment {
|
|
23
|
+
type: string
|
|
24
|
+
id: string
|
|
25
|
+
name?: string
|
|
26
|
+
meta?: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FileAttachment {
|
|
30
|
+
/** Publicly reachable URL — under API-key auth there is no upload endpoint. */
|
|
31
|
+
url: string
|
|
32
|
+
storagePath?: string
|
|
33
|
+
filename: string
|
|
34
|
+
mediaType: string
|
|
35
|
+
size: number
|
|
36
|
+
pageCount?: number
|
|
37
|
+
isScanned?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ReasoningEffort = 'max' | 'xhigh' | 'high' | 'medium' | 'low' | 'minimal' | 'none'
|
|
41
|
+
|
|
42
|
+
export interface StartChatRequest {
|
|
43
|
+
/** 1–5 messages; the server rehydrates earlier history from the conversation. */
|
|
44
|
+
messages: ChatInputMessage[]
|
|
45
|
+
conversationId?: string
|
|
46
|
+
editMessageId?: string
|
|
47
|
+
contextAttachments?: ContextAttachment[]
|
|
48
|
+
fileAttachments?: FileAttachment[]
|
|
49
|
+
analysisMode?: string
|
|
50
|
+
webSearchAugment?: boolean
|
|
51
|
+
modelMode?: string | null
|
|
52
|
+
thinkingEnabled?: boolean
|
|
53
|
+
reasoningEffort?: ReasoningEffort
|
|
54
|
+
/** Ignored under sak_/cht_ auth — the credential's agent always wins. */
|
|
55
|
+
agentName?: string
|
|
56
|
+
surface?: 'web' | 'mobile' | 'console'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What `startChat` extracts from the response before handing over the stream. */
|
|
60
|
+
export interface StartChatResult {
|
|
61
|
+
conversationId: string
|
|
62
|
+
runId: string
|
|
63
|
+
runtime: string
|
|
64
|
+
quotaWarning?: string
|
|
65
|
+
/** The raw response; `response.body` is the AI SDK v6 UI-message SSE stream. */
|
|
66
|
+
response: Response
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- auth ---
|
|
70
|
+
|
|
71
|
+
export interface ExchangeTokenRequest {
|
|
72
|
+
externalUserId: string
|
|
73
|
+
name?: string
|
|
74
|
+
metadata?: Record<string, unknown>
|
|
75
|
+
ttlSeconds?: number
|
|
76
|
+
/**
|
|
77
|
+
* Which agent the token reaches. Accepted when minting with a workspace key
|
|
78
|
+
* scoped for chat; an agent key is fixed to its own agent and refuses a
|
|
79
|
+
* different one. Omit with a workspace key for a token that reaches every
|
|
80
|
+
* agent, the way a platform user does.
|
|
81
|
+
*/
|
|
82
|
+
agentId?: string
|
|
83
|
+
agentName?: string
|
|
84
|
+
/**
|
|
85
|
+
* Limit this token to a SUBSET of the workspace's agents — ids or slugs.
|
|
86
|
+
*
|
|
87
|
+
* Omit and it reaches every agent the minting credential can (an agent key
|
|
88
|
+
* always fixes exactly one). Naming a subset is how a product that exposes
|
|
89
|
+
* two of five agents stops the other three from being reachable by a guessed
|
|
90
|
+
* id, not merely hidden from the picker.
|
|
91
|
+
*/
|
|
92
|
+
agentIds?: string[]
|
|
93
|
+
/**
|
|
94
|
+
* What YOUR product authorises this person to do beyond chatting. Everything
|
|
95
|
+
* here is DENIED unless asked for:
|
|
96
|
+
*
|
|
97
|
+
* - `plugin-connect` — connect their own account (Gmail, Drive, …) for a plugin
|
|
98
|
+
* that needs one. All-or-nothing across the plugins the workspace has
|
|
99
|
+
* opened to external users; a per-plugin scope is planned (see the TODO in
|
|
100
|
+
* the service's `chat-token.ts`).
|
|
101
|
+
* - `schedule` — read and manage the agent's scheduled tasks.
|
|
102
|
+
* - `agent-mode` — agent work items, exchanges, handoffs, agent conversations.
|
|
103
|
+
*
|
|
104
|
+
* The base token chats and nothing more: it sends, reads its own history and
|
|
105
|
+
* answers what the agent parks. Decide the rest against your own roles — we
|
|
106
|
+
* never model them. The decision rides on the token and expires with it, so
|
|
107
|
+
* withdrawing it on your side takes effect with nothing to synchronise.
|
|
108
|
+
*/
|
|
109
|
+
capabilities?: string[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ChatToken {
|
|
113
|
+
token: string
|
|
114
|
+
expiresAt: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What an upload returns — the shape `fileAttachments` expects. */
|
|
118
|
+
export interface UploadedFile {
|
|
119
|
+
url: string
|
|
120
|
+
storagePath?: string
|
|
121
|
+
filename: string
|
|
122
|
+
mediaType: string
|
|
123
|
+
size: number
|
|
124
|
+
pageCount?: number
|
|
125
|
+
isScanned?: boolean
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A document, image or deck the agent produced.
|
|
130
|
+
*
|
|
131
|
+
* The named fields are the ones a viewer reads; the index signature carries
|
|
132
|
+
* whatever else the deployment returns. Naming them matters — under an index
|
|
133
|
+
* signature alone every read is `unknown`, and a renderer cannot tell a title
|
|
134
|
+
* from a MIME type.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* One revision of an artifact, as the version picker needs it.
|
|
138
|
+
*
|
|
139
|
+
* Metadata only — no `content`, no `storageKey`. Drawing a menu does not
|
|
140
|
+
* require every revision's bytes, and the preview route serves them when a
|
|
141
|
+
* revision is actually opened.
|
|
142
|
+
*/
|
|
143
|
+
export interface ArtifactVersionMeta {
|
|
144
|
+
version: number
|
|
145
|
+
createdAt?: string
|
|
146
|
+
title?: string | null
|
|
147
|
+
/** Who saved it — an agent turn, or the person editing in the viewer. */
|
|
148
|
+
createdBy?: string | null
|
|
149
|
+
sizeBytes?: number | null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface ChatArtifact {
|
|
153
|
+
id: string
|
|
154
|
+
title?: string | null
|
|
155
|
+
kind?: string | null
|
|
156
|
+
/** Rendered format: markdown, csv, sheet, pdf, image, htmldeck, canvas. */
|
|
157
|
+
format?: string | null
|
|
158
|
+
mimeType?: string | null
|
|
159
|
+
/** The artifact's text, when it has one. */
|
|
160
|
+
content?: string | null
|
|
161
|
+
/** Source language for a code artifact, used to pick a subtitle and lexer. */
|
|
162
|
+
language?: string | null
|
|
163
|
+
conversationId?: string | null
|
|
164
|
+
/** Where the bytes live, for artifacts stored as files rather than text. */
|
|
165
|
+
storageKey?: string | null
|
|
166
|
+
/**
|
|
167
|
+
* A rendered PDF of this artifact, when the format needs one to be shown.
|
|
168
|
+
*
|
|
169
|
+
* Set for `.docx`/`.pptx` and anything else a browser cannot open. Present
|
|
170
|
+
* means `getArtifactPreview(id)` will answer; absent means it will not.
|
|
171
|
+
*/
|
|
172
|
+
previewKey?: string | null
|
|
173
|
+
createdAt?: string
|
|
174
|
+
updatedAt?: string
|
|
175
|
+
[key: string]: unknown
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** An agent the current credential may talk to — enough to draw a picker. */
|
|
179
|
+
export interface ChatAgent {
|
|
180
|
+
id: string
|
|
181
|
+
agentId: string
|
|
182
|
+
name: string
|
|
183
|
+
description?: string | null
|
|
184
|
+
avatarConfig?: unknown
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- approvals ---
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A tool call the agent has parked, waiting for a person to decide.
|
|
191
|
+
*
|
|
192
|
+
* The run is BLOCKED while this exists: an agent whose approval mode is `ask`
|
|
193
|
+
* (or whose tool the safety reviewer would not clear on its own) streams a
|
|
194
|
+
* `tool-approval-request` chunk carrying `approvalId` and then stops until
|
|
195
|
+
* `respondToApproval` settles it or `expiresAt` passes. An integration that
|
|
196
|
+
* never answers looks like an agent that silently stopped mid-sentence.
|
|
197
|
+
*
|
|
198
|
+
* Named fields are what a card needs to render; the index signature carries
|
|
199
|
+
* whatever else the deployment reports.
|
|
200
|
+
*/
|
|
201
|
+
export interface PendingApproval {
|
|
202
|
+
id: string
|
|
203
|
+
/** `pending` while it waits; then `approved`, `denied` or `expired`. */
|
|
204
|
+
status: string
|
|
205
|
+
/** `needs_approval` for a human gate; `needs_connect` for a missing account. */
|
|
206
|
+
pauseReason: string
|
|
207
|
+
/** Which app the agent was reaching for — pass it to `startAppConnection`. */
|
|
208
|
+
installId?: string | null
|
|
209
|
+
toolCallId?: string | null
|
|
210
|
+
toolName?: string | null
|
|
211
|
+
/** Where the tool came from: an app, an MCP server, the platform. */
|
|
212
|
+
toolSource?: string | null
|
|
213
|
+
risk?: string | null
|
|
214
|
+
/** A short, secret-safe description of what the tool is about to do. */
|
|
215
|
+
actionSummary?: unknown
|
|
216
|
+
/** Why the policy asked rather than allowing. */
|
|
217
|
+
policyReason?: string | null
|
|
218
|
+
expiresAt?: string
|
|
219
|
+
[key: string]: unknown
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type ApprovalDecision = 'approve' | 'deny'
|
|
223
|
+
|
|
224
|
+
// --- app connections ---
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Starting a connect for a tool that needs the END USER's own account.
|
|
228
|
+
*
|
|
229
|
+
* `redirectUri` is a callback on YOUR domain, registered both with your OAuth
|
|
230
|
+
* client and on the workspace's embed allowlist. It has to be yours: a
|
|
231
|
+
* provider only authorises redirect URIs under a domain the OAuth project's
|
|
232
|
+
* owner has verified, and you cannot verify ours.
|
|
233
|
+
*/
|
|
234
|
+
export interface StartAppConnectionRequest {
|
|
235
|
+
/** From the parked call's `installId`; or name the app with `catalogKind`. */
|
|
236
|
+
installId?: string
|
|
237
|
+
catalogKind?: string
|
|
238
|
+
redirectUri: string
|
|
239
|
+
/** Where your page sends the user afterwards — origin must be allowlisted. */
|
|
240
|
+
returnTo?: string
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface StartAppConnectionResult {
|
|
244
|
+
/** Open this in a popup or a full-page redirect. */
|
|
245
|
+
authUrl: string
|
|
246
|
+
installId: string
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** What your callback forwards to us: the provider's `code` and our `state`. */
|
|
250
|
+
export interface CompleteAppConnectionRequest {
|
|
251
|
+
code: string
|
|
252
|
+
state: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface CompleteAppConnectionResult {
|
|
256
|
+
installId: string
|
|
257
|
+
/** The account's email, when the provider reports one. */
|
|
258
|
+
email: string | null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The re-run of a parked call. `ok: false` with `reason: 'no_account'` means
|
|
263
|
+
* the connection still is not usable — the row stays parked, so the same card
|
|
264
|
+
* can retry.
|
|
265
|
+
*/
|
|
266
|
+
export type ResumeToolCallResult =
|
|
267
|
+
| { ok: true; [key: string]: unknown }
|
|
268
|
+
| { ok: false; reason: string; error?: string }
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The chunk that announces a parked tool call, as it arrives on the stream.
|
|
272
|
+
*
|
|
273
|
+
* Not exported by the AI SDK's own chunk union, so it is declared here for
|
|
274
|
+
* consumers switching on chunk types from `chatChunkStream`.
|
|
275
|
+
*/
|
|
276
|
+
export interface ToolApprovalRequestChunk {
|
|
277
|
+
type: 'tool-approval-request'
|
|
278
|
+
approvalId: string
|
|
279
|
+
toolCallId: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- sessions ---
|
|
283
|
+
|
|
284
|
+
export interface CreateSessionRequest {
|
|
285
|
+
agentName?: string
|
|
286
|
+
modelMode?: string | null
|
|
287
|
+
approvalMode?: 'ask' | 'auto' | 'full'
|
|
288
|
+
workspaceId?: string
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface UpdateSessionRequest {
|
|
292
|
+
title?: string
|
|
293
|
+
status?: string
|
|
294
|
+
pinned?: boolean
|
|
295
|
+
group?: string | null
|
|
296
|
+
modelMode?: string | null
|
|
297
|
+
approvalMode?: 'ask' | 'auto' | 'full'
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface ListSessionsQuery {
|
|
301
|
+
cursor?: string
|
|
302
|
+
limit?: number
|
|
303
|
+
status?: string
|
|
304
|
+
label?: string
|
|
305
|
+
group?: string
|
|
306
|
+
search?: string
|
|
307
|
+
pinned?: boolean
|
|
308
|
+
source?: 'chat' | 'automation' | 'all'
|
|
309
|
+
agentName?: string
|
|
310
|
+
agentId?: string
|
|
311
|
+
updatedAfter?: string
|
|
312
|
+
updatedBefore?: string
|
|
313
|
+
surface?: string
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface Paginated<T> {
|
|
317
|
+
data: T[]
|
|
318
|
+
nextCursor?: string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Loose by design: these are the fields the server guarantees today; anything
|
|
323
|
+
* else rides in the index signature until the service exposes a shared
|
|
324
|
+
* contract.
|
|
325
|
+
*/
|
|
326
|
+
export interface Session {
|
|
327
|
+
id: string
|
|
328
|
+
title?: string | null
|
|
329
|
+
status?: string
|
|
330
|
+
agentName?: string | null
|
|
331
|
+
pinned?: boolean
|
|
332
|
+
createdAt?: string
|
|
333
|
+
updatedAt?: string
|
|
334
|
+
[key: string]: unknown
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** UIMessage-shaped: feed straight into `useChat({ messages })`. */
|
|
338
|
+
export interface SessionMessage {
|
|
339
|
+
id: string
|
|
340
|
+
role: ChatRole
|
|
341
|
+
parts: unknown[]
|
|
342
|
+
metadata?: Record<string, unknown>
|
|
343
|
+
createdAt: string
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Custom data parts the server streams alongside text. Payloads are `unknown`
|
|
348
|
+
* in v1; the names are stable and safe to switch on in `onData`.
|
|
349
|
+
*/
|
|
350
|
+
export type ChatDataPartName =
|
|
351
|
+
| 'data-context'
|
|
352
|
+
| 'data-context-estimate'
|
|
353
|
+
| 'data-context-observed'
|
|
354
|
+
| 'data-context-maintenance'
|
|
355
|
+
| 'data-compaction'
|
|
356
|
+
| 'data-work-queued'
|
|
357
|
+
| 'data-desktop'
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Called before any method that touches an existing conversation. Under
|
|
361
|
+
* API-key auth the server skips conversation ownership checks entirely, so a
|
|
362
|
+
* consumer serving multiple end-users MUST decide here whether the current
|
|
363
|
+
* user may touch `conversationId` — typically a lookup in their own
|
|
364
|
+
* user↔conversation store.
|
|
365
|
+
*/
|
|
366
|
+
export type AuthorizeConversation = (conversationId: string) => boolean | Promise<boolean>
|
|
367
|
+
|
|
368
|
+
export interface ChatClientOptions {
|
|
369
|
+
authorizeConversation?: AuthorizeConversation
|
|
370
|
+
}
|
package/src/ui-stream.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseJsonEventStream,
|
|
3
|
+
readUIMessageStream,
|
|
4
|
+
type UIMessage,
|
|
5
|
+
type UIMessageChunk,
|
|
6
|
+
} from 'ai'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decode a chat SSE `Response` into the AI SDK's typed chunk stream.
|
|
10
|
+
*
|
|
11
|
+
* `readUIMessageStream` consumes chunks, not responses, so this is the bridge
|
|
12
|
+
* for server-side consumers holding the raw `Response` from `startChat`/
|
|
13
|
+
* `resumeStream`. The SSE layer (including the server's `: ping` keepalive
|
|
14
|
+
* comments and the `[DONE]` terminator) is handled by the AI SDK's own parser
|
|
15
|
+
* — the SDK never hand-parses SSE.
|
|
16
|
+
*/
|
|
17
|
+
export function chatChunkStream(response: Response): ReadableStream<UIMessageChunk> {
|
|
18
|
+
if (!response.body) {
|
|
19
|
+
throw new Error('chat response has no body to stream')
|
|
20
|
+
}
|
|
21
|
+
// No schema: chunk validation happens in readUIMessageStream's own state
|
|
22
|
+
// machine, and the service can add new data-part chunk types without the SDK
|
|
23
|
+
// rejecting them at the parse layer.
|
|
24
|
+
return parseJsonEventStream({
|
|
25
|
+
stream: response.body,
|
|
26
|
+
schema: undefined,
|
|
27
|
+
} as unknown as Parameters<typeof parseJsonEventStream>[0]).pipeThrough(
|
|
28
|
+
new TransformStream({
|
|
29
|
+
transform(result, controller) {
|
|
30
|
+
if (!result.success) throw result.error
|
|
31
|
+
controller.enqueue(result.value as UIMessageChunk)
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Iterate assembled `UIMessage` snapshots from a chat response:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const run = await chat.startChat({ messages: [...] })
|
|
42
|
+
* for await (const message of readChatStream(run.response)) {
|
|
43
|
+
* // message.parts grows as the agent streams
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function readChatStream(response: Response): AsyncIterable<UIMessage> {
|
|
48
|
+
return readUIMessageStream({ stream: chatChunkStream(response) })
|
|
49
|
+
}
|