@tidblink/server 0.0.0-stage → 0.3.0-rc.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.
Files changed (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +993 -2
  3. package/dist/audio.d.ts +63 -0
  4. package/dist/audio.d.ts.map +1 -0
  5. package/dist/audio.js +163 -0
  6. package/dist/audio.js.map +1 -0
  7. package/dist/client-tools.d.ts +46 -0
  8. package/dist/client-tools.d.ts.map +1 -0
  9. package/dist/client-tools.js +114 -0
  10. package/dist/client-tools.js.map +1 -0
  11. package/dist/control.d.ts +17 -0
  12. package/dist/control.d.ts.map +1 -0
  13. package/dist/control.js +52 -0
  14. package/dist/control.js.map +1 -0
  15. package/dist/errors.d.ts +80 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +87 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/files.d.ts +129 -0
  20. package/dist/files.d.ts.map +1 -0
  21. package/dist/files.js +388 -0
  22. package/dist/files.js.map +1 -0
  23. package/dist/history.d.ts +242 -0
  24. package/dist/history.d.ts.map +1 -0
  25. package/dist/history.js +91 -0
  26. package/dist/history.js.map +1 -0
  27. package/dist/http.d.ts +64 -0
  28. package/dist/http.d.ts.map +1 -0
  29. package/dist/http.js +282 -0
  30. package/dist/http.js.map +1 -0
  31. package/dist/index.d.ts +26 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +12 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/organization.d.ts +70 -0
  36. package/dist/organization.d.ts.map +1 -0
  37. package/dist/organization.js +118 -0
  38. package/dist/organization.js.map +1 -0
  39. package/dist/progress.d.ts +34 -0
  40. package/dist/progress.d.ts.map +1 -0
  41. package/dist/progress.js +154 -0
  42. package/dist/progress.js.map +1 -0
  43. package/dist/realtime.d.ts +91 -0
  44. package/dist/realtime.d.ts.map +1 -0
  45. package/dist/realtime.js +112 -0
  46. package/dist/realtime.js.map +1 -0
  47. package/dist/schedulers.d.ts +129 -0
  48. package/dist/schedulers.d.ts.map +1 -0
  49. package/dist/schedulers.js +91 -0
  50. package/dist/schedulers.js.map +1 -0
  51. package/dist/session.d.ts +136 -0
  52. package/dist/session.d.ts.map +1 -0
  53. package/dist/session.js +226 -0
  54. package/dist/session.js.map +1 -0
  55. package/dist/sse.d.ts +7 -0
  56. package/dist/sse.d.ts.map +1 -0
  57. package/dist/sse.js +45 -0
  58. package/dist/sse.js.map +1 -0
  59. package/dist/turn.d.ts +43 -0
  60. package/dist/turn.d.ts.map +1 -0
  61. package/dist/turn.js +206 -0
  62. package/dist/turn.js.map +1 -0
  63. package/dist/usage.d.ts +462 -0
  64. package/dist/usage.d.ts.map +1 -0
  65. package/dist/usage.js +20 -0
  66. package/dist/usage.js.map +1 -0
  67. package/dist/user.d.ts +96 -0
  68. package/dist/user.d.ts.map +1 -0
  69. package/dist/user.js +139 -0
  70. package/dist/user.js.map +1 -0
  71. package/examples/answer-client-tool.mjs +69 -0
  72. package/examples/control-turn.mjs +48 -0
  73. package/examples/create-session.mjs +69 -0
  74. package/examples/observe-message.mjs +44 -0
  75. package/examples/organization-session.mjs +66 -0
  76. package/examples/prepare-realtime-connection.mjs +61 -0
  77. package/examples/provision-user.mjs +83 -0
  78. package/examples/read-usage.mjs +62 -0
  79. package/examples/schedule-task.mjs +67 -0
  80. package/examples/send-message.mjs +43 -0
  81. package/examples/submit-audio.mjs +52 -0
  82. package/examples/upload-file.mjs +53 -0
  83. package/package.json +65 -4
package/README.md CHANGED
@@ -1,3 +1,994 @@
1
- # Temporary Holding Version
1
+ # `@tidblink/server`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official TiDB Link SDK for backends, for Node.js 22.12 or newer. ESM only, with
4
+ TypeScript declarations and no runtime dependencies. It uses API Keys that must
5
+ stay on your backend; do not ship it to browsers or devices.
6
+
7
+ ## Supported scope
8
+
9
+ This package supports Organization information, End User provisioning, User API
10
+ Key management, Template/model discovery, personal Agent and Session management,
11
+ text submission, steering, cancellation, checkpoint retry, original-result waiting,
12
+ continuous progress, Client Tools, conversation history, file attachments, Schedulers,
13
+ Realtime Model/Connection preparation, prerecorded audio and User usage reads for [service revision
14
+ 88228780](https://github.com/mem9-ai/agent-stack/commit/882287809f91d9a3fdf60537460431d9f0fc23e7).
15
+
16
+ Text commands return durable receipts independently of execution. Waiting reads the
17
+ original command's outcome. Template version application, Memory configuration,
18
+ default Agents and general Agent overrides are removed. Browser, MSP, management and device APIs are
19
+ outside this package. Existing resource workflows accept either User authority or
20
+ Organization authority with an explicit owner.
21
+
22
+ ## Quick start
23
+
24
+ Install an exact published version. Prereleases are on the `next` dist-tag:
25
+
26
+ ```sh
27
+ npm install --save-exact @tidblink/server@next
28
+ ```
29
+
30
+ An application backend uses its Organization API Key (`ti_org_…`) and explicitly
31
+ selects the ordinary End User with `organization.user(userId)`. The Key remains
32
+ the caller; the User owns the resources. No User API Key is issued by this flow.
33
+
34
+ ```js
35
+ import { randomUUID } from "node:crypto";
36
+ import { OrganizationClient } from "@tidblink/server";
37
+ import { organizationSessionWorkflow } from "@tidblink/server/examples/organization-session.mjs";
38
+
39
+ const baseUrl = process.env.AGENT_SERVICE_URL;
40
+ const organizationApiKey = process.env.ORGANIZATION_API_KEY;
41
+ const organization = new OrganizationClient({ baseUrl, apiKey: organizationApiKey });
42
+ const endUser = await organization.createEndUser({ externalId: "your-application-user-id" });
43
+ // Retain this mapping. On later startup, load it instead of repeating creation.
44
+ const user = organization.user(endUser.userId);
45
+ const page = await user.listAgentTemplates();
46
+ const template = page.templates[0];
47
+ if (!template) throw new Error("Select an available Template; follow nextCursor if hasMore");
48
+ const creation = {
49
+ baseUrl, organizationApiKey, userId: endUser.userId,
50
+ agentTemplateId: template.agentTemplateId,
51
+ agentCreationKey: randomUUID(), sessionCreationKey: randomUUID(),
52
+ };
53
+ // Persist the selected Template, optional model and both creation identities.
54
+ const { session } = await organizationSessionWorkflow(creation);
55
+ const message = { clientEventId: randomUUID(), text: "Help me plan a trip" };
56
+ // Retain the Session, command identity and original input before dispatch.
57
+ const receipt = await session.submitMessage(message);
58
+ const result = await session.waitForResult(receipt.inputId);
59
+ console.log(result.text);
60
+ const recovered = await session.waitForResult(message.clientEventId);
61
+ ```
62
+
63
+ `externalId` is unique within an Organization. A repeated `createEndUser` returns
64
+ `resource_conflict`; recover the mapping with `listEndUsers` and the original
65
+ `externalId`. Agent/Session creation replays require the same creation keys and
66
+ inputs. Reading the original message receipt or waiting again does not execute
67
+ another Turn. An HTTP request ID is diagnostic, not a creation identity.
68
+
69
+ The installed Organization example can run with `AGENT_SERVICE_URL`,
70
+ `ORGANIZATION_API_KEY`, `END_USER_ID`, `TEMPLATE_ID`, `AGENT_CREATION_KEY` and
71
+ `SESSION_CREATION_KEY` (`MODEL_ID` is optional):
72
+
73
+ ```sh
74
+ node node_modules/@tidblink/server/examples/organization-session.mjs
75
+ ```
76
+
77
+ `organization.user(userId)` returns an `EndUserClient`. Its Agent, Session, file,
78
+ Scheduler and catalog methods are the same resource methods used by `UserClient`.
79
+ Keep usage reads on `organization.getUserUsage(userId, options)` and
80
+ `getUserUsageDetails`; their range and response shapes differ from User-Key reads.
81
+ Private MCP registration, general Drive entry management, sandbox preferences,
82
+ Scheduler Plans and native Hardware APIs are not wrapped by this SDK. Use the
83
+ pinned public HTTP contract for those existing service capabilities. Building ToD,
84
+ a device gateway or management integration remains outside this package.
85
+ Artifact APIs remain gated. The SDK prepares Realtime HTTP grants; applications
86
+ own WebSocket/media transport. This change adds neither a generated full API
87
+ client nor a media client.
88
+
89
+ ### User-Key compatibility and migration
90
+
91
+ Existing `new UserClient({ baseUrl, apiKey: userApiKey })` callers and the existing
92
+ User-Key examples remain supported. To migrate resource workflows, replace that
93
+ client with `new OrganizationClient({ baseUrl, apiKey: organizationApiKey }).user(userId)`.
94
+ Retain the same resource IDs, creation inputs and command identities; do not issue
95
+ a User Key to use Organization authority. The SDK sends the real Organization
96
+ Key only to owner-scoped service endpoints and never as an owner identity header.
97
+
98
+ The optional `provision-user.mjs` example still creates a User API Key on each
99
+ successful call, including for an existing User. Its CLI requires
100
+ `AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY`, `END_USER_EXTERNAL_ID` and
101
+ `USER_CREDENTIAL_FILE`; it reserves a new private file and refuses to overwrite it.
102
+ A lost issuance response cannot recover a plaintext key from metadata.
103
+
104
+ After Organization Key rotation or revocation, create a new `OrganizationClient`
105
+ with fresh valid authority and select the same User. Existing handles retain their
106
+ old credential. New old-authority requests, subscriptions, Scheduler fires,
107
+ Realtime tokens and Organization-issued file tickets lose authority. Accepted
108
+ Turns continue, and admitted file transfers may finish. Reconnect/obtain new
109
+ grants with fresh authority and reconcile the original command before submitting
110
+ new work. Updating retained Schedulers with fresh authority reauthorizes future
111
+ fires. Another valid Key remains usable. Disabled Users cannot start new work;
112
+ Organization authority can still read retained history/files and perform supported
113
+ cleanup. User-Key ticket lifetime remains the service's separate compatibility
114
+ contract. Application policies and K3 spending policy are separate concerns.
115
+
116
+ ## Key management
117
+
118
+ Use these methods when your application also needs a User-Key client. Organization
119
+ owner-scoped resource access does not require issuing this credential.
120
+
121
+ ```ts
122
+ import { OrganizationClient } from "@tidblink/server";
123
+ const organization = new OrganizationClient({
124
+ baseUrl, apiKey: process.env.ORGANIZATION_API_KEY,
125
+ });
126
+ const issued = await organization.createUserApiKey(endUser.userId, {
127
+ name: "Optional User-Key client",
128
+ });
129
+ const keys = await organization.listUserApiKeys(endUser.userId);
130
+ const replacement = await organization.rotateUserApiKey(issued.apiKey.apiKeyId);
131
+ // Save replacement.token; the previous token immediately stops authenticating.
132
+ const revoked = await organization.revokeUserApiKey(issued.apiKey.apiKeyId);
133
+ ```
134
+
135
+ Create and rotate return `{ apiKey, token }`. List and revoke return metadata
136
+ without the token or stored hash. Rotation preserves the key ID; revocation is
137
+ idempotent. Only active ordinary End Users can own keys. Missing, foreign or
138
+ disabled targets preserve the service's refusal.
139
+
140
+ The returned User API Key (`ti_user_…`) authenticates that User's public APIs.
141
+ It grants no Organization management authority; `OrganizationClient` rejects it.
142
+ An Organization key likewise grants no User identity.
143
+
144
+ ## Templates, Agents and Sessions
145
+
146
+ Use `organization.user(userId)` or a User-Key `UserClient` to discover available Templates and deployment models. Model
147
+ IDs are strings from the deployment catalog, including newly published models.
148
+ `effectiveDefaultModel` is the create-time default; `defaultModel` is the
149
+ service's canonical catalog default and may differ.
150
+
151
+ ```ts
152
+ import { UserClient } from "@tidblink/server";
153
+
154
+ const customer = new UserClient({
155
+ baseUrl: process.env.AGENT_SERVICE_URL!,
156
+ apiKey: process.env.USER_API_KEY!,
157
+ });
158
+ const page = await customer.listAgentTemplates({ limit: 20 });
159
+ const template = page.templates[0];
160
+ if (!template) throw new Error("Select an available Template");
161
+ const models = await customer.listModels();
162
+ const agent = await customer.createAgent({
163
+ agentTemplateId: template.agentTemplateId,
164
+ idempotencyKey: "customer-agent-1",
165
+ });
166
+ await agent.update({ additionalInstructions: "Prefer concise answers." });
167
+ const session = await agent.createSession({
168
+ idempotencyKey: "customer-conversation-1",
169
+ model: models.effectiveDefaultModel,
170
+ billingTag: { key: "account", value: "customer-1" },
171
+ });
172
+ await session.rename("Planning");
173
+ await session.suggestName({ userText: "Plan a trip" }); // Generates and saves a name.
174
+ await session.setBillingTag(null);
175
+ ```
176
+
177
+ Persist each creation key before calling. A new Agent key creates an independent
178
+ Agent even for the same Template; the same key and Template recover the original
179
+ Agent, including after unlinking. Reusing the key for another Template raises
180
+ `ConflictError` with `agent_creation_conflict`. Session creation likewise requires
181
+ a stable key and the same original input when recovering a creation. Its model,
182
+ Realtime Model and Billing Tag selections participate in that identity. Do not
183
+ generate a new key just because a response was lost.
184
+
185
+ `listAgentTemplates({ limit?, cursor? })` returns
186
+ `{ templates, hasMore, nextCursor }`; `listSessions({ limit?, cursor? })` returns
187
+ `{ sessions, hasMore, nextCursor }`, with `Session` handles in `sessions`.
188
+ Continue with the returned opaque cursor until `hasMore` is false, including
189
+ Template pages with no available entries. A changed Template catalog can refuse
190
+ continuation with `agent_template_list_changed`; restart discovery.
191
+ Session cursors belong to their owning User and list. `listAgents()` returns a
192
+ complete `Agent[]` without pagination. Use `getAgent(id)` and `getSession(id)` to
193
+ read current owned resources; `session(id)` creates a handle without a request.
194
+
195
+ Agent create/list responses contain association metadata. `getAgent`,
196
+ `agent.refresh()` and `agent.update()` also include `data.additionalInstructions`.
197
+ The service normalizes line endings to LF and trims whitespace, allows up to
198
+ 4096 UTF-8 bytes after normalization, and rejects unsupported control characters.
199
+ Null or empty text clears the setting. Updates use last committed write wins,
200
+ without an ETag, and affect only future Sessions. Multiple Agents using the same
201
+ Template retain independent settings and Sessions.
202
+
203
+ Each Session freezes its Template recipe, digest and Agent preferences at
204
+ creation. Later Template saves and Agent updates do not rewrite it. Session
205
+ model selection is separate:
206
+
207
+ ```ts
208
+ const current = await customer.getSession(session.id);
209
+ await current.setModel(models.effectiveDefaultModel);
210
+ // Another handle's intervening model change makes this handle stale.
211
+ // Refresh and make an explicit choice after a ConflictError (HTTP 412).
212
+ ```
213
+
214
+ `setModel` uses the model ETag retained by the handle, reading it first if needed.
215
+ It never silently retries a stale update. Model changes apply to the next Turn;
216
+ active execution, steering and retry retain the model admitted for that Turn.
217
+ Realtime Model selection has its own independent revision token. Availability,
218
+ Organization policy, read-only Sessions and current ownership remain service
219
+ decisions. `session.delete()` updates and returns the Session handle; `.data` contains the deleted tombstone; later ordinary reads
220
+ and lists hide the Session. Active execution or a read-only Session can refuse
221
+ deletion. Name and Billing Tag changes do not change the frozen recipe.
222
+
223
+ Organization authority can explicitly unlink an Agent belonging to a target User:
224
+
225
+ ```ts
226
+ const archived = await organization.unlinkAgent(endUser.userId, agent.id);
227
+ ```
228
+
229
+ The returned association is archived. Unlinking retains existing Session
230
+ ownership, binding, configuration and history; it does not grant the Organization
231
+ access to existing Sessions by itself. Authorized Organization owner-scoped
232
+ clients and the User retain access to saved history under the service rules.
233
+
234
+ The packaged example completes discovery and creation without message execution:
235
+
236
+ ```js
237
+ import { createSessionWorkflow } from "@tidblink/server/examples/create-session.mjs";
238
+
239
+ const { agent, session } = await createSessionWorkflow({
240
+ baseUrl: process.env.AGENT_SERVICE_URL,
241
+ userApiKey: process.env.USER_API_KEY,
242
+ agentCreationKey: process.env.AGENT_CREATION_KEY,
243
+ sessionCreationKey: process.env.SESSION_CREATION_KEY,
244
+ agentTemplateId: process.env.TEMPLATE_ID,
245
+ model: process.env.MODEL_ID,
246
+ });
247
+ ```
248
+
249
+ For direct execution, set `AGENT_SERVICE_URL`, `USER_API_KEY`,
250
+ `TEMPLATE_ID`, `AGENT_CREATION_KEY` and `SESSION_CREATION_KEY`; `MODEL_ID` is optional.
251
+ Select and persist the Template ID, optional model and both keys before calling.
252
+ The example also returns the current Template page and model catalog. If no model
253
+ was requested, it preserves omission so the service can recover the original
254
+ creation even after the deployment default changes.
255
+
256
+ ## Text messages and original results
257
+
258
+ Persist the Session ID, command identity and complete input before dispatch.
259
+ `clientEventId` is required, Session-local, and contains 1–128 safe characters
260
+ (letters, digits, `_`, `.`, `:`, `-`, starting with a letter or digit). It is
261
+ separate from the diagnostic HTTP `requestId`.
262
+
263
+ ```ts
264
+ const session = customer.session(savedSessionId);
265
+ const input = { clientEventId: savedCommandId, text: "Plan the trip" };
266
+ const receipt = await session.submitMessage(input);
267
+ // Accepted does not mean incorporated or completed.
268
+ const result = await session.waitForResult(receipt.inputId, {
269
+ timeoutMs: 60_000,
270
+ pollIntervalMs: 250,
271
+ });
272
+ console.log(result.text);
273
+ // Or submit once and wait: await session.turn(input, { timeoutMs: 60_000 });
274
+ ```
275
+
276
+ `submitMessage` returns the service's `SessionInteraction`. `mode` distinguishes
277
+ started or steered work from refused inputs; `appliedAt` records actual
278
+ incorporation. `result.status` and `result.completedAt` belong to the original
279
+ execution generation, even after an explicit retry. A running Turn can accept
280
+ another message; each message needs its own identity and incorporation status.
281
+ The same identity and input recover its original receipt. A conflicting reuse
282
+ raises `ConflictError` with `interaction_input_conflict`.
283
+
284
+ Text accepts a string or a nonempty ordered array of up to nine strings for
285
+ clarification answers. `clarificationSourceTurnId` links a saved Clarification
286
+ Item. Optional `billingTag` (including null) and ready `userFileIds` are passed
287
+ through; use [file attachments](#file-attachments) to upload and verify them.
288
+ Successful `TurnResult`
289
+ contains the original `inputId`, `sessionId`, `turnId`, saved `messageId`, `text`
290
+ and `interaction`; a clarification additionally contains `clarification`.
291
+ Drafts, Session idleness, EOF and acceptance never establish success.
292
+
293
+ `waitForResult` reads receipts and the matching saved successful Turn. Its default
294
+ local deadline is 60 seconds; `pollIntervalMs` defaults to 250 milliseconds.
295
+ `TurnWaitTimeoutError` and an aborted wait (`OutcomeUnknownError`) retain
296
+ `sessionId`, `inputId`, any known `turnId`, and the HTTP request ID when available.
297
+ Neither cancels, resubmits or changes the command. `TurnFailedError` and
298
+ `TurnInterruptedError` retain the original command and Turn identity; failure
299
+ uses `turn_failed`, since a later Turn read could describe a different generation.
300
+ A refused or successful-but-unincorporated message raises `InputNotAppliedError`.
301
+ Read its receipt to inspect the original fact.
302
+
303
+ A disconnect, malformed success response, HTTP 408 or server failure during
304
+ submission raises `OutcomeUnknownError` with the original recoverable identity.
305
+ The SDK never automatically repeats text submission. Reconcile explicitly:
306
+
307
+ ```ts
308
+ const original = await session.reconcileInteraction(savedCommandId);
309
+ // null means no receipt was visible at this read; an in-flight request may still arrive.
310
+ if (original) {
311
+ const result = await session.waitForResult(savedCommandId);
312
+ }
313
+ // getInteraction(savedCommandId) returns the receipt, or interaction_not_found (404).
314
+ ```
315
+
316
+ Keep the original input identity when deliberately repeating a submission; the
317
+ service deduplicates it. Generating another identity can execute the effects
318
+ again. `AbortSignal` before dispatch stops submission; after dispatch it cannot
319
+ establish whether the command was accepted.
320
+
321
+ The packaged example uses an existing owned Session and a caller-retained input:
322
+
323
+ ```js
324
+ import { sendMessageWorkflow } from "@tidblink/server/examples/send-message.mjs";
325
+ const result = await sendMessageWorkflow({
326
+ baseUrl: process.env.AGENT_SERVICE_URL,
327
+ userApiKey: process.env.USER_API_KEY,
328
+ sessionId: process.env.SESSION_ID,
329
+ clientEventId: process.env.CLIENT_EVENT_ID,
330
+ text: process.env.MESSAGE_TEXT,
331
+ });
332
+ ```
333
+
334
+ Set those five environment variables to run the installed example directly:
335
+ `node node_modules/@tidblink/server/examples/send-message.mjs`. It prints only the
336
+ final answer. Preserve the input across invocations; each new command identity
337
+ can start new work. This slice replaces the obsolete response-owned NDJSON
338
+ `streamTurn` API. Use the independent progress subscription below.
339
+
340
+ ## Continuous progress and reconnection
341
+
342
+ Submit once with a persisted `clientEventId`, then observe that original receipt:
343
+
344
+ ```ts
345
+ const savedCommandId = crypto.randomUUID();
346
+ // Persist savedCommandId with this message before dispatch.
347
+ const accepted = await session.submitMessage({
348
+ clientEventId: savedCommandId,
349
+ text: "Summarize the findings",
350
+ });
351
+ let cursor: string | undefined;
352
+ let draft = "";
353
+ for await (const update of session.observe(accepted.inputId, {
354
+ timeoutMs: 60_000,
355
+ })) {
356
+ if (update.type === "result") {
357
+ console.log(update.result.text);
358
+ } else {
359
+ cursor = update.event.cursor; // Persist with session.id and accepted.inputId.
360
+ if (update.type === "draft") draft = update.text; // Replace, including an empty reset.
361
+ if (update.type === "progress") console.log(update.text);
362
+ }
363
+ }
364
+ ```
365
+
366
+ `draft` is provisional, and a progress update or saved-message event is not a
367
+ successful result. Only `type: "result"` establishes the original command's
368
+ successful outcome using its durable receipt and matching saved Turn. Failures,
369
+ interruption and unincorporated inputs use the same errors as `waitForResult()`.
370
+ A later explicit retry cannot replace the earlier command's result. `observe()`
371
+ also accepts the separate retry command identity, using the same original-result
372
+ checks as `waitForResult()`; cancellation receipts are not answer receipts.
373
+
374
+ `ObservationStoppedError` retains `sessionId`, `inputId`, `turnId` when known,
375
+ the last `cursor`, and `timedOut`. On disconnect, local abort or timeout, preserve
376
+ those values. Reconnect with `session.observe(inputId, { cursor })`, or query
377
+ `getInteraction(inputId)` / `waitForResult(inputId)`; do not submit another message.
378
+ Breaking the loop stops only observation. EOF, a revoked observer credential,
379
+ Session idleness and the absence of subscribers do not establish success or cancel
380
+ execution. With revoked credentials, use another currently authorized credential
381
+ for recovery. The SDK does not automatically reconnect or resubmit.
382
+
383
+ `session.events({ cursor, signal })` exposes the independent Session-wide stream
384
+ as raw envelopes with `cursor`, `type`, `sessionId`, `turnId`, `createdAt` and
385
+ `data`, including Realtime and unfamiliar event types. It ends when its connection
386
+ ends; it has no execution-result promise. `observe()` selects the original Turn,
387
+ exposes typed draft/progress text and raw other events, and confirms its original
388
+ receipt before reporting a result. A missing/expired cursor is the service's
389
+ `validation_error` refusal; it does not silently start replay from the beginning.
390
+ A resumed draft is a complete snapshot, usable without earlier frames.
391
+
392
+ The packaged `observe-message.mjs` example observes an existing input and never
393
+ submits it again. Run it with `AGENT_SERVICE_URL`, `USER_API_KEY`, `SESSION_ID` and
394
+ `INPUT_ID`; supply `EVENT_CURSOR` to resume:
395
+
396
+ ```sh
397
+ node node_modules/@tidblink/server/examples/observe-message.mjs
398
+ ```
399
+
400
+ ## Session Client Tools
401
+
402
+ Declare the application functions that an ordinary text Turn may request. The
403
+ application executes them itself; the SDK only declares tools and exchanges calls
404
+ and results. Parameters are JSON Schema objects. The service validates reserved
405
+ names, schema size/depth and combined tool limits; the `async` argument is reserved
406
+ for the platform and is not part of application arguments.
407
+
408
+ ```ts
409
+ import type { ClientToolDeclaration } from "@tidblink/server";
410
+ const tools: ClientToolDeclaration[] = [{
411
+ type: "function",
412
+ name: "lookup_order",
413
+ description: "Read an order status",
414
+ parameters: {
415
+ type: "object",
416
+ properties: { orderId: { type: "string" } },
417
+ required: ["orderId"],
418
+ additionalProperties: false,
419
+ },
420
+ }];
421
+ const session = await agent.createSession({
422
+ idempotencyKey: savedSessionCreationKey,
423
+ tools,
424
+ });
425
+ const current = await session.getClientTools();
426
+ await session.replaceClientTools(tools, { ifMatch: current.etag });
427
+ ```
428
+
429
+ Replacement is the complete set, with `[]` clearing it. Its ETag is independent of
430
+ the Session's model ETag. Stale replacement returns `precondition_failed`; read the
431
+ set and decide again. A Turn retains the declarations admitted when it started.
432
+ An identical replacement keeps its revision. Scheduler and audio inputs do not
433
+ use the Session-client declarations under this service baseline.
434
+
435
+ After submitting a message, retrieve pending calls independently of any progress
436
+ subscription:
437
+
438
+ ```ts
439
+ const calls = await session.listPendingClientToolCalls();
440
+ const call = calls.find((call) => call.callId === savedCallId);
441
+ if (!call || Date.parse(call.deadlineAt) <= Date.now()) {
442
+ throw new Error("The call is unavailable or expired; do not execute it again");
443
+ }
444
+ // The application obtains this value from its own business function.
445
+ const output = JSON.stringify({ status: "shipped" });
446
+ const receipt = await session.submitClientToolOutput({
447
+ clientEventId: savedOutputCommandId,
448
+ turnId: call.turnId,
449
+ callId: call.callId,
450
+ output,
451
+ });
452
+ // To report a business failure, supply error: { message: "Warehouse unavailable" }
453
+ // instead of output.
454
+ ```
455
+
456
+ The result command has its own stable `clientEventId`; it is distinct from the
457
+ call's `callId` and the message's input identity. Its receipt confirms admission,
458
+ not successful completion of the message. Continue with the original message's
459
+ `waitForResult(inputId)` or `observe(inputId)`. An application-reported tool error
460
+ may still produce a successful answer explaining that failure.
461
+
462
+ Pending reads count as delivery and preserve `publishedAt`/`deadlineAt`. Calls
463
+ have a 60-second deadline from publication. Retain the call identity and the
464
+ application's result before submission; a disconnect does not authorize executing
465
+ the business function again. After uncertain submission, reconcile its original
466
+ command with `getInteraction(clientEventId)` or `reconcileInteraction(clientEventId)`.
467
+ The SDK never retries output submission automatically. Repeating the exact command
468
+ recovers its original receipt; a new command for a settled call is refused with
469
+ `client_tool_call_settled`. Foreign, unknown and Hardware calls are unavailable
470
+ through this endpoint. Hardware results belong to the device connection and are
471
+ not an SDK Client Tool capability.
472
+
473
+ The packaged `answer-client-tool.mjs` example submits one already produced result.
474
+ Set `AGENT_SERVICE_URL`, `USER_API_KEY`, `SESSION_ID`, `TURN_ID`, `CALL_ID`,
475
+ `CLIENT_EVENT_ID` and exactly one of `TOOL_OUTPUT` or `TOOL_ERROR`:
476
+
477
+ ```sh
478
+ node node_modules/@tidblink/server/examples/answer-client-tool.mjs
479
+ ```
480
+
481
+ ## Saved conversation history
482
+
483
+ ```ts
484
+ const interactions = await session.listInteractions({ limit: 20 });
485
+ const turns = await session.history({ limit: 20 });
486
+ const turn = await session.getTurn(result.turnId);
487
+ const timeline = await session.timeline({ limit: 20 });
488
+ ```
489
+
490
+ Each list returns its named array (`interactions`, `turns`, or `entries`) and
491
+ `hasMore` / `nextCursor`. Limits are 1–200, default 50. Continue with the opaque
492
+ cursor in the same User, Session and list; the SDK does not silently flatten or
493
+ refetch pages. Interactions are in ascending Session-local sequence. Turns are
494
+ in creation-time order and preserve Full Agent and user-input Realtime
495
+ projections, including input context, saved multiple user Messages, Operations,
496
+ failure details and interrupted speech metadata. Narration-only Realtime facts
497
+ remain in Timeline and associated `realtimeResponses` rather than independent
498
+ public Turns.
499
+
500
+ Timeline combines Turn snapshots and durable Realtime facts in event order. A
501
+ Turn moved behind the Realtime input that started it can appear again after a
502
+ cursor; replace entries by Turn ID or Realtime fact ID. Late Realtime facts can
503
+ precede an already consumed Turn-list cursor; refresh Turn history or use
504
+ Timeline for durable ordering. History is readable content, not authority for an
505
+ original request's result: use its interaction receipt for that outcome.
506
+
507
+ ## Steering, cancellation and checkpoint retry
508
+
509
+ Send another `submitMessage` while a Turn is active to add requirements. Its
510
+ receipt reports `mode: "steered"` and the same `turnId`; `appliedAt` records actual
511
+ incorporation. This is the service's ordinary message admission, so a message
512
+ arriving after completion may start a new Turn. No target-locked steering command
513
+ is offered by this service. Inspect the receipt rather than assuming which Turn
514
+ accepted the message.
515
+
516
+ Cancellation targets an exact Turn and has its own stable command identity:
517
+
518
+ ```ts
519
+ const stop = await session.cancel({
520
+ clientEventId: savedCancellationId,
521
+ turnId: original.turnId,
522
+ });
523
+ const cancellation = await session.getInteraction(stop.inputId);
524
+ ```
525
+
526
+ Acceptance requests a stop; it does not establish termination or undo completed
527
+ effects. Read the cancellation receipt or the original message receipt until its
528
+ `result.status` is terminal. A completed Turn can retain success if completion won
529
+ the race. A stale target is refused rather than cancelling its successor.
530
+
531
+ Checkpoint retry deliberately starts another execution of the selected Turn:
532
+
533
+ ```ts
534
+ const retry = await session.retry({
535
+ clientEventId: savedRetryId,
536
+ turnId: original.turnId,
537
+ });
538
+ const retried = await session.waitForResult(retry.clientEventId);
539
+ const earlier = await session.getInteraction(original.inputId);
540
+ ```
541
+
542
+ The retry acceptance contains `{clientEventId, sessionId, turnId, disposition:
543
+ "retrying"}`. Its durable interaction is readable using `clientEventId`, including
544
+ through `reconcileInteraction`. Waiting accepts message and retry identities; a
545
+ successful saved answer must match that interaction's execution completion time.
546
+ Earlier failed or interrupted receipts keep their original outcome even when the
547
+ Turn later succeeds. A cancellation receipt is not an answer receipt.
548
+
549
+ Retry is disabled by default in Agent Service. `checkpoint_runtime_unavailable`
550
+ and other eligibility or authority refusals remain `TiDBLinkApiError`; the SDK
551
+ never substitutes a new message. An uncertain cancellation or retry preserves its
552
+ Session, command and Turn IDs in `OutcomeUnknownError`. Read the original receipt
553
+ before deciding whether to repeat the identical command. A new ID is a new
554
+ request, not network recovery.
555
+
556
+ The installed `@tidblink/server/examples/control-turn.mjs` exports
557
+ `controlTurnWorkflow`. It returns cancellation acceptance for `action: "cancel"`
558
+ and waits for the separate retry result for `action: "retry"`. Run it with
559
+ `AGENT_SERVICE_URL`, `USER_API_KEY`, `SESSION_ID`, `TURN_ID`, `CLIENT_EVENT_ID` and
560
+ `TURN_ACTION` set; retain those identities before dispatch. This example never
561
+ chooses to retry automatically.
562
+
563
+ ## Realtime connection preparation
564
+
565
+ A backend can discover the published Realtime Models and prepare an existing
566
+ owned Session for a terminal. The SDK returns the one-time connect token but does
567
+ not open or implement the WebSocket transport.
568
+
569
+ ```ts
570
+ import { UserClient } from "@tidblink/server";
571
+
572
+ const user = new UserClient({
573
+ baseUrl: process.env.AGENT_SERVICE_URL!,
574
+ apiKey: process.env.USER_API_KEY!,
575
+ });
576
+ const catalog = await user.listRealtimeModels();
577
+ const session = user.session(process.env.SESSION_ID!);
578
+ const current = await session.getRealtimeModelSelection();
579
+ const selected = catalog.models.find((model) => model.available);
580
+ if (selected && selected.id !== current.model) {
581
+ await session.setRealtimeModel(selected.id, { ifMatch: current.etag });
582
+ }
583
+ const prepared = await session.openRealtimeConnection({
584
+ metadata: { terminal: "kiosk-1" },
585
+ });
586
+ // Hand prepared.websocketUrl, prepared.connectToken and
587
+ // prepared.connectTokenExpiresAt directly to the intended terminal.
588
+ ```
589
+
590
+ `setRealtimeModel` requires the independent ETag returned by
591
+ `getRealtimeModelSelection`; a stale ETag raises `ConflictError` with code
592
+ `realtime_model_conflict`. A selection change applies to the next Realtime
593
+ Connection. A live connection keeps the model and provider identity frozen at
594
+ its admission. Model availability, Session voice eligibility, language support,
595
+ audio support and one-live-connection admission remain service decisions.
596
+
597
+ Use `getRealtimeConnection()` to inspect the newest connection,
598
+ `createRealtimeConnectToken()` to mint another one-time credential, and
599
+ `endRealtimeConnection()` to terminate it. Connect tokens expire at the returned
600
+ `connectTokenExpiresAt` and cannot be recovered from later reads. The SDK does
601
+ not cache them. Actual media transport, audio capture/playback, codecs, browser
602
+ support and device connections are outside this package slice.
603
+
604
+ The packaged example accepts an existing Session ID and never prints the token:
605
+
606
+ ```js
607
+ import { prepareRealtimeConnection } from "@tidblink/server/examples/prepare-realtime-connection.mjs";
608
+
609
+ const prepared = await prepareRealtimeConnection({
610
+ baseUrl: process.env.AGENT_SERVICE_URL,
611
+ userApiKey: process.env.USER_API_KEY,
612
+ sessionId: process.env.SESSION_ID,
613
+ realtimeModelId: process.env.REALTIME_MODEL_ID,
614
+ });
615
+ ```
616
+
617
+ Set `AGENT_SERVICE_URL`, `USER_API_KEY`, and `SESSION_ID` to run the example
618
+ directly. `REALTIME_MODEL_ID` is optional.
619
+
620
+ ## Prerecorded audio
621
+
622
+ An existing Session can accept a recording through its separate audio entry point.
623
+ The SDK sends multipart bytes and consumes that request's own response:
624
+
625
+ ```ts
626
+ import { readFile } from "node:fs/promises";
627
+
628
+ const result = await session.audio({
629
+ bytes: await readFile("recording.wav"),
630
+ filename: "recording.wav",
631
+ contentType: "audio/wav",
632
+ billingTag: null,
633
+ });
634
+ console.log(result.text);
635
+ ```
636
+
637
+ Supported recordings are mp3, mp4, mpeg, mpga, m4a, wav and webm. The request must
638
+ contain nonempty bytes no larger than 25 MiB. Transcription availability, codec
639
+ validity, budget and Session eligibility remain service decisions. Omit
640
+ `billingTag` to inherit the Session default, supply one key/value pair to override,
641
+ or use `null` to clear it for this Turn. The SDK encodes that optional field as
642
+ JSON without changing the file's bytes.
643
+
644
+ `audio()` returns an `AudioTurnResult` only after the same Turn emits its final
645
+ Message and successful terminal event. It throws `TurnFailedError` or
646
+ `TurnInterruptedError` for those terminal outcomes. `streamAudio()` exposes the
647
+ actual `AudioTurnEvent` sequence: `event`, `sessionId`, `turnId`, `seq`,
648
+ `createdAt`, optional `agentRunId`, and event-specific `payload`. An
649
+ `assistant_draft` contains a complete replacement snapshot, including an empty
650
+ reset; it is separate from `progress` and the final `assistant_message`.
651
+
652
+ **The audio request owns execution's response.** Breaking its event loop, aborting
653
+ its signal or losing the connection may request interruption. This differs from
654
+ closing a text observation subscription. EOF alone is never reported as success.
655
+ `AudioTurnStoppedError` preserves the Session and any already observed Turn ID;
656
+ inspect `getTurn(turnId)` or saved history before deciding what to submit next.
657
+ Audio accepts no `clientEventId`, has no ordinary text interaction receipt and
658
+ provides no idempotent resubmission or event-position recovery promise. The SDK
659
+ does not resubmit a recording automatically. A transcription refusal may still
660
+ have consumed provider work; it does not imply a refunded charge.
661
+
662
+ The installed `submit-audio.mjs` example reads an existing file and prints the
663
+ final answer. Set `AGENT_SERVICE_URL`, `USER_API_KEY`, `SESSION_ID` and
664
+ `AUDIO_FILE_PATH`; `AUDIO_CONTENT_TYPE` is optional:
665
+
666
+ ```sh
667
+ node node_modules/@tidblink/server/examples/submit-audio.mjs
668
+ ```
669
+
670
+ The exported `submitAudioWorkflow()` accepts bytes and an optional abort signal.
671
+ This SDK does not capture, play, encode or bridge audio.
672
+
673
+ ## File attachments
674
+
675
+ `user.files` manages owned uploaded files. Upload from a `Uint8Array` (including
676
+ Node.js `Buffer`) and retain a stable creation key with the exact bytes and name.
677
+ The service chooses inline or multipart transfer; the SDK supplies complete and
678
+ per-part SHA-256 digests, transfers the required bytes, and confirms whole-file
679
+ verification before returning a ready file. Empty files are supported.
680
+
681
+ ```ts
682
+ import { readFile } from "node:fs/promises";
683
+
684
+ const file = await customer.files.upload({
685
+ originalName: "notes.txt",
686
+ bytes: await readFile("notes.txt"),
687
+ contentType: "text/plain",
688
+ idempotencyKey: savedUploadKey,
689
+ });
690
+ const result = await session.turn({
691
+ clientEventId: savedMessageId,
692
+ text: "Summarize the attached notes.",
693
+ userFileIds: [file.fileId],
694
+ });
695
+ const downloaded = await customer.files.download(file.fileId);
696
+ ```
697
+
698
+ Only ready files authorized for the Session's User may be attached. The service
699
+ checks that authority at message admission; an earlier local readiness check
700
+ cannot grant access to another User's file. File bytes remain separate from the
701
+ message text. `capabilities()` exposes current upload limits; a typed 413 requires
702
+ reading them again and explicitly deciding whether to create another upload.
703
+
704
+ For explicit recovery use `createUpload()` with the original metadata and key,
705
+ `getUpload(uploadId)`, `uploadContent()`, `presignPart()` / `uploadPart()`,
706
+ `completeUpload()`, and `cancelUpload()`. Retain each returned part number, ETag
707
+ and digest until completion. A status read never creates or transfers anything.
708
+ Create replay retains the original file and fixed upload expiry; it may renew a
709
+ short-lived inline transfer URL. An expired or failed upload requires a new key
710
+ and file. The SDK never chooses that new identity or repeats a failed transfer
711
+ automatically.
712
+
713
+ `FileUploadOutcomeUnknownError` carries `idempotencyKey` and, once creation was
714
+ acknowledged, `uploadId`. Read that upload before deciding what to repeat. A lost
715
+ creation response can be reconciled with `createUpload()` using the exact
716
+ original metadata and key. A pending multipart part can be signed again for its
717
+ original digest; after an uncertain completion, read status rather than assuming
718
+ failure or sending the message. Cancellation marks the incomplete file unusable;
719
+ it does not delete a previously accepted conversation.
720
+
721
+ `list({ history, limit, cursor })` returns `{ files, nextCursor }`; it has no
722
+ `hasMore` field. `get(fileId)` reads metadata. `getContent(fileId)` returns either
723
+ `{ type: "bytes", bytes, contentType }` or, for a large file,
724
+ `{ type: "download", grant }`. `downloadFromGrant(grant)` redeems the latter;
725
+ `download(fileId)` handles both forms and verifies complete size and SHA-256.
726
+ A new authorized content read obtains a fresh grant after expiry. Upload and
727
+ download grants are bearer credentials: do not log or store their URLs beyond
728
+ need. Transfers send only the grant's required headers, never an API Key.
729
+ Uploads reject redirects; a download may follow one HTTPS redirect without an
730
+ API Key. Files are held in memory while hashing or downloading, so size your
731
+ backend's concurrency for the service's advertised maximum.
732
+
733
+ The packaged `upload-file.mjs` example uploads and attaches one file. Set
734
+ `AGENT_SERVICE_URL`, `USER_API_KEY`, `SESSION_ID`, `FILE_PATH`,
735
+ `UPLOAD_IDEMPOTENCY_KEY`, and `CLIENT_EVENT_ID`, then run:
736
+
737
+ ```sh
738
+ node node_modules/@tidblink/server/examples/upload-file.mjs
739
+ ```
740
+
741
+ The example returns the final conversation result; its exported
742
+ `uploadFileWorkflow()` accepts bytes directly. It does not manage general Drive
743
+ directories or write Session text files. Organization owner-scoped clients can
744
+ upload, read and attach the same ordinary Files.
745
+
746
+ ## Scheduled work
747
+
748
+ `user.schedulers` uses the full customer Scheduler API. A new-Session schedule
749
+ requires an owned Agent; a bound-Session schedule supplies only its Session.
750
+ The service resolves the Agent from that Session.
751
+
752
+ ```ts
753
+ const created = await customer.schedulers.create({
754
+ agentId: agent.id,
755
+ title: "Morning report",
756
+ prompt: "Summarize the latest activity.",
757
+ schedule: { kind: "cron", cronExpr: "0 9 * * *", timezone: "Asia/Shanghai" },
758
+ idempotencyKey: savedSchedulerKey,
759
+ });
760
+ const current = await customer.schedulers.get(created.scheduler.schedulerId);
761
+ const updated = await customer.schedulers.update(
762
+ current.scheduler.schedulerId,
763
+ { enabled: false },
764
+ { ifMatch: current.etag },
765
+ );
766
+ const history = await customer.schedulers.fires(current.scheduler.schedulerId, {
767
+ limit: 20,
768
+ });
769
+ ```
770
+
771
+ For one-time work use exactly one of `{ kind: "once", runAt: isoTimestamp }` or
772
+ `{ kind: "once", after: "1h30m" }`. `runAt` includes a timezone offset; cron
773
+ requires an IANA timezone. The service owns timing and validation. Optional
774
+ `validFrom` and `validUntil` restrict the execution window. A bound-Session create
775
+ uses `delivery: { target: "bound_session", sessionId }` and omits `agentId`.
776
+
777
+ Creation returns `{ scheduler, etag }`. Persist the key and exact input before
778
+ calling. A same-key replay returns the **original creation response and ETag**,
779
+ even if the Scheduler has since changed; use `get()` for current state before an
780
+ edit. A changed input with that key is refused. Uncertain responses raise
781
+ `OutcomeUnknownError` rather than repeating the command automatically.
782
+ `idempotency_in_progress` retains the service's raw `retryAfter` header on the
783
+ error so the application can decide when to replay the original request.
784
+
785
+ `update(id, changes, { ifMatch })` and `delete(id, { ifMatch })` require a
786
+ previously read ETag. Stale writes retain the service's 412 conflict; the SDK
787
+ never refreshes and overwrites another writer's changes. Updates support title,
788
+ prompt, Agent/delivery target, schedule, enabled state, validity window and
789
+ `snoozeUntil`. Enabling an elapsed one-time schedule requires a future time.
790
+ Disabling or deleting a Scheduler does not roll back completed effects. Deleted
791
+ Schedulers are no longer available through detail or fire-history reads.
792
+ `organization.user(userId).schedulers.list({ status: "deleted" })` returns their
793
+ retained tombstones. User-Key lists accept only `enabled` or `disabled`; both
794
+ list surfaces omit deleted records by default.
795
+
796
+ `list({ status, limit, cursor })` and `fires(id, { limit, cursor })` return
797
+ `nextCursor` rather than `hasMore`. Cursors retain their original owner/filter
798
+ scope. Each fire preserves its execution prompt and delivery snapshot; later
799
+ Scheduler edits cannot rewrite it. `pending` is not success. A `completed` fire
800
+ or a `failed` fire retains nullable result identities and a nullable failure code.
801
+ When `result.sessionId` and `result.turnId` are present, read the saved Turn with
802
+ `customer.session(sessionId).getTurn(turnId)`; do not invent a message identity
803
+ when `messageId` is null.
804
+
805
+ The installed `schedule-task.mjs` example creates one scheduled task, waits
806
+ locally for a settled fire and reads its associated Turn. Set `AGENT_SERVICE_URL`,
807
+ `USER_API_KEY`, `AGENT_ID`, `SCHEDULER_IDEMPOTENCY_KEY`, `SCHEDULE_RUN_AT` and
808
+ `SCHEDULE_PROMPT`, then run:
809
+
810
+ ```sh
811
+ node node_modules/@tidblink/server/examples/schedule-task.mjs
812
+ ```
813
+
814
+ A local example timeout leaves the schedule active and prints its identity for
815
+ later fire-history reads. The separate Scheduler plans API is outside this SDK.
816
+
817
+ ## Errors and uncertain outcomes
818
+
819
+ Organization, text/progress/history and refreshed Agent/Session lifecycle methods accept
820
+ `{ requestId?, signal? }` options; creation and list options include these fields.
821
+ `TiDBLinkApiError` preserves status, service code, request identity and details;
822
+ `ConflictError` preserves explicit 409/412 conflicts. The service's response
823
+ request identity takes precedence over the one sent.
824
+
825
+ Safe reads and keyed Agent/Session creation use bounded retries with the same
826
+ request identity and creation key. User creation, key issuance, preference and
827
+ Session updates, unlinking, deletion and Realtime mutations are never
828
+ automatically replayed. Disconnects, unusable success responses, HTTP 408 and
829
+ server failures during these mutations raise `OutcomeUnknownError`. For an HTTP failure, its
830
+ `cause` retains the `TiDBLinkApiError`. Repeating revoke is safe, but an ambiguous
831
+ failure still reports an unknown outcome. `AbortSignal` stops the local request;
832
+ after dispatch it does not establish whether a mutation happened. Redirects are
833
+ refused.
834
+
835
+ After uncertain User creation, list and match the original `externalId`.
836
+ After uncertain issuance or rotation, listing keys can confirm metadata but
837
+ **cannot recover the one-time token**. Reconcile metadata and explicitly revoke
838
+ any unwanted key before deciding whether to issue again. The provisioning
839
+ example never repeats an uncertain credential operation.
840
+
841
+ Credential receipts must match the requested User (creation) or API Key
842
+ (rotation/revocation), with the expected status. Issued tokens must also match
843
+ their key metadata. A mismatched or unusable success receipt raises
844
+ `OutcomeUnknownError` with the request identity; it never triggers an automatic
845
+ credential retry.
846
+
847
+ After uncertain Agent/Session creation, repeat the original input with the same
848
+ creation key. After an uncertain preference, name, tag or model update, read the
849
+ resource before deciding on another change. Unlinking and deletion can hide a
850
+ resource from ordinary reads, so a missing resource does not recover its original
851
+ mutation receipt.
852
+
853
+ After an uncertain Realtime Model update, read the selection again. After an
854
+ uncertain connection open or end, inspect the newest connection. An uncertain
855
+ connect-token response cannot recover that plaintext token; deliberately mint a
856
+ new token after reconciling the connection state.
857
+
858
+ ## User usage
859
+
860
+ A User client reads its own usage. An Organization client must name a target User
861
+ in that Organization. These are distinct response projections and date contracts:
862
+
863
+ | Read | User client | Organization client |
864
+ | --- | --- | --- |
865
+ | Summary | `getUsage(options)` | `getUserUsage(userId, options)` |
866
+ | Details | `getUsageDetails(options)` | `getUserUsageDetails(userId, options)` |
867
+ | Range | Optional inclusive UTC dates, `YYYY-MM-DD`; at most 370 days | Required exact UTC timestamps with milliseconds, inclusive `from` and exclusive `to`; at most 31 days |
868
+ | Buckets | `period: "day"` or `"week"` chooses the default range; series remains daily | `interval: "hour"`, `"day"`, `"week"` or `"month"` |
869
+ | Detail limits | Separate `llm`, `sandbox` and `webSearch` arrays; `limit` applies per kind, with no cursor | Globally ordered `items`; optional `kind`, `limit` and opaque `cursor`; `nextCursor: null` ends the stream |
870
+
871
+ Both support an exact `tagKey`/`tagValue` pair. Send both or neither. Values retain
872
+ case sensitivity and the service's normalization rules. Preserve the original
873
+ range, kind and tag filters when following an Organization cursor. Details accept
874
+ limits from 1 to 500. The User detail endpoint cannot retrieve older rows beyond
875
+ its limit through a fabricated continuation; Organization details support paging.
876
+
877
+ ```ts
878
+ const own = await user.getUsage({
879
+ from: "2030-01-02", to: "2030-01-02", period: "day",
880
+ });
881
+ const page = await organization.getUserUsageDetails(targetUserId, {
882
+ from: "2030-01-02T00:00:00.000Z",
883
+ to: "2030-01-03T00:00:00.000Z",
884
+ limit: 100,
885
+ });
886
+ ```
887
+
888
+ The SDK returns the service's values unchanged. `costMicros: null`,
889
+ `pricingStatus: "unknown"`, `valuationStatus: "unpriced"`, and incomplete fact or
890
+ valuation counts retain different meanings. A stored numeric cost does not make
891
+ an unpriced fact billable. Priced totals exclude unpriced facts. Scalar cost totals describe platform-borne
892
+ costs; `costByBearer` preserves the separate platform, customer and unknown
893
+ categories. Public-list estimates are not supplier invoices or known final charges. Fact completeness and
894
+ valuation completeness are independent. Do not display missing costs as zero or
895
+ infer a balance from these reads. Organization projections intentionally omit
896
+ some details available in the User's own projection.
897
+
898
+ The installed example `@tidblink/server/examples/read-usage.mjs` exports
899
+ `readOwnUsage` and `readUserUsage`, each returning a summary and the first detail
900
+ page. The CLI requires `AGENT_SERVICE_URL` and `USAGE_AUTHORITY=user` with
901
+ `USER_API_KEY`, or `USAGE_AUTHORITY=organization` with `ORGANIZATION_API_KEY` and
902
+ `END_USER_ID`. Supply `USAGE_FROM`/`USAGE_TO` in the corresponding date format;
903
+ `TAG_KEY`/`TAG_VALUE` are optional. It performs reads only and does not claim that
904
+ a first detail page is complete history.
905
+
906
+ ## Compatibility and verification
907
+
908
+ `TIDB_LINK_REVISION` / `tidbLinkRevision` identify the selected exact service
909
+ revision for every supported capability. `tidbLinkOpenApiSha256` pins that
910
+ service's `docs/openapi.yaml`. The former per-feature revision constants and
911
+ package fields have been removed; use these shared values instead.
912
+ `TIDB_LINK_API_VERSION` remains `0.0.1`; the version string alone does not establish
913
+ compatibility. This is a selective surface, not coverage of every service API.
914
+
915
+ `npm run test:service-contract` checks frozen selected SDK OpenAPI subsets from
916
+ that exact revision. The committed snapshot and metadata retain the source
917
+ fingerprint and immutable contract evidence; no live synchronization is required.
918
+ The snapshot alone does not certify service behavior.
919
+
920
+ `npm test` covers the simulated HTTP failure boundary and installs the actual
921
+ packed artifact in a clean ESM TypeScript consumer. To verify the packed SDK and
922
+ its examples against the real service, use the service's Node.js 26 runtime, install its
923
+ pinned dependencies, generate Prisma, build its Hardware contract and prepare its
924
+ current schema in a **dedicated test database**. See the selected service's
925
+ [current-schema setup](https://github.com/mem9-ai/agent-stack/blob/882287809f91d9a3fdf60537460431d9f0fc23e7/docs/prisma-first-tidb-validation-and-rollout.md).
926
+
927
+ ```sh
928
+ AGENT_SERVICE_ROOT=/path/to/agent-stack \
929
+ TEST_TIDB_DATABASE_URL=mysql://root@127.0.0.1:4000/tidb_link_sdk_test \
930
+ TIDB_SSL=off npm run test:service
931
+ ```
932
+
933
+ This command requires a clean service checkout at the exact revision. It builds
934
+ the service for the compiled Organization consumer and uses a dedicated TiDB
935
+ connection. Run just that consumer with:
936
+
937
+ ```sh
938
+ AGENT_SERVICE_ROOT=/path/to/agent-stack \
939
+ TEST_TIDB_DATABASE_URL=mysql://root@127.0.0.1:4000/tidb_link_sdk_test \
940
+ TIDB_SSL=off npm run test:service -- organization/organization.compiled.fixture.ts
941
+ ```
942
+
943
+ The compiled fixture reuses the service system harness, its isolated database,
944
+ controlled model/storage/ASR/voice dependencies and real background runner. A
945
+ separate process installs the tarball in a clean directory and calls only the
946
+ installed SDK and packaged Organization example for its application workflow.
947
+ It verifies Organization provisioning without a User Key, discovery, Agent/Session
948
+ creation replay, owner isolation, file upload/download and message attachment,
949
+ text results, SSE/history, Client Tools, audio, Scheduler fires and deleted history,
950
+ Realtime HTTP grants, rotation/revocation and fresh-authority recovery. A separate
951
+ User-Key control reads the same retained resources. Management credential changes
952
+ and controlled suppliers are fixture actions, not claimed SDK APIs. WebSocket
953
+ redemption, live speech suppliers, physical hardware and deployment compatibility
954
+ are outside this SDK proof.
955
+
956
+ The remaining fixtures use the service's existing AppModule test seam and reset
957
+ all rows in the selected dedicated test database before and after their runs.
958
+ They use genuine credentials, real TiDB and controlled dependencies. Their scope:
959
+ Fixtures prepare identities, Templates and eligible
960
+ Realtime Sessions. A composed installed quickstart provisions one fresh User/key,
961
+ discovers Templates/models, creates an Agent and Session, obtains a final answer
962
+ and recovers the same original result. Public SDK methods also verify ownership,
963
+ lifecycle updates and Realtime operations. Text cases
964
+ exercise actual acceptance, final success/failure/interruption, local timeout and
965
+ abandoned-wait recovery, response-loss reconciliation, and paginated mixed
966
+ history through public SDK methods. Progress cases verify draft replacement, cursor
967
+ reconnection/refusal, original-result confirmation, local observation timeout and
968
+ live credential revocation without remote cancellation. Client Tool cases declare and replace tools, resume pending reads, explicitly return
969
+ application output or errors, and observe those values in real Runtime answers, with
970
+ ownership, stale revision, settled-call and unavailable-call refusals. A forwarding proxy drops only
971
+ an actual service response to prove uncertain-submission recovery. Control cases
972
+ verify active-Turn steering, eventual cancellation, disabled retry and an explicitly
973
+ enabled checkpoint retry with an unchanged original outcome. The enabled fixture
974
+ uses the existing controlled Drive9 and model suppliers. File cases use a separate
975
+ fixture lifecycle with real upload state and a controlled Drive9/S3 byte supplier.
976
+ They verify inline, multipart and empty uploads, cancellation and invalid content,
977
+ large-file download-ticket redemption, and a real Runtime file read whose answer
978
+ contains the uploaded content. The installed upload example exercises the same
979
+ public workflow. Real supplier deployment certification is outside this evidence.
980
+ Scheduler cases enable the real background runner and use SDK-created schedules
981
+ to verify successful and failed execution, delivery changes, retained fire
982
+ snapshots and the installed example.
983
+ Audio cases use the real transcription adapter with a controlled HTTP supplier and
984
+ real spend limits. They cover recorded input, malformed/unsupported recordings,
985
+ supplier and budget refusals, model failure, active-response interruption and the
986
+ installed audio example.
987
+ The public Session projection exposes the frozen Template digest but hides stored
988
+ Agent preferences. Preference snapshots and active-Turn model freezing also rely
989
+ on the selected service's [existing persistence evidence](https://github.com/mem9-ai/agent-stack/blob/882287809f91d9a3fdf60537460431d9f0fc23e7/test/product/sessions/sessions-withorgstore.tidb.spec.ts); SDK reads alone do not
990
+ prove those hidden values.
991
+ Usage cases seed controlled facts and verify both public projections, exact filters,
992
+ paging and authority without executing messages to manufacture billing data.
993
+ No production supplier credentials or WebSocket redemption are needed. A
994
+ mismatch or unavailable service fails this check instead of silently skipping it.