hci-atrium 0.1.0 → 0.3.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 (96) hide show
  1. package/README.md +266 -8
  2. package/dist/asset-meta.d.ts +31 -0
  3. package/dist/asset-meta.d.ts.map +1 -0
  4. package/dist/asset-meta.js +25 -0
  5. package/dist/asset-meta.js.map +1 -0
  6. package/dist/atrium.d.ts +220 -7
  7. package/dist/atrium.d.ts.map +1 -1
  8. package/dist/atrium.js +410 -39
  9. package/dist/atrium.js.map +1 -1
  10. package/dist/dash.d.ts +206 -0
  11. package/dist/dash.d.ts.map +1 -0
  12. package/dist/dash.js +302 -0
  13. package/dist/dash.js.map +1 -0
  14. package/dist/direct.d.ts +86 -0
  15. package/dist/direct.d.ts.map +1 -0
  16. package/dist/direct.js +121 -0
  17. package/dist/direct.js.map +1 -0
  18. package/dist/index.d.ts +8 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/models.d.ts +70 -0
  23. package/dist/models.d.ts.map +1 -1
  24. package/dist/models.js +56 -0
  25. package/dist/models.js.map +1 -1
  26. package/dist/player/audio-layers.d.ts +306 -0
  27. package/dist/player/audio-layers.d.ts.map +1 -0
  28. package/dist/player/audio-layers.js +1245 -0
  29. package/dist/player/audio-layers.js.map +1 -0
  30. package/dist/player/index.d.ts +13 -0
  31. package/dist/player/index.d.ts.map +1 -0
  32. package/dist/player/index.js +13 -0
  33. package/dist/player/index.js.map +1 -0
  34. package/dist/player/layer-signals.d.ts +80 -0
  35. package/dist/player/layer-signals.d.ts.map +1 -0
  36. package/dist/player/layer-signals.js +174 -0
  37. package/dist/player/layer-signals.js.map +1 -0
  38. package/dist/player/play-command.d.ts +55 -0
  39. package/dist/player/play-command.d.ts.map +1 -0
  40. package/dist/player/play-command.js +36 -0
  41. package/dist/player/play-command.js.map +1 -0
  42. package/dist/player/play-intent.d.ts +18 -0
  43. package/dist/player/play-intent.d.ts.map +1 -0
  44. package/dist/player/play-intent.js +26 -0
  45. package/dist/player/play-intent.js.map +1 -0
  46. package/dist/player/runtime.d.ts +105 -0
  47. package/dist/player/runtime.d.ts.map +1 -0
  48. package/dist/player/runtime.js +402 -0
  49. package/dist/player/runtime.js.map +1 -0
  50. package/dist/react/index.d.ts +1 -0
  51. package/dist/react/index.d.ts.map +1 -1
  52. package/dist/react/index.js +1 -0
  53. package/dist/react/index.js.map +1 -1
  54. package/dist/react/use-room-connection.d.ts +40 -0
  55. package/dist/react/use-room-connection.d.ts.map +1 -0
  56. package/dist/react/use-room-connection.js +85 -0
  57. package/dist/react/use-room-connection.js.map +1 -0
  58. package/dist/rooms/recorder.d.ts +4 -0
  59. package/dist/rooms/recorder.d.ts.map +1 -1
  60. package/dist/rooms/recorder.js +7 -0
  61. package/dist/rooms/recorder.js.map +1 -1
  62. package/dist/rooms/room.d.ts +78 -0
  63. package/dist/rooms/room.d.ts.map +1 -1
  64. package/dist/rooms/room.js +208 -3
  65. package/dist/rooms/room.js.map +1 -1
  66. package/dist/rooms/standing-source.d.ts +3 -0
  67. package/dist/rooms/standing-source.d.ts.map +1 -1
  68. package/dist/rooms/standing-source.js +6 -0
  69. package/dist/rooms/standing-source.js.map +1 -1
  70. package/dist/rooms/transcriber.d.ts +4 -0
  71. package/dist/rooms/transcriber.d.ts.map +1 -1
  72. package/dist/rooms/transcriber.js +1 -0
  73. package/dist/rooms/transcriber.js.map +1 -1
  74. package/dist/shaping/index.d.ts +1 -0
  75. package/dist/shaping/index.d.ts.map +1 -1
  76. package/dist/shaping/index.js +1 -0
  77. package/dist/shaping/index.js.map +1 -1
  78. package/dist/shaping/request.d.ts +1 -1
  79. package/dist/shaping/request.d.ts.map +1 -1
  80. package/dist/shaping/surveys.d.ts +69 -0
  81. package/dist/shaping/surveys.d.ts.map +1 -0
  82. package/dist/shaping/surveys.js +129 -0
  83. package/dist/shaping/surveys.js.map +1 -0
  84. package/dist/tools.d.ts +84 -0
  85. package/dist/tools.d.ts.map +1 -0
  86. package/dist/tools.js +105 -0
  87. package/dist/tools.js.map +1 -0
  88. package/dist/trace.d.ts +96 -0
  89. package/dist/trace.d.ts.map +1 -0
  90. package/dist/trace.js +251 -0
  91. package/dist/trace.js.map +1 -0
  92. package/dist/transcribe.d.ts +5 -0
  93. package/dist/transcribe.d.ts.map +1 -1
  94. package/dist/transcribe.js +1 -1
  95. package/dist/transcribe.js.map +1 -1
  96. package/package.json +5 -1
