@tidblink/server 0.3.0-rc.0 → 0.4.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.
- package/README.md +557 -368
- package/dist/audio.d.ts +9 -0
- package/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +6 -1
- package/dist/audio.js.map +1 -1
- package/dist/backend-connection.d.ts +89 -0
- package/dist/backend-connection.d.ts.map +1 -0
- package/dist/backend-connection.js +300 -0
- package/dist/backend-connection.js.map +1 -0
- package/dist/backend-pool.d.ts +93 -0
- package/dist/backend-pool.d.ts.map +1 -0
- package/dist/backend-pool.js +419 -0
- package/dist/backend-pool.js.map +1 -0
- package/dist/client-tool-executor.d.ts +31 -0
- package/dist/client-tool-executor.d.ts.map +1 -0
- package/dist/client-tool-executor.js +190 -0
- package/dist/client-tool-executor.js.map +1 -0
- package/dist/client-tools.d.ts +0 -25
- package/dist/client-tools.d.ts.map +1 -1
- package/dist/client-tools.js +1 -67
- package/dist/client-tools.js.map +1 -1
- package/dist/connector-callback.d.ts +92 -0
- package/dist/connector-callback.d.ts.map +1 -0
- package/dist/connector-callback.js +155 -0
- package/dist/connector-callback.js.map +1 -0
- package/dist/connectors.d.ts +66 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +71 -0
- package/dist/connectors.js.map +1 -0
- package/dist/control.js +1 -1
- package/dist/control.js.map +1 -1
- package/dist/errors.d.ts +34 -11
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +36 -8
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +158 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +110 -0
- package/dist/events.js.map +1 -0
- package/dist/files.js +9 -9
- package/dist/files.js.map +1 -1
- package/dist/history.d.ts +38 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +1 -1
- package/dist/history.js.map +1 -1
- package/dist/http.d.ts +28 -4
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +44 -24
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +26 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/organization.d.ts +45 -28
- package/dist/organization.d.ts.map +1 -1
- package/dist/organization.js +67 -73
- package/dist/organization.js.map +1 -1
- package/dist/realtime.d.ts +0 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +0 -1
- package/dist/realtime.js.map +1 -1
- package/dist/session-connection.d.ts +144 -0
- package/dist/session-connection.d.ts.map +1 -0
- package/dist/session-connection.js +334 -0
- package/dist/session-connection.js.map +1 -0
- package/dist/session-send.d.ts +49 -0
- package/dist/session-send.d.ts.map +1 -0
- package/dist/session-send.js +179 -0
- package/dist/session-send.js.map +1 -0
- package/dist/session.d.ts +37 -8
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +56 -29
- package/dist/session.js.map +1 -1
- package/dist/skills.d.ts +103 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +98 -0
- package/dist/skills.js.map +1 -0
- package/dist/sse.d.ts +5 -2
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +7 -2
- package/dist/sse.js.map +1 -1
- package/dist/template-switch.d.ts +51 -0
- package/dist/template-switch.d.ts.map +1 -0
- package/dist/template-switch.js +81 -0
- package/dist/template-switch.js.map +1 -0
- package/dist/templates.d.ts +129 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +92 -0
- package/dist/templates.js.map +1 -0
- package/dist/turn.d.ts +4 -0
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +17 -13
- package/dist/turn.js.map +1 -1
- package/dist/usage.d.ts +13 -197
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +5 -3
- package/dist/usage.js.map +1 -1
- package/dist/user.d.ts +10 -51
- package/dist/user.d.ts.map +1 -1
- package/dist/user.js +18 -105
- package/dist/user.js.map +1 -1
- package/examples/control-turn.mjs +9 -4
- package/examples/create-session.mjs +37 -24
- package/examples/http-connector.mjs +127 -0
- package/examples/lamp-backend.mjs +175 -0
- package/examples/prepare-realtime-connection.mjs +18 -7
- package/examples/read-usage.mjs +7 -25
- package/examples/send-message.mjs +9 -4
- package/examples/submit-audio.mjs +9 -4
- package/examples/upload-file.mjs +9 -4
- package/package.json +7 -13
- package/dist/progress.d.ts +0 -34
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js +0 -154
- package/dist/progress.js.map +0 -1
- package/dist/schedulers.d.ts +0 -129
- package/dist/schedulers.d.ts.map +0 -1
- package/dist/schedulers.js +0 -91
- package/dist/schedulers.js.map +0 -1
- package/examples/answer-client-tool.mjs +0 -69
- package/examples/observe-message.mjs +0 -44
- package/examples/organization-session.mjs +0 -66
- package/examples/provision-user.mjs +0 -83
- package/examples/schedule-task.mjs +0 -67
package/README.md
CHANGED
|
@@ -6,18 +6,20 @@ stay on your backend; do not ship it to browsers or devices.
|
|
|
6
6
|
|
|
7
7
|
## Supported scope
|
|
8
8
|
|
|
9
|
-
This package supports Organization information, End User provisioning,
|
|
10
|
-
|
|
9
|
+
This package supports Organization information, End User provisioning,
|
|
10
|
+
Template, Skill and Connector management, Connector callback verification,
|
|
11
|
+
Template/model discovery, Session management and Template switches,
|
|
11
12
|
text submission, steering, cancellation, checkpoint retry, original-result waiting,
|
|
12
|
-
continuous progress, Client Tools,
|
|
13
|
+
continuous progress, Client Tools, Session connections over Backend Connections,
|
|
14
|
+
conversation history, file attachments,
|
|
13
15
|
Realtime Model/Connection preparation, prerecorded audio and User usage reads for [service revision
|
|
14
|
-
|
|
16
|
+
4305ef67](https://github.com/mem9-ai/agent-stack/commit/4305ef67daebf23c416badf806217a7c11dcef37).
|
|
15
17
|
|
|
16
18
|
Text commands return durable receipts independently of execution. Waiting reads the
|
|
17
|
-
original command's outcome.
|
|
18
|
-
|
|
19
|
-
outside this package.
|
|
20
|
-
|
|
19
|
+
original command's outcome. The service has no Agent: a Session is created
|
|
20
|
+
directly from an explicit Template, and there is no default Template. Browser, MSP, Console and device APIs are
|
|
21
|
+
outside this package. Every resource workflow uses an Organization API Key with
|
|
22
|
+
an explicitly selected End User; User API Keys are not supported.
|
|
21
23
|
|
|
22
24
|
## Quick start
|
|
23
25
|
|
|
@@ -29,29 +31,30 @@ npm install --save-exact @tidblink/server@next
|
|
|
29
31
|
|
|
30
32
|
An application backend uses its Organization API Key (`ti_org_…`) and explicitly
|
|
31
33
|
selects the ordinary End User with `organization.user(userId)`. The Key remains
|
|
32
|
-
the caller; the User owns the resources.
|
|
34
|
+
the caller; the User owns the resources.
|
|
33
35
|
|
|
34
36
|
```js
|
|
35
37
|
import { randomUUID } from "node:crypto";
|
|
36
38
|
import { OrganizationClient } from "@tidblink/server";
|
|
37
|
-
import {
|
|
39
|
+
import { createSessionWorkflow } from "@tidblink/server/examples/create-session.mjs";
|
|
38
40
|
|
|
39
41
|
const baseUrl = process.env.AGENT_SERVICE_URL;
|
|
40
42
|
const organizationApiKey = process.env.ORGANIZATION_API_KEY;
|
|
41
43
|
const organization = new OrganizationClient({ baseUrl, apiKey: organizationApiKey });
|
|
42
44
|
const endUser = await organization.createEndUser({ externalId: "your-application-user-id" });
|
|
43
|
-
//
|
|
45
|
+
// Safe to repeat: the same externalId returns the same End User.
|
|
44
46
|
const user = organization.user(endUser.userId);
|
|
45
47
|
const page = await user.listAgentTemplates();
|
|
46
48
|
const template = page.templates[0];
|
|
47
49
|
if (!template) throw new Error("Select an available Template; follow nextCursor if hasMore");
|
|
48
50
|
const creation = {
|
|
49
51
|
baseUrl, organizationApiKey, userId: endUser.userId,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
templateId: template.agentTemplateId,
|
|
53
|
+
sessionCreationKey: randomUUID(),
|
|
52
54
|
};
|
|
53
|
-
// Persist the selected Template, optional model and
|
|
54
|
-
|
|
55
|
+
// Persist the selected Template, optional model and the creation identity.
|
|
56
|
+
// Add acknowledgeSessionRecall: true when the Template enables cross-Session Recall.
|
|
57
|
+
const { session } = await createSessionWorkflow(creation);
|
|
55
58
|
const message = { clientEventId: randomUUID(), text: "Help me plan a trip" };
|
|
56
59
|
// Retain the Session, command identity and original input before dispatch.
|
|
57
60
|
const receipt = await session.submitMessage(message);
|
|
@@ -60,149 +63,239 @@ console.log(result.text);
|
|
|
60
63
|
const recovered = await session.waitForResult(message.clientEventId);
|
|
61
64
|
```
|
|
62
65
|
|
|
63
|
-
`externalId` is unique within an Organization
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
+
`externalId` is unique within an Organization, and creation with it is
|
|
67
|
+
idempotent: a repeated `createEndUser` returns the existing End User unchanged and
|
|
68
|
+
does not apply the other fields, and concurrent calls create one End User. The SDK
|
|
69
|
+
retries such calls automatically. An `externalId` that equals an existing one only
|
|
70
|
+
when case and accents are ignored is refused with `resource_conflict`. Without an
|
|
71
|
+
`externalId`, every call creates a new End User. A Template that enables
|
|
72
|
+
cross-Session Recall refuses Session creation with
|
|
73
|
+
`agent_template_recall_acknowledgment_required` (HTTP 409) unless the input
|
|
74
|
+
includes `acknowledgeSessionRecall: true`. Session creation replays require the same creation key and
|
|
66
75
|
inputs. Reading the original message receipt or waiting again does not execute
|
|
67
76
|
another Turn. An HTTP request ID is diagnostic, not a creation identity.
|
|
68
77
|
|
|
69
|
-
The installed
|
|
70
|
-
`ORGANIZATION_API_KEY`, `END_USER_ID`, `TEMPLATE_ID
|
|
71
|
-
`SESSION_CREATION_KEY` (`MODEL_ID`
|
|
78
|
+
The installed example can run with `AGENT_SERVICE_URL`,
|
|
79
|
+
`ORGANIZATION_API_KEY`, `END_USER_ID`, `TEMPLATE_ID` and
|
|
80
|
+
`SESSION_CREATION_KEY` (`MODEL_ID` and `ACKNOWLEDGE_SESSION_RECALL=true` are optional):
|
|
72
81
|
|
|
73
82
|
```sh
|
|
74
|
-
node node_modules/@tidblink/server/examples/
|
|
83
|
+
node node_modules/@tidblink/server/examples/create-session.mjs
|
|
75
84
|
```
|
|
76
85
|
|
|
77
|
-
`organization.user(userId)` returns an `EndUserClient
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`getUserUsageDetails
|
|
81
|
-
Private MCP registration, general Drive entry management,
|
|
82
|
-
|
|
86
|
+
`organization.user(userId)` returns an `EndUserClient` with that User's
|
|
87
|
+
Session, file and catalog methods. Selecting a User sends no request.
|
|
88
|
+
Usage reads stay on `organization.getUserUsage(userId, options)` and
|
|
89
|
+
`getUserUsageDetails`.
|
|
90
|
+
Private MCP registration, general Drive entry management, Template switching,
|
|
91
|
+
Schedulers and native Hardware APIs are not wrapped by this SDK. Use the
|
|
83
92
|
pinned public HTTP contract for those existing service capabilities. Building ToD,
|
|
84
93
|
a device gateway or management integration remains outside this package.
|
|
85
94
|
Artifact APIs remain gated. The SDK prepares Realtime HTTP grants; applications
|
|
86
95
|
own WebSocket/media transport. This change adds neither a generated full API
|
|
87
96
|
client nor a media client.
|
|
88
97
|
|
|
89
|
-
###
|
|
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.
|
|
98
|
+
### Credential rotation
|
|
103
99
|
|
|
104
100
|
After Organization Key rotation or revocation, create a new `OrganizationClient`
|
|
105
101
|
with fresh valid authority and select the same User. Existing handles retain their
|
|
106
|
-
old credential. New old-authority requests, subscriptions,
|
|
102
|
+
old credential. New old-authority requests, subscriptions,
|
|
107
103
|
Realtime tokens and Organization-issued file tickets lose authority. Accepted
|
|
108
104
|
Turns continue, and admitted file transfers may finish. Reconnect/obtain new
|
|
109
105
|
grants with fresh authority and reconcile the original command before submitting
|
|
110
|
-
new work.
|
|
111
|
-
fires. Another valid Key remains usable. Disabled Users cannot start new work;
|
|
106
|
+
new work. Another valid Key remains usable. Disabled Users cannot start new work;
|
|
112
107
|
Organization authority can still read retained history/files and perform supported
|
|
113
|
-
cleanup.
|
|
114
|
-
contract. Application policies and K3 spending policy are separate concerns.
|
|
108
|
+
cleanup. Application policies and K3 spending policy are separate concerns.
|
|
115
109
|
|
|
116
|
-
|
|
110
|
+
### Custom transport
|
|
117
111
|
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
Pass `fetch` to route every request through your own transport, for example a
|
|
113
|
+
proxy, a custom HTTP agent, instrumentation or a test double. It carries all
|
|
114
|
+
requests the client makes: JSON calls, event streams, and file transfers to
|
|
115
|
+
presigned storage URLs (which never receive the API Key).
|
|
120
116
|
|
|
121
|
-
```
|
|
122
|
-
import { OrganizationClient } from "@tidblink/server";
|
|
117
|
+
```js
|
|
123
118
|
const organization = new OrganizationClient({
|
|
124
|
-
baseUrl,
|
|
119
|
+
baseUrl,
|
|
120
|
+
apiKey: organizationApiKey,
|
|
121
|
+
fetch: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Managing Templates and Skills
|
|
126
|
+
|
|
127
|
+
A Template defines what a Session runs: its identity, instructions, Sandbox,
|
|
128
|
+
voice language, Skills and Connectors. `organization.templates` manages the Organization's Templates, and
|
|
129
|
+
`organization.skills` uploads and publishes the Skills they reference. Both act
|
|
130
|
+
on the Organization; they need no End User.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const catalog = await organization.templates.catalog();
|
|
134
|
+
const created = await organization.templates.create({
|
|
135
|
+
displayName: "Support assistant",
|
|
136
|
+
content: {
|
|
137
|
+
identity: { agentName: "Ava" },
|
|
138
|
+
instructions: "Answer support questions briefly.",
|
|
139
|
+
sandbox: { profileId: catalog.sandboxes[0].id },
|
|
140
|
+
realtime: { language: "en-US" },
|
|
141
|
+
},
|
|
142
|
+
idempotencyKey: templateCreationKey,
|
|
125
143
|
});
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
// Replace and archive take the ETag you read; a stale one is a ConflictError.
|
|
145
|
+
await organization.templates.replace(created.template.id, { displayName, content }, {
|
|
146
|
+
ifMatch: created.template.etag,
|
|
147
|
+
idempotencyKey: templateChangeKey,
|
|
128
148
|
});
|
|
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
149
|
```
|
|
134
150
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
`templates.catalog()` lists the models, Sandbox profiles and published Skills a
|
|
152
|
+
Template may reference. `templates.list()` filters by `lifecycleStatus` and `q`
|
|
153
|
+
and pages with `cursor`. Replacing a Template does not change Sessions already
|
|
154
|
+
created from it; they keep the content snapshot identified by
|
|
155
|
+
`session.data.template.contentDigest`. An archived Template can no longer start
|
|
156
|
+
Sessions. Creation, replacement and archive are retried automatically with their
|
|
157
|
+
idempotency key; after an `OutcomeUnknownError`, repeat the call with the same key
|
|
158
|
+
and input.
|
|
139
159
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
An Organization key likewise grants no User identity.
|
|
160
|
+
A Skill is a ZIP package with a `SKILL.md` at its root. Uploading validates it
|
|
161
|
+
asynchronously; publishing makes it the Skill's next version:
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
```js
|
|
164
|
+
const upload = await organization.skills.upload({ bytes: zipBytes, idempotencyKey });
|
|
165
|
+
const validated = await organization.skills.waitForUpload(upload.uploadId);
|
|
166
|
+
if (validated.state !== "awaiting_confirmation")
|
|
167
|
+
throw new Error(validated.validation.failureCode ?? validated.state);
|
|
168
|
+
await organization.skills.publish(upload.uploadId, { idempotencyKey: publishKey });
|
|
169
|
+
const published = await organization.skills.waitForUpload(upload.uploadId, { until: ["done", "failed"] });
|
|
170
|
+
// Reference it from a Template: content.skills = [{ skillId, version }]
|
|
171
|
+
const reference = { skillId: published.skillId, version: published.version };
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Each upload creates a new Skill, and an upload is visible only to the Organization
|
|
175
|
+
API Key that created it. Publish before `expiresAt`, or cancel an unpublished
|
|
176
|
+
upload with `cancelUpload`. `skills.list()` and `skills.get(skillId)` read
|
|
177
|
+
published Skills and their versions.
|
|
178
|
+
|
|
179
|
+
## Connectors
|
|
145
180
|
|
|
146
|
-
|
|
181
|
+
A Connector is an Organization-owned MCP tool server that Sessions call. The
|
|
182
|
+
platform calls it for the End User who owns the Session and names that User in
|
|
183
|
+
every call, so your server can act on the right account without trusting model
|
|
184
|
+
arguments.
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
let connector = await organization.connectors.register({
|
|
188
|
+
displayName: "Orders",
|
|
189
|
+
endpointUrl: "https://orders.example.com/mcp",
|
|
190
|
+
});
|
|
191
|
+
// Activation connects to the endpoint and discovers its tools.
|
|
192
|
+
connector = await organization.connectors.activate(connector.connectorId, {
|
|
193
|
+
expectedConnectorVersion: connector.connectorVersion,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Edit (`update`) only an inactive Connector, and pass the `connectorVersion` you
|
|
198
|
+
read to every change; a stale one is a `ConflictError`. `deactivate` stops new
|
|
199
|
+
calls. Registration is not retried automatically: after an `OutcomeUnknownError`,
|
|
200
|
+
list the Connectors and match the display name, which is unique. Templates refer
|
|
201
|
+
to Connectors through Console tool settings; this SDK does not set them.
|
|
202
|
+
|
|
203
|
+
Your Connector verifies every request the platform sends (initialization, tool
|
|
204
|
+
discovery and tool calls) with `verifyConnectorCallback`, and reads who a tool
|
|
205
|
+
call is for with `readConnectorCallContext`:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
import { ConnectorKeySet, verifyConnectorCallback, readConnectorCallContext } from "@tidblink/server";
|
|
209
|
+
|
|
210
|
+
// Fetch keys only from your trusted deployment URL; they are cached.
|
|
211
|
+
const keys = new ConnectorKeySet({ baseUrl });
|
|
212
|
+
await verifyConnectorCallback(request.headers.authorization, {
|
|
213
|
+
keys, organizationId, connectorId,
|
|
214
|
+
}); // Throws ConnectorAuthError: answer 401.
|
|
215
|
+
// For tools/call:
|
|
216
|
+
const { callId, userId, sessionId, deadline } = readConnectorCallContext(
|
|
217
|
+
message.params._meta, { organizationId },
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Tokens are Ed25519-signed, valid for at most 60 seconds, scoped to your
|
|
222
|
+
Organization and Connector, and may be reused within that scope. Deduplicate side
|
|
223
|
+
effects by `callId`; the platform never retries a call (`attempt` is always 1).
|
|
224
|
+
Abandon work after `deadline`. The packaged `http-connector.mjs` example is a
|
|
225
|
+
complete Connector over Streamable HTTP without an MCP library. The model sees each
|
|
226
|
+
tool as `mcp_<server name>__<tool>`. This SDK does not include a client for the
|
|
227
|
+
`tunnel` transport.
|
|
228
|
+
|
|
229
|
+
## Templates and Sessions
|
|
230
|
+
|
|
231
|
+
Use `organization.user(userId)` to discover available Templates and deployment models. Model
|
|
147
232
|
IDs are strings from the deployment catalog, including newly published models.
|
|
148
233
|
`effectiveDefaultModel` is the create-time default; `defaultModel` is the
|
|
149
234
|
service's canonical catalog default and may differ.
|
|
150
235
|
|
|
151
236
|
```ts
|
|
152
|
-
import {
|
|
237
|
+
import { OrganizationClient } from "@tidblink/server";
|
|
153
238
|
|
|
154
|
-
const
|
|
239
|
+
const organization = new OrganizationClient({
|
|
155
240
|
baseUrl: process.env.AGENT_SERVICE_URL!,
|
|
156
|
-
apiKey: process.env.
|
|
241
|
+
apiKey: process.env.ORGANIZATION_API_KEY!,
|
|
157
242
|
});
|
|
243
|
+
const customer = organization.user(process.env.END_USER_ID!);
|
|
158
244
|
const page = await customer.listAgentTemplates({ limit: 20 });
|
|
159
245
|
const template = page.templates[0];
|
|
160
246
|
if (!template) throw new Error("Select an available Template");
|
|
161
247
|
const models = await customer.listModels();
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
idempotencyKey: "customer-agent-1",
|
|
165
|
-
});
|
|
166
|
-
await agent.update({ additionalInstructions: "Prefer concise answers." });
|
|
167
|
-
const session = await agent.createSession({
|
|
248
|
+
const session = await customer.createSession({
|
|
249
|
+
templateId: template.agentTemplateId,
|
|
168
250
|
idempotencyKey: "customer-conversation-1",
|
|
169
251
|
model: models.effectiveDefaultModel,
|
|
170
252
|
billingTag: { key: "account", value: "customer-1" },
|
|
253
|
+
additionalInstructions: "Prefer concise answers.",
|
|
254
|
+
sandbox: { workingDirectory: "projects/trip" },
|
|
171
255
|
});
|
|
172
256
|
await session.rename("Planning");
|
|
173
257
|
await session.suggestName({ userText: "Plan a trip" }); // Generates and saves a name.
|
|
174
258
|
await session.setBillingTag(null);
|
|
175
259
|
```
|
|
176
260
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
261
|
+
`createSession` requires `templateId`, an active Template of the Organization;
|
|
262
|
+
there is no default Template. The other options belong to this Session only:
|
|
263
|
+
|
|
264
|
+
- `additionalInstructions` are added to the Template instructions. The service
|
|
265
|
+
normalizes line endings to LF and trims whitespace, allows up to 4096 UTF-8
|
|
266
|
+
bytes after normalization, and rejects unsupported control characters. The
|
|
267
|
+
Template and additional instructions together are limited to 16 KiB
|
|
268
|
+
(`agent_instructions_too_large`).
|
|
269
|
+
- `sandbox.profileId` selects a Sandbox profile; omit it or pass `null` to use
|
|
270
|
+
the Template's profile. `sandbox.workingDirectory` is a Drive-relative
|
|
271
|
+
directory and defaults to `.`; creating the Session does not create it.
|
|
272
|
+
- `acknowledgeSessionRecall: true` is required when the Template enables
|
|
273
|
+
cross-Session Recall, which can read other Sessions of the same End User;
|
|
274
|
+
otherwise creation fails with `agent_template_recall_acknowledgment_required`.
|
|
275
|
+
- `model`, `realtimeModel`, `billingTag` and Client Tool `tools` work as
|
|
276
|
+
described in their own sections.
|
|
277
|
+
|
|
278
|
+
Persist the creation key before calling. The same key and the same input recover
|
|
279
|
+
the original Session; reusing the key with different input raises
|
|
280
|
+
`ConflictError` with `session_creation_conflict`. Every option participates in
|
|
281
|
+
that identity. Do not generate a new key just because a response was lost.
|
|
184
282
|
|
|
185
283
|
`listAgentTemplates({ limit?, cursor? })` returns
|
|
186
|
-
`{ templates, hasMore, nextCursor }`; `listSessions({ limit?, cursor? })`
|
|
187
|
-
`{ sessions, hasMore, nextCursor }`, with `Session` handles in `sessions`.
|
|
284
|
+
`{ templates, hasMore, nextCursor }`; `listSessions({ limit?, cursor?, templateId? })`
|
|
285
|
+
returns `{ sessions, hasMore, nextCursor }`, with `Session` handles in `sessions`.
|
|
286
|
+
`templateId` lists only Sessions currently on that Template; keep the same
|
|
287
|
+
`templateId` when following a cursor.
|
|
188
288
|
Continue with the returned opaque cursor until `hasMore` is false, including
|
|
189
289
|
Template pages with no available entries. A changed Template catalog can refuse
|
|
190
290
|
continuation with `agent_template_list_changed`; restart discovery.
|
|
191
|
-
Session cursors belong to their owning User and list. `
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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:
|
|
291
|
+
Session cursors belong to their owning User and list. Use `getSession(id)` to
|
|
292
|
+
read a current owned Session; `session(id)` creates a handle without a request.
|
|
293
|
+
|
|
294
|
+
Each Session freezes its Template recipe, digest and creation options.
|
|
295
|
+
Later Template saves do not rewrite it; `session.data.template` names the
|
|
296
|
+
Template and digest in use. Only an explicit
|
|
297
|
+
[Template switch](#switching-a-sessions-template) moves it to a Template's
|
|
298
|
+
current content. Session model selection is separate:
|
|
206
299
|
|
|
207
300
|
```ts
|
|
208
301
|
const current = await customer.getSession(session.id);
|
|
@@ -220,39 +313,96 @@ decisions. `session.delete()` updates and returns the Session handle; `.data` co
|
|
|
220
313
|
and lists hide the Session. Active execution or a read-only Session can refuse
|
|
221
314
|
deletion. Name and Billing Tag changes do not change the frozen recipe.
|
|
222
315
|
|
|
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
316
|
The packaged example completes discovery and creation without message execution:
|
|
235
317
|
|
|
236
318
|
```js
|
|
237
319
|
import { createSessionWorkflow } from "@tidblink/server/examples/create-session.mjs";
|
|
238
320
|
|
|
239
|
-
const {
|
|
321
|
+
const { session } = await createSessionWorkflow({
|
|
240
322
|
baseUrl: process.env.AGENT_SERVICE_URL,
|
|
241
|
-
|
|
242
|
-
|
|
323
|
+
organizationApiKey: process.env.ORGANIZATION_API_KEY,
|
|
324
|
+
userId: process.env.END_USER_ID,
|
|
243
325
|
sessionCreationKey: process.env.SESSION_CREATION_KEY,
|
|
244
|
-
|
|
326
|
+
templateId: process.env.TEMPLATE_ID,
|
|
245
327
|
model: process.env.MODEL_ID,
|
|
246
328
|
});
|
|
247
329
|
```
|
|
248
330
|
|
|
249
|
-
For direct execution, set `AGENT_SERVICE_URL`, `
|
|
250
|
-
`TEMPLATE_ID
|
|
251
|
-
|
|
331
|
+
For direct execution, set `AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY`, `END_USER_ID`,
|
|
332
|
+
`TEMPLATE_ID` and `SESSION_CREATION_KEY`; `MODEL_ID` and
|
|
333
|
+
`ACKNOWLEDGE_SESSION_RECALL=true` are optional. The workflow also passes
|
|
334
|
+
`additionalInstructions`, `sandbox` and `acknowledgeSessionRecall` through.
|
|
335
|
+
Select and persist the Template ID, optional model and the key before calling.
|
|
252
336
|
The example also returns the current Template page and model catalog. If no model
|
|
253
337
|
was requested, it preserves omission so the service can recover the original
|
|
254
338
|
creation even after the deployment default changes.
|
|
255
339
|
|
|
340
|
+
### Switching a Session's Template
|
|
341
|
+
|
|
342
|
+
`session.switchTemplate()` moves a Session to another active Template's current
|
|
343
|
+
content. Switching to the Template the Session is already on also applies that
|
|
344
|
+
Template's latest saved content. Persist a key per switch, as for creation:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const receipt = await session.switchTemplate({
|
|
348
|
+
templateId: nextTemplateId,
|
|
349
|
+
idempotencyKey: switchKey,
|
|
350
|
+
// acknowledgeSessionRecall: true, when the target enables cross-Session Recall
|
|
351
|
+
});
|
|
352
|
+
if (receipt.status === "pending") {
|
|
353
|
+
const handoff = await session.getTurn(receipt.handoffTurnId);
|
|
354
|
+
if (handoff.status !== "running") await session.refresh();
|
|
355
|
+
} else if (receipt.status === "failed") {
|
|
356
|
+
console.warn(receipt.error.code);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
| `status` | Meaning |
|
|
361
|
+
| --- | --- |
|
|
362
|
+
| `unchanged` | The target's content equals the current configuration; nothing changed and no `handoffTurnId` |
|
|
363
|
+
| `succeeded` | The Session is on the target at the receipt's `configurationEpoch`. A first response says this only when the Session had no history, so no Handoff ran |
|
|
364
|
+
| `pending` | The Session has history, so the Handoff Turn `handoffTurnId` runs under the target Template |
|
|
365
|
+
|
|
366
|
+
A `pending` switch settles when its Handoff Turn does: the Session moves to the
|
|
367
|
+
target, or the switch fails and the Session stays on its previous Template.
|
|
368
|
+
Follow it like any Turn, with `session.getTurn(receipt.handoffTurnId)` or the
|
|
369
|
+
event stream, which reports `agent.session.template_switch.succeeded` or
|
|
370
|
+
`.failed`. `session.refresh()` then shows the new `template` and
|
|
371
|
+
`configurationEpoch`, and `switching` is `false` once it has settled. The receipt
|
|
372
|
+
holds no Session, so `switchTemplate()` never updates `session.data`; call
|
|
373
|
+
`refresh()`. While a switch is pending, new messages and another switch are
|
|
374
|
+
refused with `session_switch_in_progress`.
|
|
375
|
+
|
|
376
|
+
The service refuses a switch it does not record:
|
|
377
|
+
|
|
378
|
+
| Error | Cause |
|
|
379
|
+
| --- | --- |
|
|
380
|
+
| `ConflictError` `session_switch_busy` | A Turn is running or a Realtime Connection is live |
|
|
381
|
+
| `ConflictError` `session_switch_in_progress` | Another switch has not settled |
|
|
382
|
+
| `ConflictError` `agent_template_recall_acknowledgment_required` | The target enables cross-Session Recall; pass `acknowledgeSessionRecall: true` |
|
|
383
|
+
| `ConflictError` `session_read_only` | The Session cannot change Template, such as a Template Test Session |
|
|
384
|
+
| `ConflictError` `resource_conflict` | The target's Realtime language is no longer offered, or state changed during admission |
|
|
385
|
+
| `ConflictError` `idempotency_key_reused` | The key was used with a different Session, Template or acknowledgment |
|
|
386
|
+
| `TiDBLinkApiError` 404 `agent_template_unavailable` | The target is archived, missing or its dependencies are unusable |
|
|
387
|
+
| `TiDBLinkApiError` 404 `session_not_found` | The Session or its owner is unavailable |
|
|
388
|
+
| `TiDBLinkApiError` 503 `turn_admission_closed`, `skill_package_store_unavailable` | Temporarily unavailable; retry later |
|
|
389
|
+
|
|
390
|
+
After a lost response (`OutcomeUnknownError`), repeat the call with the same key
|
|
391
|
+
and input to recover the receipt; the service returns the command's latest
|
|
392
|
+
receipt and records no second switch. Do not poll this way. Commands belong to
|
|
393
|
+
the Organization API Key that sent them, so recover with the same key; after a
|
|
394
|
+
rotation, follow the Session instead. A replay after the Session is deleted or
|
|
395
|
+
its owner loses access is refused with `session_not_found`.
|
|
396
|
+
|
|
397
|
+
The Handoff Turn summarizes the Session so far for the target Template. History
|
|
398
|
+
reads return it as a Turn with `kind: "template_handoff"`, no `userMessage` and
|
|
399
|
+
an empty `userMessages`; when it succeeds, its assistant Message is the summary.
|
|
400
|
+
The service's
|
|
401
|
+
[ADR 0107](https://github.com/mem9-ai/agent-stack/blob/4305ef67daebf23c416badf806217a7c11dcef37/docs/adr/0107-switch-session-template-baselines-through-configuration-epochs.md)
|
|
402
|
+
describes what the Session keeps across a switch (models, Billing Tag, Client
|
|
403
|
+
Tools and creation options) and that cancelling the Handoff Turn fails the
|
|
404
|
+
switch.
|
|
405
|
+
|
|
256
406
|
## Text messages and original results
|
|
257
407
|
|
|
258
408
|
Persist the Session ID, command identity and complete input before dispatch.
|
|
@@ -324,7 +474,8 @@ The packaged example uses an existing owned Session and a caller-retained input:
|
|
|
324
474
|
import { sendMessageWorkflow } from "@tidblink/server/examples/send-message.mjs";
|
|
325
475
|
const result = await sendMessageWorkflow({
|
|
326
476
|
baseUrl: process.env.AGENT_SERVICE_URL,
|
|
327
|
-
|
|
477
|
+
organizationApiKey: process.env.ORGANIZATION_API_KEY,
|
|
478
|
+
userId: process.env.END_USER_ID,
|
|
328
479
|
sessionId: process.env.SESSION_ID,
|
|
329
480
|
clientEventId: process.env.CLIENT_EVENT_ID,
|
|
330
481
|
text: process.env.MESSAGE_TEXT,
|
|
@@ -337,71 +488,237 @@ final answer. Preserve the input across invocations; each new command identity
|
|
|
337
488
|
can start new work. This slice replaces the obsolete response-owned NDJSON
|
|
338
489
|
`streamTurn` API. Use the independent progress subscription below.
|
|
339
490
|
|
|
340
|
-
##
|
|
491
|
+
## Progress and Session events
|
|
492
|
+
|
|
493
|
+
Progress and events reach the application over a Session connection, not over
|
|
494
|
+
a per-Session stream: `conn.send(text, { onProgress })` reports a message's draft,
|
|
495
|
+
and `session.connect({ onEvent, cursor, onCursor })` follows every event of the
|
|
496
|
+
Session with a resumable cursor (see [Connecting a Session](#connecting-a-session)).
|
|
497
|
+
`waitForResult(inputId)` and `turn()` stay available over plain HTTP for a
|
|
498
|
+
process that keeps no connection, for example to recover a result after a restart.
|
|
499
|
+
|
|
500
|
+
`asKnownSessionEvent(event)` narrows an event to the types the SDK knows: Turn
|
|
501
|
+
lifecycle, drafts, progress, saved messages and Operation steps, Client Tool
|
|
502
|
+
calls, Realtime transcripts and responses, Template switches and
|
|
503
|
+
`agent.session.idle`. It returns `undefined` for anything else, which is still
|
|
504
|
+
delivered unchanged. A draft (`agent.session.turn.output_text.snapshot`) is a
|
|
505
|
+
complete snapshot: replace what you showed, including with an empty reset.
|
|
506
|
+
|
|
507
|
+
## Connecting a Session
|
|
341
508
|
|
|
342
|
-
|
|
509
|
+
`session.connect()` attaches a Session to this process's Backend Connection, one
|
|
510
|
+
WebSocket per Application Server process to the core (ADR 0109 of the service).
|
|
511
|
+
It is meant for a backend that stays attached to a Session, for example while
|
|
512
|
+
the user's device is online: it receives the Session's events, and it declares
|
|
513
|
+
and executes the Session's Client Tools. Each connection is one row in the
|
|
514
|
+
core and costs no polling, so attach when the device comes online.
|
|
343
515
|
|
|
344
516
|
```ts
|
|
345
|
-
const
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
517
|
+
const conn = await organization.user(userId).session(sessionId).connect({
|
|
518
|
+
tools: lampTools(deviceId), // see below
|
|
519
|
+
cursor: await db.getCursor(deviceId), // resume after a restart
|
|
520
|
+
onCursor: (cursor) => db.saveCursor(deviceId, cursor),
|
|
521
|
+
onTranscript: (t) => app.push(deviceId, { role: "user", text: t.text }),
|
|
522
|
+
onResponse: (r) => app.push(deviceId, { role: "assistant", text: r.text }),
|
|
523
|
+
onEvent: (event) => forward(event), // every event, in order
|
|
350
524
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
525
|
+
conn.closed.then((end) => console.log("connection ended", end.reason));
|
|
526
|
+
// ...when the device goes offline:
|
|
527
|
+
await conn.close();
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
- **Events.** Callbacks run one event at a time, in the Session's order, and
|
|
531
|
+
`onCursor` follows once an event's callbacks have completed. Persist that
|
|
532
|
+
cursor and pass it back as `cursor` to replay what was missed; without one,
|
|
533
|
+
delivery starts after the Session's latest event and history is not replayed.
|
|
534
|
+
`onTranscript` and `onResponse` receive completed Realtime transcripts and
|
|
535
|
+
responses; `onEvent` receives every event, including types the SDK does not
|
|
536
|
+
know.
|
|
537
|
+
- **Reconnection.** The SDK opens Backend Connections on demand, holds up to the
|
|
538
|
+
`maxSessions` the core announces on each (1024 today) and opens another when
|
|
539
|
+
one is full. After a lost connection, or when the core asks a connection to
|
|
540
|
+
drain before a restart, it re-attaches each Session on another connection from
|
|
541
|
+
the last processed event, without gaps or repeats. A connection that holds
|
|
542
|
+
nothing closes after a few seconds. Liveness is checked with an application
|
|
543
|
+
`ping` when a connection is silent.
|
|
544
|
+
- **One executor per Session.** A connection attaches for execution by default.
|
|
545
|
+
Another process's connection to the same Session takes it over, and this one
|
|
546
|
+
ends with `closed` resolving `{ reason: "superseded" }`. One `OrganizationClient`
|
|
547
|
+
holds at most one connection per Session; reuse it. `observe: true` attaches
|
|
548
|
+
for events only, never takes the Session over and cannot declare tools.
|
|
549
|
+
- **Ends.** `closed` resolves once: `closed` after `close()` or
|
|
550
|
+
`organization.close()`, `superseded`, `session_deleted`, or `error` when the
|
|
551
|
+
connection cannot be restored, for example after the Organization API Key was
|
|
552
|
+
revoked (a `BackendConnectionClosedError` with code 4001). A refused attach
|
|
553
|
+
rejects `connect()` with a `BackendRequestError` whose `reason` is the core's,
|
|
554
|
+
such as `session_not_found`.
|
|
555
|
+
- **Errors in your callbacks** do not stop delivery. They go to `onError`;
|
|
556
|
+
without it they are rethrown asynchronously, which by default ends a Node.js
|
|
557
|
+
process.
|
|
558
|
+
|
|
559
|
+
### Client Tools on a connection
|
|
560
|
+
|
|
561
|
+
`tools` maps each Client Tool name to its declaration and a `run` function. Each
|
|
562
|
+
attach replaces the Session's whole Client Tool set with these declarations, and
|
|
563
|
+
the connection executes their calls:
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
const lampTools = (deviceId: string): ClientToolHandlers => ({
|
|
567
|
+
set_light: {
|
|
568
|
+
description: "Turn the lamp on or off, or set its brightness",
|
|
569
|
+
parameters: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
on: { type: "boolean" },
|
|
573
|
+
brightness: { type: "integer", minimum: 1, maximum: 100 },
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
run: async (args, { signal }) => {
|
|
577
|
+
// Fail at once when the device is offline, so the model does not wait.
|
|
578
|
+
const state = await askDevice(deviceId, "set_light", args, { signal });
|
|
579
|
+
return state.on ? `On at ${state.brightness}%` : "Off";
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
- **Claim before running.** For each call the SDK first asks the core for the
|
|
586
|
+
right to execute it and runs `run` only when the core grants it, so a call the
|
|
587
|
+
core reports as not performed was never executed. A call that nobody claims
|
|
588
|
+
within the core's claim window (5 seconds) is reported to the model as not
|
|
589
|
+
performed.
|
|
590
|
+
- **Results.** A returned string is what the model receives; other values are
|
|
591
|
+
sent as JSON. A thrown error becomes a failure whose message the model sees.
|
|
592
|
+
A tool the connection has no handler for fails with a message saying so.
|
|
593
|
+
Results go back over any Backend Connection of the Organization and are
|
|
594
|
+
retried after a lost connection until the call's deadline.
|
|
595
|
+
- **Once per call.** A `callId` runs at most once per connection, even when the
|
|
596
|
+
core sends it again after a reconnect. If the core's answer to a claim is
|
|
597
|
+
lost, the SDK does not run the call and closes it as failed.
|
|
598
|
+
- **Deadline.** `run` receives `{ callId, sessionId, userId, turnId, name,
|
|
599
|
+
deadlineAt, signal }`. At `deadlineAt`, 60 seconds after the call was
|
|
600
|
+
published, `signal` aborts and the SDK reports a timeout; the core then tells
|
|
601
|
+
the model the effect is unknown.
|
|
602
|
+
- **After `close()`** calls already running finish and their results are still
|
|
603
|
+
submitted; later calls are not received.
|
|
604
|
+
|
|
605
|
+
A result the core does not accept, for example after the call's deadline,
|
|
606
|
+
reaches `onError` as a `ClientToolResultRefusedError` with the core's `reason`.
|
|
607
|
+
Calls are answered only on the executing connection; the `claimBy` they carry in
|
|
608
|
+
`requires_action` events marks them as such.
|
|
609
|
+
|
|
610
|
+
### Messages and voice on a connection
|
|
611
|
+
|
|
612
|
+
`conn.send(text, options)` sends a user message over the connection and resolves
|
|
613
|
+
with the same result as `session.turn()`, once the Turn finishes:
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
const reply = await conn.send(text, {
|
|
617
|
+
clientEventId: savedMessageId, // optional; persist it to recover after a crash
|
|
618
|
+
onProgress: (draft) => app.show(deviceId, draft),
|
|
619
|
+
});
|
|
620
|
+
return reply.text;
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
- The message starts a Turn on an idle Session or steers the active one; the
|
|
624
|
+
call waits for that Turn either way. Rejections, failures, interruptions and
|
|
625
|
+
local timeouts raise the same errors as `session.turn()`: `InputNotAppliedError`,
|
|
626
|
+
`TurnFailedError`, `TurnInterruptedError` and `TurnWaitTimeoutError` (default
|
|
627
|
+
`timeoutMs` 60 seconds, which never cancels the Turn).
|
|
628
|
+
- The call resolves once the Session has also released the Turn
|
|
629
|
+
(`agent.session.idle`), so the next `send()`, audio Turn or Template switch
|
|
630
|
+
starts at once instead of steering a finished Turn or being refused as busy.
|
|
631
|
+
The release normally follows the result at once; after 5 seconds without it
|
|
632
|
+
the result is returned anyway.
|
|
633
|
+
- Progress comes from the pushed events, without polling. `onProgress` receives
|
|
634
|
+
the complete draft each time it grows (today every 1,024 characters): replace
|
|
635
|
+
what you showed, do not append. When the Turn finishes, the SDK reads the
|
|
636
|
+
settled result once over REST.
|
|
637
|
+
- `clientEventId` shares the Session's command namespace with REST. Without
|
|
638
|
+
one the SDK generates a UUID. If a reconnect loses the core's answer, the SDK
|
|
639
|
+
sends the message again with the same `clientEventId`, which the core
|
|
640
|
+
deduplicates; if it cannot, the call raises `OutcomeUnknownError` with
|
|
641
|
+
`inputId`, and `session.waitForResult(inputId)` recovers the result.
|
|
642
|
+
- A refusal of the message command, such as `session_read_only` or
|
|
643
|
+
`turn_admission_closed`, raises `BackendRequestError` with that code as
|
|
644
|
+
`reason` and the REST `details`.
|
|
645
|
+
|
|
646
|
+
`conn.realtimeGrant({ audio?, metadata? })` prepares the Session's Realtime
|
|
647
|
+
connection and returns `{ websocketUrl, connectToken, connectTokenExpiresAt }`
|
|
648
|
+
for a device to join it directly. When a Realtime connection is already live it
|
|
649
|
+
returns a new token for that one, and `audio` is not applied. The token is short
|
|
650
|
+
lived; ask for a new grant each time the device wants to talk.
|
|
651
|
+
|
|
652
|
+
### Example: a smart lamp
|
|
653
|
+
|
|
654
|
+
The packaged `lamp-backend.mjs` example is the backend of a lamp that users talk
|
|
655
|
+
to by typing in an app or by speaking to the lamp, with one Session per lamp so
|
|
656
|
+
both share a conversation. `createLampBackend({ baseUrl, organizationApiKey,
|
|
657
|
+
templateId, device, store })` returns:
|
|
658
|
+
|
|
659
|
+
- `bindDevice(appUserId, deviceId)`: one End User per app user, one Session per lamp.
|
|
660
|
+
- `onDeviceOnline(deviceId)` / `onDeviceOffline(deviceId)`: keep the lamp's Session
|
|
661
|
+
connected while it is online, with the lamp's tools `set_light`, `get_status`
|
|
662
|
+
and `set_timer`, which forward each call to the lamp over your device channel.
|
|
663
|
+
- `onAppMessage(deviceId, text, { onDraft })`: a typed message; when the lamp is
|
|
664
|
+
offline a temporary connection is used and the lamp's tools fail at once.
|
|
665
|
+
- `onDeviceWantsToTalk(deviceId)`: sends the lamp a Realtime grant.
|
|
666
|
+
|
|
667
|
+
`device` is your channel to the lamps: `isOnline(deviceId)`,
|
|
668
|
+
`request(deviceId, command, args, { signal })` and `send(deviceId, topic,
|
|
669
|
+
payload)`. Wired to MQTT, with the lamp announcing itself through a retained
|
|
670
|
+
presence message:
|
|
671
|
+
|
|
672
|
+
```js
|
|
673
|
+
const lamps = createLampBackend({ baseUrl, organizationApiKey, templateId, device: mqttDevices(broker) });
|
|
674
|
+
broker.subscribe("lamp/+/presence");
|
|
675
|
+
broker.subscribe("lamp/+/talk");
|
|
676
|
+
broker.on("message", (topic, payload) => {
|
|
677
|
+
const [, deviceId, kind] = topic.split("/");
|
|
678
|
+
if (kind === "presence")
|
|
679
|
+
payload.toString() === "online" ? lamps.onDeviceOnline(deviceId) : lamps.onDeviceOffline(deviceId);
|
|
680
|
+
if (kind === "talk") lamps.onDeviceWantsToTalk(deviceId);
|
|
681
|
+
});
|
|
682
|
+
app.post("/devices/:id/messages", async (req, res) =>
|
|
683
|
+
res.json({ text: await lamps.onAppMessage(req.params.id, req.body.text) }));
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
The lamp itself does not use the SDK. Its firmware, in pseudo-code:
|
|
687
|
+
|
|
688
|
+
```c
|
|
689
|
+
mqtt_connect(broker, will = { "lamp/{id}/presence", "offline", retain });
|
|
690
|
+
mqtt_publish("lamp/{id}/presence", "online", retain);
|
|
691
|
+
|
|
692
|
+
on_message("lamp/{id}/cmd", msg) { // a Client Tool call, forwarded
|
|
693
|
+
switch (msg.command) {
|
|
694
|
+
case "set_light": apply(msg.args); reply(msg.requestId, ok, state()); break;
|
|
695
|
+
case "get_status": reply(msg.requestId, ok, { ...state(), ambientLux: lux() }); break;
|
|
696
|
+
case "set_timer": timer_start(msg.args.minutes * 60, led_off); reply(msg.requestId, ok, {}); break;
|
|
697
|
+
default: reply(msg.requestId, error, "unknown command");
|
|
362
698
|
}
|
|
363
699
|
}
|
|
364
|
-
```
|
|
365
700
|
|
|
366
|
-
|
|
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:
|
|
701
|
+
on_button_press() { mqtt_publish("lamp/{id}/talk", ""); }
|
|
395
702
|
|
|
396
|
-
|
|
397
|
-
|
|
703
|
+
on_message("lamp/{id}/voice_grant", grant) { // audio goes to TiDB Link directly
|
|
704
|
+
ws = websocket_connect(grant.websocketUrl, grant.connectToken);
|
|
705
|
+
while (ws.open) { ws.send(mic_read(20ms)); if (ws.has_audio()) speaker_play(ws.recv()); }
|
|
706
|
+
}
|
|
398
707
|
```
|
|
399
708
|
|
|
709
|
+
Cloud capabilities such as a warranty lookup belong in a Connector the
|
|
710
|
+
Template references (see [Connectors](#connectors)), not in the lamp's tools.
|
|
711
|
+
|
|
712
|
+
The connection uses the global `WebSocket` of Node.js 22, which sends the
|
|
713
|
+
Organization API Key in the upgrade's `Authorization` header and never in the
|
|
714
|
+
URL. Pass `WebSocket` to `OrganizationClient` to use another implementation; it
|
|
715
|
+
must accept `new WebSocket(url, { protocols, headers })`.
|
|
716
|
+
|
|
400
717
|
## Session Client Tools
|
|
401
718
|
|
|
402
|
-
Declare the application functions that
|
|
403
|
-
|
|
404
|
-
|
|
719
|
+
Declare the application functions that a Turn may request. `connect({ tools })`
|
|
720
|
+
declares them on every attach; the REST methods below read or replace the set
|
|
721
|
+
without a connection. Parameters are JSON Schema objects. The service validates reserved
|
|
405
722
|
names, schema size/depth and combined tool limits; the `async` argument is reserved
|
|
406
723
|
for the platform and is not part of application arguments.
|
|
407
724
|
|
|
@@ -418,7 +735,8 @@ const tools: ClientToolDeclaration[] = [{
|
|
|
418
735
|
additionalProperties: false,
|
|
419
736
|
},
|
|
420
737
|
}];
|
|
421
|
-
const session = await
|
|
738
|
+
const session = await customer.createSession({
|
|
739
|
+
templateId: savedTemplateId,
|
|
422
740
|
idempotencyKey: savedSessionCreationKey,
|
|
423
741
|
tools,
|
|
424
742
|
});
|
|
@@ -429,54 +747,12 @@ await session.replaceClientTools(tools, { ifMatch: current.etag });
|
|
|
429
747
|
Replacement is the complete set, with `[]` clearing it. Its ETag is independent of
|
|
430
748
|
the Session's model ETag. Stale replacement returns `precondition_failed`; read the
|
|
431
749
|
set and decide again. A Turn retains the declarations admitted when it started.
|
|
432
|
-
An identical replacement keeps its revision.
|
|
433
|
-
use the Session
|
|
750
|
+
An identical replacement keeps its revision. Text, voice and prerecorded-audio Turns
|
|
751
|
+
use the Session's declarations; Scheduler-started Turns do not.
|
|
434
752
|
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
```
|
|
753
|
+
An executing Session connection runs the calls of these tools; see
|
|
754
|
+
[Client Tools on a connection](#client-tools-on-a-connection). There is no REST
|
|
755
|
+
path to answer a call.
|
|
480
756
|
|
|
481
757
|
## Saved conversation history
|
|
482
758
|
|
|
@@ -556,7 +832,7 @@ request, not network recovery.
|
|
|
556
832
|
The installed `@tidblink/server/examples/control-turn.mjs` exports
|
|
557
833
|
`controlTurnWorkflow`. It returns cancellation acceptance for `action: "cancel"`
|
|
558
834
|
and waits for the separate retry result for `action: "retry"`. Run it with
|
|
559
|
-
`AGENT_SERVICE_URL`, `
|
|
835
|
+
`AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY`, `END_USER_ID`, `SESSION_ID`, `TURN_ID`, `CLIENT_EVENT_ID` and
|
|
560
836
|
`TURN_ACTION` set; retain those identities before dispatch. This example never
|
|
561
837
|
chooses to retry automatically.
|
|
562
838
|
|
|
@@ -567,12 +843,12 @@ owned Session for a terminal. The SDK returns the one-time connect token but doe
|
|
|
567
843
|
not open or implement the WebSocket transport.
|
|
568
844
|
|
|
569
845
|
```ts
|
|
570
|
-
import {
|
|
846
|
+
import { OrganizationClient } from "@tidblink/server";
|
|
571
847
|
|
|
572
|
-
const user = new
|
|
848
|
+
const user = new OrganizationClient({
|
|
573
849
|
baseUrl: process.env.AGENT_SERVICE_URL!,
|
|
574
|
-
apiKey: process.env.
|
|
575
|
-
});
|
|
850
|
+
apiKey: process.env.ORGANIZATION_API_KEY!,
|
|
851
|
+
}).user(process.env.END_USER_ID!);
|
|
576
852
|
const catalog = await user.listRealtimeModels();
|
|
577
853
|
const session = user.session(process.env.SESSION_ID!);
|
|
578
854
|
const current = await session.getRealtimeModelSelection();
|
|
@@ -608,13 +884,14 @@ import { prepareRealtimeConnection } from "@tidblink/server/examples/prepare-rea
|
|
|
608
884
|
|
|
609
885
|
const prepared = await prepareRealtimeConnection({
|
|
610
886
|
baseUrl: process.env.AGENT_SERVICE_URL,
|
|
611
|
-
|
|
887
|
+
organizationApiKey: process.env.ORGANIZATION_API_KEY,
|
|
888
|
+
userId: process.env.END_USER_ID,
|
|
612
889
|
sessionId: process.env.SESSION_ID,
|
|
613
890
|
realtimeModelId: process.env.REALTIME_MODEL_ID,
|
|
614
891
|
});
|
|
615
892
|
```
|
|
616
893
|
|
|
617
|
-
Set `AGENT_SERVICE_URL`, `
|
|
894
|
+
Set `AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY`, `END_USER_ID`, and `SESSION_ID` to run the example
|
|
618
895
|
directly. `REALTIME_MODEL_ID` is optional.
|
|
619
896
|
|
|
620
897
|
## Prerecorded audio
|
|
@@ -660,7 +937,7 @@ does not resubmit a recording automatically. A transcription refusal may still
|
|
|
660
937
|
have consumed provider work; it does not imply a refunded charge.
|
|
661
938
|
|
|
662
939
|
The installed `submit-audio.mjs` example reads an existing file and prints the
|
|
663
|
-
final answer. Set `AGENT_SERVICE_URL`, `
|
|
940
|
+
final answer. Set `AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY`, `END_USER_ID`, `SESSION_ID` and
|
|
664
941
|
`AUDIO_FILE_PATH`; `AUDIO_CONTENT_TYPE` is optional:
|
|
665
942
|
|
|
666
943
|
```sh
|
|
@@ -731,7 +1008,7 @@ API Key. Files are held in memory while hashing or downloading, so size your
|
|
|
731
1008
|
backend's concurrency for the service's advertised maximum.
|
|
732
1009
|
|
|
733
1010
|
The packaged `upload-file.mjs` example uploads and attaches one file. Set
|
|
734
|
-
`AGENT_SERVICE_URL`, `
|
|
1011
|
+
`AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY`, `END_USER_ID`, `SESSION_ID`, `FILE_PATH`,
|
|
735
1012
|
`UPLOAD_IDEMPOTENCY_KEY`, and `CLIENT_EVENT_ID`, then run:
|
|
736
1013
|
|
|
737
1014
|
```sh
|
|
@@ -743,110 +1020,28 @@ The example returns the final conversation result; its exported
|
|
|
743
1020
|
directories or write Session text files. Organization owner-scoped clients can
|
|
744
1021
|
upload, read and attach the same ordinary Files.
|
|
745
1022
|
|
|
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
1023
|
## Errors and uncertain outcomes
|
|
818
1024
|
|
|
819
|
-
Organization, text/progress/history and
|
|
1025
|
+
Organization, text/progress/history and Session lifecycle methods accept
|
|
820
1026
|
`{ requestId?, signal? }` options; creation and list options include these fields.
|
|
821
1027
|
`TiDBLinkApiError` preserves status, service code, request identity and details;
|
|
822
1028
|
`ConflictError` preserves explicit 409/412 conflicts. The service's response
|
|
823
1029
|
request identity takes precedence over the one sent.
|
|
824
1030
|
|
|
825
|
-
Safe reads
|
|
826
|
-
request identity and
|
|
827
|
-
Session updates,
|
|
1031
|
+
Safe reads, keyed Session creation and Template switches use bounded retries
|
|
1032
|
+
with the same request identity and key. User creation,
|
|
1033
|
+
Session updates, deletion and Realtime mutations are never
|
|
828
1034
|
automatically replayed. Disconnects, unusable success responses, HTTP 408 and
|
|
829
1035
|
server failures during these mutations raise `OutcomeUnknownError`. For an HTTP failure, its
|
|
830
|
-
`cause` retains the `TiDBLinkApiError`.
|
|
831
|
-
failure still reports an unknown outcome. `AbortSignal` stops the local request;
|
|
1036
|
+
`cause` retains the `TiDBLinkApiError`. `AbortSignal` stops the local request;
|
|
832
1037
|
after dispatch it does not establish whether a mutation happened. Redirects are
|
|
833
1038
|
refused.
|
|
834
1039
|
|
|
835
|
-
After uncertain User creation,
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|
1040
|
+
After uncertain End User creation, repeat it with the same `externalId`.
|
|
1041
|
+
|
|
1042
|
+
After uncertain Session creation or Template switch, repeat the original input
|
|
1043
|
+
with the same key. After an uncertain name, tag or model update, read the
|
|
1044
|
+
resource before deciding on another change. Deletion can hide a
|
|
850
1045
|
resource from ordinary reads, so a missing resource does not recover its original
|
|
851
1046
|
mutation receipt.
|
|
852
1047
|
|
|
@@ -857,27 +1052,22 @@ new token after reconciling the connection state.
|
|
|
857
1052
|
|
|
858
1053
|
## User usage
|
|
859
1054
|
|
|
860
|
-
|
|
861
|
-
|
|
1055
|
+
`organization.getUserUsage(userId, options)` and
|
|
1056
|
+
`organization.getUserUsageDetails(userId, options)` read one target User's usage.
|
|
862
1057
|
|
|
863
|
-
|
|
|
864
|
-
| --- | --- |
|
|
865
|
-
|
|
|
866
|
-
|
|
|
867
|
-
|
|
|
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 |
|
|
1058
|
+
| | |
|
|
1059
|
+
| --- | --- |
|
|
1060
|
+
| Range | Required exact UTC timestamps with milliseconds, inclusive `from` and exclusive `to`; at most 31 days |
|
|
1061
|
+
| Buckets | `interval: "hour"`, `"day"`, `"week"` or `"month"` |
|
|
1062
|
+
| Details | Globally ordered `items`; optional `kind`, `limit` (1 to 500) and opaque `cursor`; `nextCursor: null` ends the stream |
|
|
870
1063
|
|
|
871
1064
|
Both support an exact `tagKey`/`tagValue` pair. Send both or neither. Values retain
|
|
872
|
-
case sensitivity and the service's normalization rules.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1065
|
+
case sensitivity and the service's normalization rules. Both also accept
|
|
1066
|
+
`templateId`, which keeps only usage admitted under that Template; each detail
|
|
1067
|
+
item reports it as `agentTemplateId`. Preserve the original range, kind, Template
|
|
1068
|
+
and tag filters when following a cursor.
|
|
876
1069
|
|
|
877
1070
|
```ts
|
|
878
|
-
const own = await user.getUsage({
|
|
879
|
-
from: "2030-01-02", to: "2030-01-02", period: "day",
|
|
880
|
-
});
|
|
881
1071
|
const page = await organization.getUserUsageDetails(targetUserId, {
|
|
882
1072
|
from: "2030-01-02T00:00:00.000Z",
|
|
883
1073
|
to: "2030-01-03T00:00:00.000Z",
|
|
@@ -892,14 +1082,12 @@ an unpriced fact billable. Priced totals exclude unpriced facts. Scalar cost tot
|
|
|
892
1082
|
costs; `costByBearer` preserves the separate platform, customer and unknown
|
|
893
1083
|
categories. Public-list estimates are not supplier invoices or known final charges. Fact completeness and
|
|
894
1084
|
valuation completeness are independent. Do not display missing costs as zero or
|
|
895
|
-
infer a balance from these reads.
|
|
896
|
-
some details available in the User's own projection.
|
|
1085
|
+
infer a balance from these reads.
|
|
897
1086
|
|
|
898
1087
|
The installed example `@tidblink/server/examples/read-usage.mjs` exports
|
|
899
|
-
`
|
|
900
|
-
|
|
901
|
-
`
|
|
902
|
-
`END_USER_ID`. Supply `USAGE_FROM`/`USAGE_TO` in the corresponding date format;
|
|
1088
|
+
`readUserUsage`, returning a summary and the first detail page. The CLI requires
|
|
1089
|
+
`AGENT_SERVICE_URL`, `ORGANIZATION_API_KEY` and `END_USER_ID`. Supply
|
|
1090
|
+
`USAGE_FROM`/`USAGE_TO` as exact UTC timestamps;
|
|
903
1091
|
`TAG_KEY`/`TAG_VALUE` are optional. It performs reads only and does not claim that
|
|
904
1092
|
a first detail page is complete history.
|
|
905
1093
|
|
|
@@ -922,7 +1110,7 @@ packed artifact in a clean ESM TypeScript consumer. To verify the packed SDK and
|
|
|
922
1110
|
its examples against the real service, use the service's Node.js 26 runtime, install its
|
|
923
1111
|
pinned dependencies, generate Prisma, build its Hardware contract and prepare its
|
|
924
1112
|
current schema in a **dedicated test database**. See the selected service's
|
|
925
|
-
[current-schema setup](https://github.com/mem9-ai/agent-stack/blob/
|
|
1113
|
+
[current-schema setup](https://github.com/mem9-ai/agent-stack/blob/4305ef67daebf23c416badf806217a7c11dcef37/docs/prisma-first-tidb-validation-and-rollout.md).
|
|
926
1114
|
|
|
927
1115
|
```sh
|
|
928
1116
|
AGENT_SERVICE_ROOT=/path/to/agent-stack \
|
|
@@ -944,11 +1132,11 @@ The compiled fixture reuses the service system harness, its isolated database,
|
|
|
944
1132
|
controlled model/storage/ASR/voice dependencies and real background runner. A
|
|
945
1133
|
separate process installs the tarball in a clean directory and calls only the
|
|
946
1134
|
installed SDK and packaged Organization example for its application workflow.
|
|
947
|
-
It verifies Organization provisioning
|
|
1135
|
+
It verifies Organization provisioning, discovery, Session
|
|
948
1136
|
creation replay, owner isolation, file upload/download and message attachment,
|
|
949
|
-
text results
|
|
950
|
-
|
|
951
|
-
|
|
1137
|
+
text results and history, Client Tools on a Backend Connection, Connector registration and a Session's call
|
|
1138
|
+
to the example HTTP Connector, audio,
|
|
1139
|
+
Realtime HTTP grants, rotation/revocation and fresh-authority recovery. Management credential changes
|
|
952
1140
|
and controlled suppliers are fixture actions, not claimed SDK APIs. WebSocket
|
|
953
1141
|
redemption, live speech suppliers, physical hardware and deployment compatibility
|
|
954
1142
|
are outside this SDK proof.
|
|
@@ -957,18 +1145,23 @@ The remaining fixtures use the service's existing AppModule test seam and reset
|
|
|
957
1145
|
all rows in the selected dedicated test database before and after their runs.
|
|
958
1146
|
They use genuine credentials, real TiDB and controlled dependencies. Their scope:
|
|
959
1147
|
Fixtures prepare identities, Templates and eligible
|
|
960
|
-
Realtime Sessions. A composed installed quickstart provisions one fresh User
|
|
961
|
-
discovers Templates/models, creates
|
|
1148
|
+
Realtime Sessions. A composed installed quickstart provisions one fresh User,
|
|
1149
|
+
discovers Templates/models, creates a Session, obtains a final answer
|
|
962
1150
|
and recovers the same original result. Public SDK methods also verify ownership,
|
|
963
|
-
lifecycle updates and
|
|
1151
|
+
lifecycle updates, Template switches with and without a Handoff Turn, and Realtime
|
|
1152
|
+
operations. Text cases
|
|
964
1153
|
exercise actual acceptance, final success/failure/interruption, local timeout and
|
|
965
1154
|
abandoned-wait recovery, response-loss reconciliation, and paginated mixed
|
|
966
|
-
history through public SDK methods.
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1155
|
+
history through public SDK methods. Client Tool cases declare and replace tools
|
|
1156
|
+
with ownership and stale-revision refusals. Backend Connection cases attach
|
|
1157
|
+
Sessions, run the real Runtime's Client Tool calls through claim and result,
|
|
1158
|
+
send messages with drafts, failures, timeouts and cancellation, resume from a
|
|
1159
|
+
cursor, supersede connections and grant Realtime tokens. A second compiled
|
|
1160
|
+
fixture runs two replicas behind a local load balancer: a Turn on one replica
|
|
1161
|
+
calls the tool of an attachment on the other, a draining replica moves the
|
|
1162
|
+
attachment to the other one, a dropped connection replays its events, and
|
|
1163
|
+
another process supersedes the connection. A forwarding proxy drops only an actual service response to prove
|
|
1164
|
+
uncertain-submission recovery. Control cases
|
|
972
1165
|
verify active-Turn steering, eventual cancellation, disabled retry and an explicitly
|
|
973
1166
|
enabled checkpoint retry with an unchanged original outcome. The enabled fixture
|
|
974
1167
|
uses the existing controlled Drive9 and model suppliers. File cases use a separate
|
|
@@ -977,16 +1170,12 @@ They verify inline, multipart and empty uploads, cancellation and invalid conten
|
|
|
977
1170
|
large-file download-ticket redemption, and a real Runtime file read whose answer
|
|
978
1171
|
contains the uploaded content. The installed upload example exercises the same
|
|
979
1172
|
public workflow. Real supplier deployment certification is outside this evidence.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
1173
|
+
Audio cases use the real transcription adapter with a controlled HTTP supplier.
|
|
1174
|
+
They cover recorded input, malformed/unsupported recordings, supplier refusals,
|
|
1175
|
+
model failure, active-response interruption and the installed audio example.
|
|
1176
|
+
The public Session projection exposes the frozen Template digest but hides the
|
|
1177
|
+
stored creation options. Those snapshots and active-Turn model freezing also rely
|
|
1178
|
+
on the selected service's [existing persistence evidence](https://github.com/mem9-ai/agent-stack/blob/4305ef67daebf23c416badf806217a7c11dcef37/test/product/sessions/sessions-withorgstore.tidb.spec.ts); SDK reads alone do not
|
|
990
1179
|
prove those hidden values.
|
|
991
1180
|
Usage cases seed controlled facts and verify both public projections, exact filters,
|
|
992
1181
|
paging and authority without executing messages to manufacture billing data.
|