package/dist/atrium.js CHANGED
@@ -1,16 +1,20 @@
1
1
  /** The ``Atrium`` client — the entry point that owns auth, an authenticated transport, and the
2
- * phase-1 namespaces (``library`` / ``assets`` / ``sources`` / ``sessions`` / ``rooms``) plus the
3
- * ``chat`` and ``embed`` methods. */
2
+ * namespaces (``library`` / ``assets`` / ``sources`` / ``sessions`` / ``rooms`` / ``surveys``) plus
3
+ * the ``chat`` / ``agent`` / ``embed`` methods and the direct-model registry. */
4
4
  import { Auth } from "./auth.js";
5
5
  import { seedAssetId, toOperand } from "./by.js";
6
+ import { DirectEndpoint, DirectRegistry, directFetch, parseModelNames, } from "./direct.js";
6
7
  import { DEFAULT_BASE_URL, readEnv } from "./env.js";
7
- import { AuthError, InvalidArgumentError, raiseForStatus } from "./errors.js";
8
+ import { APIError, AtriumError, AuthError, InvalidArgumentError, raiseForStatus } from "./errors.js";
8
9
  import { httpFetch } from "./http.js";
9
- import { parseAsset, parseAssetPage, parseAssetVector, parseChatReply, parseCompareResult, parseEmbedding, parseIndexEntry, parseRoom, parseSearchByExamplesHit, parseSearchHit, parseSegment, parseSession, parseSessionEvent, parseSessionEventRow, parseSessionInfo, parseSourceInfo, parseSourceSearchResult, parseSpace, parseTagDefinition, parseTranscript, } from "./models.js";
10
+ import { parseAsset, parseAssetPage, parseAssetVector, parseChatReply, parseCompareResult, parseEmbedding, parseIndexEntry, parseRoom, parseSearchByExamplesHit, parseSearchHit, parseSegment, parseSession, parseSessionEvent, parseSessionEventRow, parseSessionInfo, parseSourceInfo, parseSourceSearchResult, parseSpace, parseSurvey, parseSurveyResponse, parseSurveyTemplate, parseTagDefinition, parseTranscript, } from "./models.js";
10
11
  import { Room } from "./rooms/index.js";
11
- import { buildAssetGet, buildAssetIndex, buildAssetList, buildAssetSegments, buildAssetVectors, buildBulkVectors, buildChat, buildCompare, buildEmbed, buildEnrich, buildRoomOpen, buildSearch, buildSearchByExamples, buildSessionEvent, buildSessionEvents, buildSessionExport, buildSessionExportBundle, buildSessionStart, buildSessionsList, buildSourceImport, buildSourcesSearch, buildTagGet, buildTagsList, buildTranscribe, buildUpload, chatBody, distinctExportIds, isZodSchema, JSON_OBJECT, repairMessages, resolveImportTarget, responseFormatParam, structuredError, tryParse, } from "./shaping/index.js";
12
+ import { sseFrames } from "./rooms/sse.js";
13
+ import { buildAssetGet, buildAssetIndex, buildAssetList, buildAssetSegments, buildAssetVectors, buildBulkVectors, buildChat, buildCompare, buildEmbed, buildEnrich, buildProjectFileUpload, buildRoomOpen, buildSearch, buildSearchByExamples, buildSessionEvent, buildSessionEvents, buildSessionExport, buildSessionExportBundle, buildSessionStart, buildSessionsList, buildSourceImport, buildSourcesSearch, buildSurveyResponses, buildSurveyStart, buildTagGet, buildTagsList, buildTemplateCreate, buildTemplateDefault, buildTemplateGet, buildTemplatesList, buildTemplateVersion, buildTranscribe, buildUpload, chatBody, distinctExportIds, isZodSchema, JSON_OBJECT, repairMessages, resolveImportTarget, responseFormatParam, sameJsonBody, structuredError, tryParse, } from "./shaping/index.js";
12
14
  import { buildRoomResolve } from "./shaping/rooms.js";
13
15
  import { PkceStore, TokenStore } from "./storage.js";
16
+ import { parseToolArguments, readToolCalls, toolSchema } from "./tools.js";
17
+ import { summarizeChat, summarizeEnrich, summarizeSearch, summarizeSearchByExamples, traced, } from "./trace.js";
14
18
  import { transcribeEngine } from "./transcribe.js";
15
19
  export { DEFAULT_BASE_URL };
16
20
  // Emitted at most once per page load: an API key in a browser is readable by anyone on the page.
@@ -67,6 +71,63 @@ function bytesToBase64(bytes) {
67
71
  }
68
72
  return btoa(binary);
69
73
  }
74
+ /** The OpenAI passthrough params of a {@link ChatOptions} — everything the SDK doesn't own itself. */
75
+ function stripChatOptions(opts) {
76
+ const params = { ...opts };
77
+ for (const key of ["model", "responseFormat", "strictParams", "signal", "stream"]) {
78
+ delete params[key];
79
+ }
80
+ return params;
81
+ }
82
+ /**
83
+ * Yield the content deltas of a streamed chat completion. The wire is SSE — ``data:`` frames of
84
+ * OpenAI chunk JSON, terminated by ``data: [DONE]`` — read through the room transport's
85
+ * {@link sseFrames} parser. A frame that isn't JSON is skipped (mirror of Python's tolerant loop).
86
+ */
87
+ async function* chatDeltas(res) {
88
+ if (!res.body) {
89
+ throw new AtriumError("chat(stream: true): this runtime's fetch returned no readable body — streaming needs one");
90
+ }
91
+ const reader = res.body.getReader();
92
+ try {
93
+ for await (const frame of sseFrames(reader)) {
94
+ const payload = frame.data.trim();
95
+ if (payload === "[DONE]") {
96
+ return;
97
+ }
98
+ let chunk;
99
+ try {
100
+ chunk = JSON.parse(payload);
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ const piece = chunk.choices?.[0]?.delta?.content;
106
+ if (typeof piece === "string" && piece) {
107
+ yield piece;
108
+ }
109
+ }
110
+ }
111
+ finally {
112
+ await reader.cancel().catch(() => { });
113
+ }
114
+ }
115
+ /** A tool's return value as the ``content`` of its tool message. Python sends ``str(result)``; JS's
116
+ * ``String({})`` is the useless ``"[object Object]"``, so an object is JSON-encoded instead. */
117
+ function toolResultContent(result) {
118
+ if (typeof result === "string") {
119
+ return result;
120
+ }
121
+ if (result !== null && typeof result === "object") {
122
+ try {
123
+ return JSON.stringify(result);
124
+ }
125
+ catch {
126
+ return String(result);
127
+ }
128
+ }
129
+ return String(result);
130
+ }
70
131
  /**
71
132
  * A client bound to one Atrium instance and one credential (an OAuth ``clientId`` or an ``apiKey``).
72
133
  *
@@ -85,11 +146,18 @@ export class Atrium {
85
146
  sources;
86
147
  sessions;
87
148
  rooms;
149
+ surveys;
150
+ /** Tracing sinks armed by ``room.trace()`` (shared, mutated by the room handle) — every traced
151
+ * library/model call mirrors into each. Empty until a room arms one: that emptiness IS the
152
+ * zero-cost-when-off fast path. */
153
+ traceSinks = [];
88
154
  apiKey;
89
155
  defaultProject;
90
156
  defaultCollection;
91
157
  lifecycle = new AbortController();
92
158
  spacesPromise = null;
159
+ /** Model name → OpenAI-compatible endpoint; consulted by ``chat`` / ``agent`` only. */
160
+ direct;
93
161
  constructor(opts = {}) {
94
162
  const baseUrl = (opts.baseUrl ?? readEnv("ATRIUM_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
95
163
  this.baseUrl = baseUrl;
@@ -97,6 +165,7 @@ export class Atrium {
97
165
  this.apiKey = opts.apiKey ?? readEnv("ATRIUM_API_KEY");
98
166
  this.defaultProject = opts.project;
99
167
  this.defaultCollection = opts.collection;
168
+ this.direct = new DirectRegistry(opts.directModels);
100
169
  if (this.apiKey && typeof window !== "undefined" && !browserKeyWarned) {
101
170
  browserKeyWarned = true;
102
171
  console.info("Atrium: this page carries an API key — fine for local research setups; " +
@@ -109,6 +178,7 @@ export class Atrium {
109
178
  this.sources = new Sources(this);
110
179
  this.sessions = new Sessions(this);
111
180
  this.rooms = new Rooms(this);
181
+ this.surveys = new Surveys(this);
112
182
  }
113
183
  /**
114
184
  * ``fetch`` against this instance with the signed-in user's bearer token attached, silent refresh,
@@ -125,50 +195,209 @@ export class Atrium {
125
195
  async close() {
126
196
  this.lifecycle.abort();
127
197
  }
128
- async chat(messages, opts = {}) {
129
- const { model, responseFormat, strictParams, signal, ...params } = opts;
130
- if (isZodSchema(responseFormat)) {
131
- return this.chatParse(messages, model, responseFormat, params, signal, strictParams);
198
+ // --- direct models -----------------------------------------------------------------------------
199
+ /**
200
+ * Route ``name`` straight to an OpenAI-compatible endpoint instead of Atrium.
201
+ *
202
+ * ``chat`` / ``agent`` calls naming ``name`` POST directly to ``baseUrl`` (bypassing Atrium, and
203
+ * carrying **only** that endpoint's own key — never the Atrium credential); everything else —
204
+ * embeddings, library search, sessions, rooms — still goes to Atrium. A registered name
205
+ * **shadows** an Atrium model of the same name (intentional, opt-in). Idempotent: re-registering
206
+ * a name overwrites its endpoint.
207
+ */
208
+ addDirectModel(name, endpoint) {
209
+ this.direct.register(name, new DirectEndpoint(endpoint));
210
+ }
211
+ /**
212
+ * Register several model names sharing one endpoint (see {@link addDirectModel}) — the common
213
+ * "one server, many names" case. Still explicit per name; no auto-enumeration.
214
+ */
215
+ addDirectModels(names, endpoint) {
216
+ const shared = new DirectEndpoint(endpoint);
217
+ for (const name of names) {
218
+ this.direct.register(name, shared);
132
219
  }
133
- const p = { ...params };
134
- if (responseFormat !== undefined) {
135
- p.response_format = responseFormat;
220
+ }
221
+ /** Stop routing ``name`` directly — subsequent calls go to Atrium (idempotent). */
222
+ removeDirectModel(name) {
223
+ this.direct.remove(name);
224
+ }
225
+ /** A snapshot of the registered direct models: name → {@link DirectEndpoint}. */
226
+ listDirectModels() {
227
+ return this.direct.list();
228
+ }
229
+ /**
230
+ * List the model names an OpenAI-compatible endpoint advertises (read-only ``GET {baseUrl}/models``).
231
+ *
232
+ * A helper for *choosing* names to register with {@link addDirectModel} — it **never** registers
233
+ * anything and doesn't touch the registry.
234
+ *
235
+ * **In the browser this is subject to CORS**: a local Ollama / LM Studio typically sends no
236
+ * ``Access-Control-Allow-Origin``, so the fetch is blocked before the SDK sees a response. That is
237
+ * the endpoint's policy to change (e.g. Ollama's ``OLLAMA_ORIGINS``), not something the SDK can
238
+ * work around — from Node it just works.
239
+ */
240
+ async discoverModels(baseUrl, opts = {}) {
241
+ const endpoint = new DirectEndpoint({ baseUrl, apiKey: opts.apiKey });
242
+ const { signal, dispose } = this.directSignal(endpoint, opts.signal);
243
+ try {
244
+ const res = await directFetch(endpoint, endpoint.modelsUrl, { method: "GET", signal });
245
+ return parseModelNames(await res.json());
136
246
  }
137
- if (strictParams) {
138
- p.strict_params = true;
247
+ finally {
248
+ dispose();
249
+ }
250
+ }
251
+ /** The abort signal for one direct call: the caller's, the client lifecycle, and the endpoint's
252
+ * own timeout (``AbortSignal.timeout`` — the stateless-``fetch`` stand-in for Python's per-endpoint
253
+ * transport timeout), folded into one. */
254
+ directSignal(endpoint, signal) {
255
+ const timeoutOf = AbortSignal.timeout;
256
+ const caller = linkSignals(signal, this.lifecycle.signal);
257
+ const timeout = timeoutOf ? timeoutOf(endpoint.timeout) : undefined;
258
+ const linked = linkSignals(caller.signal, timeout);
259
+ return {
260
+ signal: linked.signal,
261
+ dispose: () => {
262
+ linked.dispose();
263
+ caller.dispose();
264
+ },
265
+ };
266
+ }
267
+ /** POST a chat body to a direct endpoint (raw ``fetch``, never the authed transport). */
268
+ async directChat(endpoint, body, signal) {
269
+ const linked = this.directSignal(endpoint, signal);
270
+ try {
271
+ return await directFetch(endpoint, endpoint.chatUrl, {
272
+ method: "POST",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify(body),
275
+ signal: linked.signal,
276
+ });
277
+ }
278
+ finally {
279
+ linked.dispose();
139
280
  }
140
- const data = await this.request(buildChat(chatBody(messages, model, p)), signal);
141
- return parseChatReply(data);
142
281
  }
143
- async chatParse(messages, model, schema, params, signal, strictParams) {
282
+ chat(messages, opts = {}) {
283
+ if (opts.stream) {
284
+ if (isZodSchema(opts.responseFormat)) {
285
+ throw new InvalidArgumentError("chat(responseFormat: <Zod schema>) cannot be combined with stream: true — structured " +
286
+ "output validates a complete reply");
287
+ }
288
+ const chunks = this.chatStream(messages, opts);
289
+ // Python's decorator emits the trace when the generator is handed back (nothing is consumed
290
+ // yet), so a streamed call has no reply preview and a ~0 duration. Mirror that rather than
291
+ // deferring the emit until the stream drains — a `trace.chat` should appear when the call is
292
+ // made. Fire-and-forget: `traced` swallows its own failures and the fn resolves immediately.
293
+ void traced(this.traceSinks, "chat", { model: opts.model }, summarizeChat, async () => chunks);
294
+ return chunks;
295
+ }
296
+ // One logical call, one `trace.chat` — the structured path recurses through the UNTRACED
297
+ // `chatOnce` below, which is how TS gets Python's re-entrancy guard for free.
298
+ return traced(this.traceSinks, "chat", { model: opts.model }, summarizeChat, () => isZodSchema(opts.responseFormat)
299
+ ? this.chatParse(messages, opts.responseFormat, opts)
300
+ : this.chatOnce(messages, opts));
301
+ }
302
+ /** The chat body for one completion: the caller's passthrough params plus the SDK-owned keys. */
303
+ chatParams(opts) {
304
+ const p = stripChatOptions(opts);
305
+ if (opts.responseFormat !== undefined) {
306
+ p.response_format = opts.responseFormat;
307
+ }
308
+ if (opts.strictParams) {
309
+ p.strict_params = true;
310
+ }
311
+ return p;
312
+ }
313
+ // The plain (non-structured) completion — the one HTTP round-trip. Untraced on purpose: the public
314
+ // `chat` owns the trace, so a structured call's repair turn doesn't emit a second one.
315
+ async chatOnce(messages, opts = {}) {
316
+ const body = chatBody(messages, opts.model, this.chatParams(opts));
317
+ const endpoint = this.direct.resolve(opts.model);
318
+ if (endpoint) {
319
+ const res = await this.directChat(endpoint, body, opts.signal);
320
+ return parseChatReply(await res.json());
321
+ }
322
+ return parseChatReply(await this.request(buildChat(body), opts.signal));
323
+ }
324
+ /** Content deltas of a streamed completion (SSE, ``[DONE]``-terminated) — the Atrium route or,
325
+ * for a registered model, the direct endpoint. Mirror of Python's ``_chat_stream``. */
326
+ async *chatStream(messages, opts) {
327
+ const body = chatBody(messages, opts.model, { ...this.chatParams(opts), stream: true });
328
+ const endpoint = this.direct.resolve(opts.model);
329
+ const res = endpoint
330
+ ? await this.directChat(endpoint, body, opts.signal)
331
+ : await this.send(buildChat(body), opts.signal);
332
+ yield* chatDeltas(res);
333
+ }
334
+ async chatParse(messages, schema, opts) {
144
335
  const msgs = typeof messages === "string" ? [{ role: "user", content: messages }] : [...messages];
145
336
  const rf = await responseFormatParam(schema);
146
- const jsonSchema = rf.json_schema.schema;
147
- const first = (await this.chat(msgs, {
148
- model,
149
- responseFormat: rf,
150
- strictParams,
151
- signal,
152
- ...params,
153
- }));
154
- let [value, detail] = tryParse(schema, first.content);
337
+ const first = await this.chatOnce(msgs, { ...opts, responseFormat: rf });
338
+ return this.parseOrRepair(msgs, first, schema, opts);
339
+ }
340
+ /**
341
+ * Validate ``reply`` into ``schema``; on failure run ONE tool-free json-mode repair turn, then give
342
+ * up. Shared by the ``chat`` and ``agent`` structured paths so the repair policy lives in exactly
343
+ * one place (mirror of Python's ``_parse_or_repair``). ``opts`` carries the caller's model /
344
+ * strictness / passthrough params, so the repair turn inherits them.
345
+ */
346
+ async parseOrRepair(messages, reply, schema, opts) {
347
+ let [value, detail] = tryParse(schema, reply.content);
155
348
  if (value !== null) {
156
349
  return value;
157
350
  }
158
- const repaired = repairMessages(msgs, first.content, detail, jsonSchema);
159
- const second = (await this.chat(repaired, {
160
- model,
161
- responseFormat: JSON_OBJECT,
162
- strictParams,
163
- signal,
164
- ...params,
165
- }));
351
+ const rf = await responseFormatParam(schema);
352
+ const jsonSchema = rf.json_schema.schema;
353
+ const repaired = repairMessages(messages, reply.content, detail, jsonSchema);
354
+ const second = await this.chatOnce(repaired, { ...opts, responseFormat: JSON_OBJECT });
166
355
  [value, detail] = tryParse(schema, second.content);
167
356
  if (value !== null) {
168
357
  return value;
169
358
  }
170
359
  throw structuredError(schema.constructor.name, second.content, detail);
171
360
  }
361
+ async agent(prompt, opts) {
362
+ const { tools, model, maxTurns = 6, responseFormat, signal } = opts;
363
+ if (responseFormat !== undefined && !isZodSchema(responseFormat)) {
364
+ // Unlike chat(), a raw dict has nowhere sensible to go: json-mode conflicts with the
365
+ // tool-calling turns, so forwarding it would break the loop and ignoring it would silently do
366
+ // nothing. Say so instead.
367
+ throw new InvalidArgumentError("agent(responseFormat) takes a Zod schema (it structures the final answer); a json-mode " +
368
+ "dict isn't applied to the tool-calling turns");
369
+ }
370
+ const params = { ...opts };
371
+ for (const key of ["tools", "model", "maxTurns", "responseFormat", "signal"])
372
+ delete params[key];
373
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
374
+ const schemas = tools.map(toolSchema);
375
+ const messages = [{ role: "user", content: prompt }];
376
+ let reply = { content: "", model: "", raw: {} };
377
+ for (let turn = 0; turn < maxTurns; turn++) {
378
+ reply = await this.chat(messages, { model, tools: schemas, signal, ...params });
379
+ const choices = reply.raw.choices;
380
+ const message = (Array.isArray(choices) ? choices[0] : undefined);
381
+ const assistant = message?.message;
382
+ const calls = assistant ? readToolCalls(assistant) : [];
383
+ if (!calls.length) {
384
+ break;
385
+ }
386
+ messages.push(assistant);
387
+ for (const call of calls) {
388
+ const tool = byName.get(call.name);
389
+ const result = tool
390
+ ? await tool.execute(await parseToolArguments(tool, call.arguments))
391
+ : `error: unknown tool ${call.name}`;
392
+ messages.push({ role: "tool", tool_call_id: call.id, content: toolResultContent(result) });
393
+ }
394
+ }
395
+ if (isZodSchema(responseFormat)) {
396
+ // Structure the FINAL answer only — json-mode conflicts with the tool-calling turns.
397
+ return this.parseOrRepair(messages, reply, responseFormat, { model, signal, ...params });
398
+ }
399
+ return reply;
400
+ }
172
401
  /**
173
402
  * Embed text or a media file into a vector (``POST /v1/embeddings``). Pass exactly one of
174
403
  * ``text`` / ``image`` / ``audio`` / ``video``. ``model`` is required — an embedding's model is
@@ -457,8 +686,10 @@ export class Library {
457
686
  project,
458
687
  collection,
459
688
  });
460
- const data = await this.c.request(req, opts.signal);
461
- return (Array.isArray(data) ? data : []).map((d) => parseSearchHit(d, this.c.baseUrl, this.c.assetBinder));
689
+ return traced(this.c.traceSinks, "library.search", { query, like: opts.like }, summarizeSearch, async () => {
690
+ const data = await this.c.request(req, opts.signal);
691
+ return (Array.isArray(data) ? data : []).map((d) => parseSearchHit(d, this.c.baseUrl, this.c.assetBinder));
692
+ });
462
693
  }
463
694
  /** Find assets similar to a *set* of example assets (a mood board). */
464
695
  async searchByExamples(positives, opts = {}) {
@@ -479,8 +710,10 @@ export class Library {
479
710
  project,
480
711
  collection,
481
712
  });
482
- const data = await this.c.request(req, opts.signal);
483
- return (Array.isArray(data) ? data : []).map((d) => parseSearchByExamplesHit(d, this.c.baseUrl, this.c.assetBinder));
713
+ return traced(this.c.traceSinks, "library.search_by_examples", { positives, method: opts.method ?? "cav" }, summarizeSearchByExamples, async () => {
714
+ const data = await this.c.request(req, opts.signal);
715
+ return (Array.isArray(data) ? data : []).map((d) => parseSearchByExamplesHit(d, this.c.baseUrl, this.c.assetBinder));
716
+ });
484
717
  }
485
718
  /**
486
719
  * Compare two operands and return their similarity (raw + calibrated when available). Each operand
@@ -507,7 +740,7 @@ export class Library {
507
740
  preview: opts.preview,
508
741
  wait: opts.wait,
509
742
  });
510
- return (await this.c.request(req, opts.signal));
743
+ return traced(this.c.traceSinks, "library.enrich", { query, modality: opts.modality }, summarizeEnrich, async () => (await this.c.request(req, opts.signal)));
511
744
  }
512
745
  /**
513
746
  * Fetch stored embedding vectors for many assets at once (``POST /v1/library/vectors``,
@@ -744,6 +977,140 @@ export class Sessions {
744
977
  return parseSessionEvent(await this.c.request(req, opts.signal));
745
978
  }
746
979
  }
980
+ /**
981
+ * Survey templates + starting surveys.
982
+ *
983
+ * A **survey** is a data object on a session (``POST /v1/sessions/{id}/data``) guarded by a
984
+ * per-object capability token: whoever holds the token can read and write exactly that object,
985
+ * nothing else. {@link Surveys.start} creates it — pinning a template's *current* version, so later
986
+ * template edits never change a running survey — and returns a {@link Survey} carrying the once-only
987
+ * ``token`` and the hosted tool ``url``.
988
+ *
989
+ * **Templates** are project-scoped JSON documents with immutable, numbered versions. The server
990
+ * stores the body verbatim — the hosted survey tool's dialect is documented in
991
+ * ``docs/reference/survey-template-dialect.md``. {@link Surveys.template} is the code-first "ensure"
992
+ * flow: safe to rerun on every start-up, appending a new version only when the body actually changed.
993
+ */
994
+ export class Surveys {
995
+ c;
996
+ constructor(c) {
997
+ this.c = c;
998
+ }
999
+ /** The project ref for a ``/v1/projects/{ref}/…`` path — the per-call value or the client's
1000
+ * default scope, passed through **verbatim** (the survey/file routers accept a UUID or a short_id;
1001
+ * resolving a short_id here would issue a projects read a project-scoped key is denied). */
1002
+ projectRef(project) {
1003
+ const { project: ref } = this.c.scope(project, undefined);
1004
+ if (ref === undefined) {
1005
+ throw new InvalidArgumentError("surveys: no project in scope — pass project, or construct the client with one");
1006
+ }
1007
+ return ref;
1008
+ }
1009
+ /**
1010
+ * Start a survey on a session: creates the data object (pinning the template's current version —
1011
+ * or the project's DEFAULT template when ``template`` is omitted) and returns the handle with its
1012
+ * once-only token.
1013
+ */
1014
+ async start(template, opts) {
1015
+ const sessionId = typeof opts.into === "string" ? opts.into : opts.into.id;
1016
+ const req = buildSurveyStart(sessionId, {
1017
+ name: opts.name ?? template ?? "survey",
1018
+ template,
1019
+ useDefault: template === undefined,
1020
+ returnTo: opts.returnTo,
1021
+ });
1022
+ return parseSurvey(await this.c.request(req, opts.signal));
1023
+ }
1024
+ /**
1025
+ * Ensure a template exists with exactly this body (idempotent, code-first).
1026
+ *
1027
+ * Create it if missing; if it exists and the CURRENT version's body differs, append a new
1028
+ * immutable version; flip the default via PATCH when requested and not already set. Returns
1029
+ * ``{name, version, isDefault}``. Safe to rerun on every start-up.
1030
+ */
1031
+ async template(name, body, opts = {}) {
1032
+ const project = this.projectRef(opts.project);
1033
+ const wantDefault = opts.default ?? false;
1034
+ let conflict;
1035
+ try {
1036
+ const created = (await this.c.request(buildTemplateCreate(project, { name, body, isDefault: wantDefault }), opts.signal));
1037
+ return { name: created.name, version: created.version, isDefault: created.is_default };
1038
+ }
1039
+ catch (err) {
1040
+ if (!(err instanceof APIError) || err.status !== 409) {
1041
+ throw err; // only a conflict flows into the ensure path
1042
+ }
1043
+ conflict = err;
1044
+ }
1045
+ let detail;
1046
+ try {
1047
+ detail = (await this.c.request(buildTemplateGet(project, name), opts.signal));
1048
+ }
1049
+ catch (err) {
1050
+ // The create 409 wasn't "name exists" (a default-flip race creates nothing) — the original
1051
+ // conflict is the real story, not a "template not found".
1052
+ throw err instanceof APIError && err.status === 404 ? conflict : err;
1053
+ }
1054
+ if (!sameJsonBody(body, detail.body)) {
1055
+ detail = (await this.c.request(buildTemplateVersion(project, name, body), opts.signal));
1056
+ }
1057
+ let isDefault = Boolean(detail.is_default);
1058
+ if (wantDefault && !isDefault) {
1059
+ const patched = (await this.c.request(buildTemplateDefault(project, name), opts.signal));
1060
+ isDefault = Boolean(patched.is_default);
1061
+ }
1062
+ return { name, version: detail.version, isDefault };
1063
+ }
1064
+ /** List the project's survey templates (``GET …/survey-templates``). */
1065
+ async templates(opts = {}) {
1066
+ const req = buildTemplatesList(this.projectRef(opts.project));
1067
+ const data = await this.c.request(req, opts.signal);
1068
+ return (Array.isArray(data) ? data : []).map(parseSurveyTemplate);
1069
+ }
1070
+ /**
1071
+ * A survey template's responses across the project's sessions, newest first. ``template`` is the
1072
+ * template **name** (not a started {@link Survey}), resolved with ``project`` exactly like
1073
+ * {@link template} / {@link templates}. For the CSV export use {@link responsesCsv}.
1074
+ */
1075
+ async responses(template, opts = {}) {
1076
+ const req = buildSurveyResponses(this.projectRef(opts.project), template, {
1077
+ limit: opts.limit ?? 200,
1078
+ submittedOnly: opts.submittedOnly,
1079
+ phase: opts.phase,
1080
+ });
1081
+ const data = await this.c.request(req, opts.signal);
1082
+ return (Array.isArray(data) ? data : []).map(parseSurveyResponse);
1083
+ }
1084
+ /**
1085
+ * The same responses as the server's CSV export (one row per response, question-id columns),
1086
+ * returned as raw bytes. A separate method rather than a ``format`` union on {@link responses} —
1087
+ * the return types have nothing in common, exactly like ``sessions.export``. The SDK never touches
1088
+ * the filesystem, so a Node caller writes the bytes itself.
1089
+ */
1090
+ async responsesCsv(template, opts = {}) {
1091
+ const req = buildSurveyResponses(this.projectRef(opts.project), template, { limit: opts.limit ?? 200, submittedOnly: opts.submittedOnly, phase: opts.phase }, true);
1092
+ return this.c.requestBytes(req, opts.signal);
1093
+ }
1094
+ /**
1095
+ * Upload a write-once project file (survey imagery) — ``file`` is a ``Blob``/``File`` or a URL
1096
+ * string, never a filesystem path (the core is browser-safe). Returns the file's ``id`` — which a
1097
+ * template body's ``image`` field references — plus its public content ``url``.
1098
+ */
1099
+ async uploadFile(file, opts = {}) {
1100
+ const req = buildProjectFileUpload(this.projectRef(opts.project), {
1101
+ file,
1102
+ filename: opts.filename,
1103
+ role: opts.role ?? "attachment",
1104
+ });
1105
+ const data = (await this.c.request(req, opts.signal));
1106
+ return {
1107
+ id: String(data.id),
1108
+ url: `${this.c.baseUrl}/v1/project-files/${data.id}/content`,
1109
+ filename: data.filename ?? null,
1110
+ contentType: data.content_type ?? null,
1111
+ };
1112
+ }
1113
+ }
747
1114
  /** Open / fetch / join rooms — each returns a live {@link Room} handle (connect, send, on, request,
748
1115
  * player controls, audio signals). ``connect()`` (called for you by any wire method) opens the socket. */
749
1116
  export class Rooms {
@@ -769,6 +1136,10 @@ export class Rooms {
769
1136
  library: this.c.library,
770
1137
  request: (req) => this.c.request(req),
771
1138
  transcribeStream: (clips, o) => this.c.transcribeStream(clips, o),
1139
+ transcribe: (audio, o) => this.c.transcribe(audio, o),
1140
+ // The client's shared sink list — `room.trace()` registers on it, so the client's own
1141
+ // library/model calls mirror into this room.
1142
+ traceSinks: this.c.traceSinks,
772
1143
  },
773
1144
  identity: live.identity,
774
1145
  announce: live.announce,