langwatch 1.19.0 → 2.0.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 (91) hide show
  1. package/README.md +27 -13
  2. package/dist/agent/index.d.mts +122 -79
  3. package/dist/agent/index.d.ts +122 -79
  4. package/dist/agent/index.js +541 -301
  5. package/dist/agent/index.js.map +1 -1
  6. package/dist/agent/index.mjs +540 -302
  7. package/dist/agent/index.mjs.map +1 -1
  8. package/dist/{chunk-NPEK4RSD.mjs → chunk-2GKEMCKT.mjs} +7757 -2409
  9. package/dist/chunk-2GKEMCKT.mjs.map +1 -0
  10. package/dist/chunk-5VYEG3RS.js +117 -0
  11. package/dist/chunk-5VYEG3RS.js.map +1 -0
  12. package/dist/chunk-AYPQNLCC.mjs +852 -0
  13. package/dist/chunk-AYPQNLCC.mjs.map +1 -0
  14. package/dist/chunk-BL3GHEKF.js +59 -0
  15. package/dist/chunk-BL3GHEKF.js.map +1 -0
  16. package/dist/chunk-C7IWJ2GJ.mjs +59 -0
  17. package/dist/chunk-C7IWJ2GJ.mjs.map +1 -0
  18. package/dist/chunk-DLZCOUXC.js +852 -0
  19. package/dist/chunk-DLZCOUXC.js.map +1 -0
  20. package/dist/{chunk-O4J3ZPZY.js → chunk-F7BVJW3S.js} +7912 -2564
  21. package/dist/chunk-F7BVJW3S.js.map +1 -0
  22. package/dist/{chunk-SY5HSL5G.mjs → chunk-M3DSPR5S.mjs} +124 -112
  23. package/dist/chunk-M3DSPR5S.mjs.map +1 -0
  24. package/dist/{chunk-CSV7B7B5.js → chunk-MAO3SIBB.js} +141 -129
  25. package/dist/chunk-MAO3SIBB.js.map +1 -0
  26. package/dist/{chunk-LCG5Y2EG.js → chunk-N76BG37W.js} +15 -101
  27. package/dist/chunk-N76BG37W.js.map +1 -0
  28. package/dist/chunk-SERULD4Z.mjs +117 -0
  29. package/dist/chunk-SERULD4Z.mjs.map +1 -0
  30. package/dist/{chunk-73QOHSHU.js → chunk-WDUUAIG3.js} +2 -2
  31. package/dist/chunk-WDUUAIG3.js.map +1 -0
  32. package/dist/{chunk-WMDQVWA6.mjs → chunk-WSGVZ3J2.mjs} +22 -108
  33. package/dist/chunk-WSGVZ3J2.mjs.map +1 -0
  34. package/dist/{chunk-57L2K66L.mjs → chunk-XQ4CP2JK.mjs} +2 -2
  35. package/dist/chunk-XQ4CP2JK.mjs.map +1 -0
  36. package/dist/cli/bundle.js +853 -552
  37. package/dist/implementation-BXVPzwOt.d.mts +261 -0
  38. package/dist/implementation-DJ-JveZl.d.ts +261 -0
  39. package/dist/index.d.mts +479 -1130
  40. package/dist/index.d.ts +479 -1130
  41. package/dist/index.js +7908 -3525
  42. package/dist/index.js.map +1 -1
  43. package/dist/index.mjs +7883 -3500
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/observability-sdk/index.d.mts +16 -76
  46. package/dist/observability-sdk/index.d.ts +16 -76
  47. package/dist/observability-sdk/index.js +8 -5
  48. package/dist/observability-sdk/index.js.map +1 -1
  49. package/dist/observability-sdk/index.mjs +13 -10
  50. package/dist/observability-sdk/instrumentation/langchain/index.d.mts +3 -5
  51. package/dist/observability-sdk/instrumentation/langchain/index.d.ts +3 -5
  52. package/dist/observability-sdk/instrumentation/langchain/index.js +160 -112
  53. package/dist/observability-sdk/instrumentation/langchain/index.js.map +1 -1
  54. package/dist/observability-sdk/instrumentation/langchain/index.mjs +148 -100
  55. package/dist/observability-sdk/instrumentation/langchain/index.mjs.map +1 -1
  56. package/dist/observability-sdk/semconv/index.d.mts +37 -0
  57. package/dist/observability-sdk/semconv/index.d.ts +37 -0
  58. package/dist/observability-sdk/semconv/index.js +121 -0
  59. package/dist/observability-sdk/semconv/index.js.map +1 -0
  60. package/dist/observability-sdk/semconv/index.mjs +121 -0
  61. package/dist/observability-sdk/semconv/index.mjs.map +1 -0
  62. package/dist/observability-sdk/setup/node/index.d.mts +73 -313
  63. package/dist/observability-sdk/setup/node/index.d.ts +73 -313
  64. package/dist/observability-sdk/setup/node/index.js +6 -4
  65. package/dist/observability-sdk/setup/node/index.js.map +1 -1
  66. package/dist/observability-sdk/setup/node/index.mjs +5 -3
  67. package/dist/types--ExXvd26.d.mts +16583 -0
  68. package/dist/{types-VOZv9LYO.d.ts → types-CoS5PmeU.d.mts} +22 -59
  69. package/dist/{types-VOZv9LYO.d.mts → types-CoS5PmeU.d.ts} +22 -59
  70. package/dist/types-CpT3iEIj.d.ts +16583 -0
  71. package/dist/{types-DLfhJb38.d.mts → types-RkiBQZ9j.d.mts} +1 -6
  72. package/dist/{types-DLfhJb38.d.ts → types-RkiBQZ9j.d.ts} +1 -6
  73. package/dist/values-1yj9l0Sa.d.mts +112 -0
  74. package/dist/values-1yj9l0Sa.d.ts +112 -0
  75. package/package.json +94 -64
  76. package/dist/chunk-57L2K66L.mjs.map +0 -1
  77. package/dist/chunk-73QOHSHU.js.map +0 -1
  78. package/dist/chunk-CSV7B7B5.js.map +0 -1
  79. package/dist/chunk-LCG5Y2EG.js.map +0 -1
  80. package/dist/chunk-NPEK4RSD.mjs.map +0 -1
  81. package/dist/chunk-O4J3ZPZY.js.map +0 -1
  82. package/dist/chunk-PIYOJF6V.mjs +0 -566
  83. package/dist/chunk-PIYOJF6V.mjs.map +0 -1
  84. package/dist/chunk-SY5HSL5G.mjs.map +0 -1
  85. package/dist/chunk-WGLVIV4J.js +0 -566
  86. package/dist/chunk-WGLVIV4J.js.map +0 -1
  87. package/dist/chunk-WMDQVWA6.mjs.map +0 -1
  88. package/dist/implementation-CeTvnq6e.d.mts +0 -716
  89. package/dist/implementation-CsQrexpY.d.ts +0 -716
  90. package/dist/types-ofe1fAYQ.d.mts +0 -38143
  91. package/dist/types-wuYJPoFb.d.ts +0 -38143
@@ -1,4 +1,4 @@
1
- // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.26_tsx@4.22.4_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/esm_shims.js
1
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.26_supports-color@10.2.2_tsx@4.22.4_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/esm_shims.js
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  var getFilename = () => fileURLToPath(import.meta.url);
@@ -40,14 +40,16 @@ var ConsoleLogger = class {
40
40
  import { context, propagation, trace } from "@opentelemetry/api";
41
41
 
42
42
  // src/agent/identity.ts
43
- import * as os from "os";
44
43
  import { randomUUID } from "crypto";
44
+ import * as os from "os";
45
45
 
46
46
  // package.json
47
- var version = "1.19.0";
47
+ var version = "2.0.0";
48
48
 
49
49
  // src/internal/constants.ts
50
50
  var LANGWATCH_SDK_VERSION = version;
51
+ var API_VERSION_HEADER = "X-API-Version";
52
+ var LANGWATCH_API_VERSION = "2026-10-26";
51
53
  var DEFAULT_ENDPOINT = "https://app.langwatch.ai";
52
54
 
53
55
  // src/internal/endpoint.ts
@@ -102,7 +104,9 @@ function resolveEnabled({
102
104
  env = process.env
103
105
  }) {
104
106
  const flag = env.LANGWATCH_AGENT_CONNECT;
105
- if (isSet2(flag) && !isTruthy(flag)) return false;
107
+ if (isSet2(flag)) {
108
+ if (!isTruthy(flag)) return false;
109
+ }
106
110
  if (explicit !== void 0) return explicit;
107
111
  return !isTruthy(env.CI);
108
112
  }
@@ -185,24 +189,37 @@ var isStringList = (value) => Array.isArray(value) && value.every(isString);
185
189
  var readScope = (value) => {
186
190
  if (!isRecord(value)) return { kind: "shared" };
187
191
  if (value.kind === "owner") return { kind: "owner" };
188
- if (value.kind === "host" && isString(value.hostLabel)) return { kind: "host", hostLabel: value.hostLabel };
192
+ if (value.kind === "host" && isString(value.hostLabel))
193
+ return { kind: "host", hostLabel: value.hostLabel };
189
194
  return { kind: "shared" };
190
195
  };
196
+ var readRegisteredAgent = (entry) => {
197
+ if (!isRecord(entry)) return null;
198
+ if (!isString(entry.name)) return null;
199
+ let id = null;
200
+ if (isString(entry.id)) {
201
+ id = entry.id;
202
+ } else if (isString(entry.agentId)) {
203
+ id = entry.agentId;
204
+ }
205
+ if (id === null) return null;
206
+ return {
207
+ name: entry.name,
208
+ environment: isString(entry.environment) ? entry.environment : "",
209
+ id,
210
+ url: isString(entry.url) ? entry.url : "",
211
+ parameterNotes: isStringList(entry.parameterNotes) ? entry.parameterNotes : [],
212
+ scope: readScope(entry.scope)
213
+ };
214
+ };
191
215
  var readRegistered = (frame) => {
192
- if (!Array.isArray(frame.agents) || !isString(frame.instanceId)) return null;
216
+ if (!Array.isArray(frame.agents)) return null;
217
+ if (!isString(frame.instanceId)) return null;
193
218
  const agents = [];
194
219
  for (const entry of frame.agents) {
195
- if (!isRecord(entry) || !isString(entry.name)) return null;
196
- const id = isString(entry.id) ? entry.id : isString(entry.agentId) ? entry.agentId : null;
197
- if (id === null) return null;
198
- agents.push({
199
- name: entry.name,
200
- environment: isString(entry.environment) ? entry.environment : "",
201
- id,
202
- url: isString(entry.url) ? entry.url : "",
203
- parameterNotes: isStringList(entry.parameterNotes) ? entry.parameterNotes : [],
204
- scope: readScope(entry.scope)
205
- });
220
+ const agent = readRegisteredAgent(entry);
221
+ if (agent === null) return null;
222
+ agents.push(agent);
206
223
  }
207
224
  return {
208
225
  type: "registered",
@@ -238,12 +255,15 @@ var readDeadline = (value) => {
238
255
  return null;
239
256
  };
240
257
  var readCall = (frame) => {
241
- if (!isString(frame.callId) || !isString(frame.agentId)) return null;
258
+ if (!isString(frame.callId)) return null;
259
+ if (!isString(frame.agentId)) return null;
242
260
  const messages = isMessageList(frame.messages) ? frame.messages : [];
243
261
  const run = isRecord(frame.run) ? frame.run : {};
244
262
  return {
245
263
  type: "call",
246
264
  protocol: typeof frame.protocol === "number" ? frame.protocol : PROTOCOL_VERSION,
265
+ // A platform that predates hooks sends no kind: every call is a turn.
266
+ kind: isString(frame.kind) ? frame.kind : "turn",
247
267
  callId: frame.callId,
248
268
  agentId: frame.agentId,
249
269
  threadId: isString(frame.threadId) ? frame.threadId : "",
@@ -251,6 +271,7 @@ var readCall = (frame) => {
251
271
  newMessages: isMessageList(frame.newMessages) ? frame.newMessages : messages,
252
272
  params: readParams(frame.params),
253
273
  session: frame.session === void 0 ? null : frame.session,
274
+ state: isRecord(frame.state) ? frame.state : null,
254
275
  traceparent: isString(frame.traceparent) ? frame.traceparent : null,
255
276
  deadlineAt: readDeadline(frame.deadlineAt),
256
277
  run: {
@@ -267,7 +288,8 @@ function parseServerFrame(raw) {
267
288
  } catch {
268
289
  return null;
269
290
  }
270
- if (!isRecord(parsed) || !isString(parsed.type)) return null;
291
+ if (!isRecord(parsed)) return null;
292
+ if (!isString(parsed.type)) return null;
271
293
  switch (parsed.type) {
272
294
  case "registered":
273
295
  return readRegistered(parsed);
@@ -296,164 +318,6 @@ function traceIdFromTraceparent(traceparent) {
296
318
  return traceId.toLowerCase();
297
319
  }
298
320
 
299
- // src/agent/schema.ts
300
- var AgentParameterError = class extends Error {
301
- constructor(message) {
302
- super(message);
303
- this.code = "agent_parameter_invalid";
304
- this.name = "AgentParameterError";
305
- }
306
- };
307
- var ACCEPTED_FORMS = `parameters must be a definition map ({ model: { options: [...], default: '...' } }), a Standard JSON Schema object (one with "~standard".jsonSchema), or a JSON Schema object ({ type: 'object', properties })`;
308
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
309
- var isStandardJsonSchema = (value) => {
310
- if (!isRecord2(value)) return false;
311
- const standard = value["~standard"];
312
- return isRecord2(standard) && isRecord2(standard.jsonSchema);
313
- };
314
- var isJsonSchemaObject = (value) => isRecord2(value) && value.type === "object" && isRecord2(value.properties);
315
- var isDefinitionMap = (value) => isRecord2(value) && Object.values(value).every(isRecord2);
316
- var definitionType = (definition) => {
317
- if (definition.type) return definition.type;
318
- if (definition.options) return "string";
319
- const value = definition.default;
320
- if (typeof value === "number") return "number";
321
- if (typeof value === "boolean") return "boolean";
322
- return "string";
323
- };
324
- var definitionMapToSchema = (definitions) => {
325
- const properties = {};
326
- const required = [];
327
- for (const [name, definition] of Object.entries(definitions)) {
328
- const property = { type: definitionType(definition) };
329
- if (definition.options) property.enum = [...definition.options];
330
- if (definition.default !== void 0) property.default = definition.default;
331
- else required.push(name);
332
- if (definition.description !== void 0) property.description = definition.description;
333
- properties[name] = property;
334
- }
335
- const schema = { type: "object", properties };
336
- if (required.length > 0) schema.required = required;
337
- return schema;
338
- };
339
- var readStandardJsonSchema = (input) => {
340
- const converter = input["~standard"].jsonSchema;
341
- const options = { target: "draft-2020-12" };
342
- const schema = converter.input?.(options) ?? converter.output?.(options);
343
- if (schema === void 0) {
344
- throw new AgentParameterError(`the "~standard".jsonSchema converter has no input function; ${ACCEPTED_FORMS}`);
345
- }
346
- if (!isRecord2(schema)) {
347
- throw new AgentParameterError(`the "~standard".jsonSchema converter returned no object; ${ACCEPTED_FORMS}`);
348
- }
349
- return schema;
350
- };
351
- function toParameterSchema(input) {
352
- if (input === void 0) return { type: "object", properties: {} };
353
- if (isStandardJsonSchema(input)) return readStandardJsonSchema(input);
354
- if (isJsonSchemaObject(input)) return input;
355
- if (isDefinitionMap(input)) return definitionMapToSchema(input);
356
- throw new AgentParameterError(ACCEPTED_FORMS);
357
- }
358
- var specType = (property) => {
359
- const type = Array.isArray(property.type) ? property.type.find((item) => item !== "null") : property.type;
360
- if (type === "number" || type === "integer") return "number";
361
- if (type === "boolean") return "boolean";
362
- return "string";
363
- };
364
- var scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : void 0;
365
- function parameterSpecsFromSchema(schema) {
366
- const properties = isRecord2(schema.properties) ? schema.properties : {};
367
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
368
- const specs = [];
369
- for (const [name, raw] of Object.entries(properties)) {
370
- const property = isRecord2(raw) ? raw : {};
371
- const spec = { name, type: specType(property) };
372
- const options = Array.isArray(property.enum) ? property.enum.filter((item) => typeof item === "string") : void 0;
373
- if (options && options.length > 0) spec.options = options;
374
- const fallback = scalar(property.default);
375
- if (fallback !== void 0) spec.default = fallback;
376
- if (typeof property.description === "string") spec.description = property.description;
377
- spec.required = fallback === void 0 && required.has(name);
378
- specs.push(spec);
379
- }
380
- return specs;
381
- }
382
- var coerce = ({
383
- spec,
384
- value
385
- }) => {
386
- if (spec.type === "number") {
387
- const asNumber = typeof value === "number" ? value : Number(value);
388
- if (typeof value === "boolean" || !Number.isFinite(asNumber) || String(value).trim() === "") {
389
- throw new AgentParameterError(`parameter "${spec.name}" must be a number, got ${JSON.stringify(value)}`);
390
- }
391
- return asNumber;
392
- }
393
- if (spec.type === "boolean") {
394
- if (typeof value === "boolean") return value;
395
- if (value === "true") return true;
396
- if (value === "false") return false;
397
- throw new AgentParameterError(`parameter "${spec.name}" must be true or false, got ${JSON.stringify(value)}`);
398
- }
399
- const asString = typeof value === "string" ? value : String(value);
400
- if (spec.options && !spec.options.includes(asString)) {
401
- throw new AgentParameterError(
402
- `parameter "${spec.name}" must be one of ${spec.options.join(", ")}, got ${JSON.stringify(value)}`
403
- );
404
- }
405
- return asString;
406
- };
407
- function resolveParameterValues({
408
- specs,
409
- supplied
410
- }) {
411
- const values = { ...supplied ?? {} };
412
- for (const spec of specs) {
413
- const value = values[spec.name];
414
- if (value === void 0) {
415
- if (spec.default !== void 0) {
416
- values[spec.name] = spec.default;
417
- continue;
418
- }
419
- if (!spec.required) continue;
420
- throw new AgentParameterError(`parameter "${spec.name}" is required and the run did not supply it`);
421
- }
422
- values[spec.name] = coerce({ spec, value });
423
- }
424
- return values;
425
- }
426
- var issuePath = (issue) => (issue.path ?? []).map(
427
- (segment) => typeof segment === "object" && segment !== null ? String(segment.key) : String(segment)
428
- ).join(".");
429
- async function validateParameterValues({
430
- schema,
431
- values
432
- }) {
433
- const standard = schema["~standard"];
434
- const result = await standard.validate?.(values);
435
- if (result === void 0) return values;
436
- if (result.issues) {
437
- const detail = result.issues.map((issue) => {
438
- const path2 = issuePath(issue);
439
- return path2 ? `${path2}: ${issue.message}` : issue.message;
440
- }).join("; ");
441
- throw new AgentParameterError(`parameters refused by the schema: ${detail}`);
442
- }
443
- const parsed = isRecord2(result.value) ? result.value : {};
444
- return { ...values, ...parsed };
445
- }
446
- function createParameterReader({
447
- input,
448
- specs
449
- }) {
450
- const schema = isStandardJsonSchema(input) ? input : void 0;
451
- return async (supplied) => {
452
- const values = resolveParameterValues({ specs, supplied });
453
- return schema ? validateParameterValues({ schema, values }) : values;
454
- };
455
- }
456
-
457
321
  // src/agent/transport.ts
458
322
  import { createRequire } from "module";
459
323
 
@@ -470,11 +334,7 @@ var CREDENTIAL_HEADERS = [
470
334
  "x-project-id"
471
335
  ];
472
336
  var LangWatchRedirectError = class extends Error {
473
- constructor({
474
- url,
475
- location,
476
- status
477
- }) {
337
+ constructor({ url, location, status }) {
478
338
  super(
479
339
  `LangWatch refused to follow a redirect from ${url} to ${location ?? "an unknown location"} (HTTP ${status}). Set the endpoint to the final URL.`
480
340
  );
@@ -484,6 +344,20 @@ var LangWatchRedirectError = class extends Error {
484
344
  this.status = status;
485
345
  }
486
346
  };
347
+ var isPlainHeaders = (headers) => !(headers instanceof Headers) && !Array.isArray(headers);
348
+ var withApiVersion = ({
349
+ input,
350
+ init
351
+ }) => {
352
+ const sent = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
353
+ if (new Headers(sent).has(API_VERSION_HEADER)) return { ...init };
354
+ if (sent === void 0 || isPlainHeaders(sent)) {
355
+ return { ...init, headers: { ...sent, [API_VERSION_HEADER]: LANGWATCH_API_VERSION } };
356
+ }
357
+ const headers = new Headers(sent);
358
+ headers.set(API_VERSION_HEADER, LANGWATCH_API_VERSION);
359
+ return { ...init, headers };
360
+ };
487
361
  var warnedOrigins = /* @__PURE__ */ new Set();
488
362
  var isRedirect = (response) => response.type === "opaqueredirect" || REDIRECT_STATUSES.has(response.status);
489
363
  var isStream = (body) => typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
@@ -621,7 +495,19 @@ var upgrade = async ({
621
495
  const location = first.headers.get("location");
622
496
  const refused = refusalOf({ url, response: first });
623
497
  const target = location === null ? null : schemeUpgradeTarget({ url, location });
624
- if (location === null || first.status === 303 || target === null || isStream(init?.body)) {
498
+ if (location === null) {
499
+ discard(spare);
500
+ throw refused;
501
+ }
502
+ if (first.status === 303) {
503
+ discard(spare);
504
+ throw refused;
505
+ }
506
+ if (target === null) {
507
+ discard(spare);
508
+ throw refused;
509
+ }
510
+ if (isStream(init?.body)) {
625
511
  discard(spare);
626
512
  throw refused;
627
513
  }
@@ -644,7 +530,8 @@ var createLangWatchFetch = ({
644
530
  } = {}) => {
645
531
  const send = fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
646
532
  const log = logger ?? new ConsoleLogger({ level: "warn", prefix: "LangWatch" });
647
- return async (input, init) => {
533
+ return async (input, given) => {
534
+ const init = withApiVersion({ input, init: given });
648
535
  const url = requestUrl(input);
649
536
  const effective = effectiveRequest({ input, init });
650
537
  const method = (effective?.method ?? init?.method ?? "GET").toUpperCase();
@@ -671,7 +558,10 @@ function resolveTransport({
671
558
  env = process.env
672
559
  }) {
673
560
  const candidate = isSet3(explicit) ? explicit : env.LANGWATCH_AGENT_TRANSPORT;
674
- return isSet3(candidate) && candidate.trim().toLowerCase() === "http" ? "http" : "websocket";
561
+ if (!isSet3(candidate)) return "websocket";
562
+ const normalized = candidate.trim().toLowerCase();
563
+ if (normalized === "http") return "http";
564
+ return "websocket";
675
565
  }
676
566
  var NoWebSocketError = class extends Error {
677
567
  constructor() {
@@ -768,14 +658,14 @@ var HttpLongPollSocket = class {
768
658
  return;
769
659
  }
770
660
  if (frame.type === "ack" && typeof frame.callId === "string") this.inFlight.add(frame.callId);
771
- if (frame.type === "result" && typeof frame.callId === "string") this.inFlight.delete(frame.callId);
661
+ if (frame.type === "result" && typeof frame.callId === "string")
662
+ this.inFlight.delete(frame.callId);
772
663
  this.outbox = this.outbox.then(() => this.registered).then(() => this.post(data)).catch(() => void 0);
773
664
  }
774
665
  /**
775
- * Stops polling, lets the frames already queued go out, then reports the
776
- * close. The wait is bounded: on the deadline the frame requests are aborted
777
- * and the close is reported anyway, so a request that never answers cannot
778
- * hold the connection open.
666
+ * Stops polling, lets queued frames go out, then reports close. Bounded: on
667
+ * the deadline the frame requests are aborted and close is reported anyway,
668
+ * so an unanswering request cannot hold the connection open.
779
669
  */
780
670
  close(code = 1e3) {
781
671
  this.closed = true;
@@ -830,7 +720,8 @@ var HttpLongPollSocket = class {
830
720
  signal: this.polls.signal
831
721
  });
832
722
  } catch (error) {
833
- if (!this.closed) this.fail(`could not reach ${this.url}/register (${describe(error)})`, 1006);
723
+ if (!this.closed)
724
+ this.fail(`could not reach ${this.url}/register (${describe(error)})`, 1006);
834
725
  return;
835
726
  }
836
727
  const body = await this.jsonOf(response);
@@ -851,46 +742,66 @@ var HttpLongPollSocket = class {
851
742
  }
852
743
  async pollLoop() {
853
744
  while (!this.closed && this.token) {
854
- const query = this.inFlight.size > 0 ? `?inFlight=${encodeURIComponent([...this.inFlight].join(","))}` : "";
855
- const startedAt = Date.now();
856
- let response;
857
- try {
858
- response = await this.fetchImpl(`${this.url}/poll${query}`, {
859
- method: "GET",
860
- headers: this.requestHeaders(),
861
- signal: this.polls.signal
862
- });
863
- } catch (error) {
864
- if (!this.closed) this.fail(`the poll failed (${describe(error)})`, 1006);
865
- return;
866
- }
867
- if (this.closed) return;
868
- if (response.status === 410) {
869
- this.fail("the platform no longer knows this instance, registering again", SESSION_LOST_CLOSE_CODE);
870
- return;
871
- }
872
- const body = await this.jsonOf(response);
873
- if (!response.ok) {
874
- const answered = body && typeof body.frame === "object" && body.frame !== null ? body.frame : null;
875
- if (answered) this.emitMessage(JSON.stringify(answered));
876
- if (answered?.type === "refused") {
877
- return;
878
- }
879
- this.fail(`the poll was answered with HTTP ${response.status}`, 1006);
880
- return;
881
- }
882
- const frames = Array.isArray(body?.frames) ? body.frames : [];
883
- for (const frame of frames) {
884
- const entry = frame;
885
- if (entry.type === "cancel" && typeof entry.callId === "string") this.inFlight.delete(entry.callId);
886
- this.emitMessage(JSON.stringify(frame));
887
- }
888
- for (const listener of this.pingListeners) listener();
889
- if (frames.length === 0) {
890
- const elapsedMs = Date.now() - startedAt;
891
- if (elapsedMs < EMPTY_POLL_FLOOR_MS) await wait(EMPTY_POLL_FLOOR_MS - elapsedMs);
892
- }
745
+ if (!await this.pollOnce()) return;
746
+ }
747
+ }
748
+ /** One long poll: delivers what it answered, and says whether polling goes on. */
749
+ async pollOnce() {
750
+ const query = this.inFlight.size > 0 ? `?inFlight=${encodeURIComponent([...this.inFlight].join(","))}` : "";
751
+ const startedAt = Date.now();
752
+ let response;
753
+ try {
754
+ response = await this.fetchImpl(`${this.url}/poll${query}`, {
755
+ method: "GET",
756
+ headers: this.requestHeaders(),
757
+ signal: this.polls.signal
758
+ });
759
+ } catch (error) {
760
+ if (!this.closed) this.fail(`the poll failed (${describe(error)})`, 1006);
761
+ return false;
762
+ }
763
+ if (this.closed) return false;
764
+ if (response.status === 410) {
765
+ this.fail(
766
+ "the platform no longer knows this instance, registering again",
767
+ SESSION_LOST_CLOSE_CODE
768
+ );
769
+ return false;
770
+ }
771
+ const body = await this.jsonOf(response);
772
+ if (!response.ok) {
773
+ this.refusePoll({ status: response.status, body });
774
+ return false;
775
+ }
776
+ const delivered = this.deliverFrames(body);
777
+ for (const listener of this.pingListeners) listener();
778
+ if (delivered === 0) {
779
+ const elapsedMs = Date.now() - startedAt;
780
+ if (elapsedMs < EMPTY_POLL_FLOOR_MS) await wait(EMPTY_POLL_FLOOR_MS - elapsedMs);
781
+ }
782
+ return true;
783
+ }
784
+ refusePoll({
785
+ status,
786
+ body
787
+ }) {
788
+ const answered = body && typeof body.frame === "object" && body.frame !== null ? body.frame : null;
789
+ if (answered) this.emitMessage(JSON.stringify(answered));
790
+ if (answered?.type === "refused") {
791
+ return;
792
+ }
793
+ this.fail(`the poll was answered with HTTP ${status}`, 1006);
794
+ }
795
+ /** Emits every frame a poll answered and returns how many there were. */
796
+ deliverFrames(body) {
797
+ const frames = Array.isArray(body?.frames) ? body.frames : [];
798
+ for (const frame of frames) {
799
+ const entry = frame;
800
+ if (entry.type === "cancel" && typeof entry.callId === "string")
801
+ this.inFlight.delete(entry.callId);
802
+ this.emitMessage(JSON.stringify(frame));
893
803
  }
804
+ return frames.length;
894
805
  }
895
806
  async post(data) {
896
807
  if (!this.token) return;
@@ -908,12 +819,7 @@ var HttpLongPollSocket = class {
908
819
  if (this.frames.signal.aborted) return;
909
820
  response = null;
910
821
  }
911
- if (response?.ok) return;
912
- if (response?.status === 410) {
913
- this.fail("the platform no longer knows this instance, registering again", SESSION_LOST_CLOSE_CODE);
914
- return;
915
- }
916
- if (response && response.status < 500) return;
822
+ if (this.postSettled(response)) return;
917
823
  const delay = POST_RETRY_DELAYS_MS[attempt];
918
824
  if (delay === void 0) {
919
825
  this.fail(`a frame could not be posted after ${attempt} retries`, 1006);
@@ -922,6 +828,18 @@ var HttpLongPollSocket = class {
922
828
  await wait(delay);
923
829
  }
924
830
  }
831
+ /** Whether a post's response ends its retries: delivered, refused, or the session lost. */
832
+ postSettled(response) {
833
+ if (response?.ok) return true;
834
+ if (response?.status === 410) {
835
+ this.fail(
836
+ "the platform no longer knows this instance, registering again",
837
+ SESSION_LOST_CLOSE_CODE
838
+ );
839
+ return true;
840
+ }
841
+ return response !== null && response.status < 500;
842
+ }
925
843
  async jsonOf(response) {
926
844
  try {
927
845
  const parsed = await response.json();
@@ -975,19 +893,257 @@ function openTransportSocket({
975
893
  return transport === "http" ? new HttpLongPollSocket({ url: httpUrl, headers }) : socketFactory({ url: websocketUrl, headers });
976
894
  }
977
895
 
896
+ // src/agent/schema.ts
897
+ var AgentParameterError = class extends Error {
898
+ constructor(message) {
899
+ super(message);
900
+ this.code = "agent_parameter_invalid";
901
+ this.name = "AgentParameterError";
902
+ }
903
+ };
904
+ var ACCEPTED_FORMS = `parameters must be a definition map ({ model: { options: [...], default: '...' } }), a Standard JSON Schema object (one with "~standard".jsonSchema), or a JSON Schema object ({ type: 'object', properties })`;
905
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
906
+ var isStandardJsonSchema = (value) => {
907
+ if (!isRecord2(value)) return false;
908
+ const standard = value["~standard"];
909
+ return isRecord2(standard) && isRecord2(standard.jsonSchema);
910
+ };
911
+ var isJsonSchemaObject = (value) => isRecord2(value) && value.type === "object" && isRecord2(value.properties);
912
+ var isDefinitionMap = (value) => isRecord2(value) && Object.values(value).every(isRecord2);
913
+ var definitionType = (definition) => {
914
+ if (definition.type) return definition.type;
915
+ if (definition.options) return "string";
916
+ const value = definition.default;
917
+ if (typeof value === "number") return "number";
918
+ if (typeof value === "boolean") return "boolean";
919
+ return "string";
920
+ };
921
+ var definitionMapToSchema = (definitions) => {
922
+ const properties = {};
923
+ const required = [];
924
+ for (const [name, definition] of Object.entries(definitions)) {
925
+ const property = { type: definitionType(definition) };
926
+ if (definition.options) property.enum = [...definition.options];
927
+ if (definition.default !== void 0) property.default = definition.default;
928
+ else required.push(name);
929
+ if (definition.description !== void 0) property.description = definition.description;
930
+ properties[name] = property;
931
+ }
932
+ const schema = { type: "object", properties };
933
+ if (required.length > 0) schema.required = required;
934
+ return schema;
935
+ };
936
+ var readStandardJsonSchema = (input) => {
937
+ const converter = input["~standard"].jsonSchema;
938
+ const options = { target: "draft-2020-12" };
939
+ const schema = converter.input?.(options) ?? converter.output?.(options);
940
+ if (schema === void 0) {
941
+ throw new AgentParameterError(
942
+ `the "~standard".jsonSchema converter has no input function; ${ACCEPTED_FORMS}`
943
+ );
944
+ }
945
+ if (!isRecord2(schema)) {
946
+ throw new AgentParameterError(
947
+ `the "~standard".jsonSchema converter returned no object; ${ACCEPTED_FORMS}`
948
+ );
949
+ }
950
+ return schema;
951
+ };
952
+ function toParameterSchema(input) {
953
+ if (input === void 0) return { type: "object", properties: {} };
954
+ if (isStandardJsonSchema(input)) return readStandardJsonSchema(input);
955
+ if (isJsonSchemaObject(input)) return input;
956
+ if (isDefinitionMap(input)) return definitionMapToSchema(input);
957
+ throw new AgentParameterError(ACCEPTED_FORMS);
958
+ }
959
+ var specType = (property) => {
960
+ const type = Array.isArray(property.type) ? property.type.find((item) => item !== "null") : property.type;
961
+ if (type === "number" || type === "integer") return "number";
962
+ if (type === "boolean") return "boolean";
963
+ return "string";
964
+ };
965
+ var scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : void 0;
966
+ function parameterSpecsFromSchema(schema) {
967
+ const properties = isRecord2(schema.properties) ? schema.properties : {};
968
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
969
+ const specs = [];
970
+ for (const [name, raw] of Object.entries(properties)) {
971
+ const property = isRecord2(raw) ? raw : {};
972
+ const spec = { name, type: specType(property) };
973
+ const options = Array.isArray(property.enum) ? property.enum.filter((item) => typeof item === "string") : void 0;
974
+ if (options && options.length > 0) spec.options = options;
975
+ const fallback = scalar(property.default);
976
+ if (fallback !== void 0) spec.default = fallback;
977
+ if (typeof property.description === "string") spec.description = property.description;
978
+ spec.required = fallback === void 0 && required.has(name);
979
+ specs.push(spec);
980
+ }
981
+ return specs;
982
+ }
983
+ var coerce = ({
984
+ spec,
985
+ value
986
+ }) => {
987
+ if (spec.type === "number") {
988
+ return coerceNumber(spec.name, value);
989
+ }
990
+ if (spec.type === "boolean") {
991
+ if (typeof value === "boolean") return value;
992
+ if (value === "true") return true;
993
+ if (value === "false") return false;
994
+ throw new AgentParameterError(
995
+ `parameter "${spec.name}" must be true or false, got ${JSON.stringify(value)}`
996
+ );
997
+ }
998
+ const asString = typeof value === "string" ? value : String(value);
999
+ if (spec.options && !spec.options.includes(asString)) {
1000
+ throw new AgentParameterError(
1001
+ `parameter "${spec.name}" must be one of ${spec.options.join(", ")}, got ${JSON.stringify(value)}`
1002
+ );
1003
+ }
1004
+ return asString;
1005
+ };
1006
+ function resolveParameterValues({
1007
+ specs,
1008
+ supplied
1009
+ }) {
1010
+ const values = { ...supplied };
1011
+ for (const spec of specs) {
1012
+ const value = values[spec.name];
1013
+ if (value === void 0) {
1014
+ if (spec.default !== void 0) {
1015
+ values[spec.name] = spec.default;
1016
+ continue;
1017
+ }
1018
+ if (!spec.required) continue;
1019
+ throw new AgentParameterError(
1020
+ `parameter "${spec.name}" is required and the run did not supply it`
1021
+ );
1022
+ }
1023
+ values[spec.name] = coerce({ spec, value });
1024
+ }
1025
+ return values;
1026
+ }
1027
+ var issuePath = (issue) => (issue.path ?? []).map(
1028
+ (segment) => typeof segment === "object" && segment !== null ? String(segment.key) : String(segment)
1029
+ ).join(".");
1030
+ async function validateParameterValues({
1031
+ schema,
1032
+ values
1033
+ }) {
1034
+ const standard = schema["~standard"];
1035
+ const result = await standard.validate?.(values);
1036
+ if (result === void 0) return values;
1037
+ if (result.issues) {
1038
+ const detail = result.issues.map((issue) => {
1039
+ const path2 = issuePath(issue);
1040
+ return path2 ? `${path2}: ${issue.message}` : issue.message;
1041
+ }).join("; ");
1042
+ throw new AgentParameterError(`parameters refused by the schema: ${detail}`);
1043
+ }
1044
+ const parsed = isRecord2(result.value) ? result.value : {};
1045
+ return { ...values, ...parsed };
1046
+ }
1047
+ function createParameterReader({
1048
+ input,
1049
+ specs
1050
+ }) {
1051
+ const schema = isStandardJsonSchema(input) ? input : void 0;
1052
+ return async (supplied) => {
1053
+ const values = resolveParameterValues({ specs, supplied });
1054
+ return schema ? validateParameterValues({ schema, values }) : values;
1055
+ };
1056
+ }
1057
+ function coerceNumber(name, value) {
1058
+ const asNumber = typeof value === "number" ? value : Number(value);
1059
+ if (typeof value === "boolean") {
1060
+ throw new AgentParameterError(
1061
+ `parameter "${name}" must be a number, got ${JSON.stringify(value)}`
1062
+ );
1063
+ }
1064
+ if (!Number.isFinite(asNumber)) {
1065
+ throw new AgentParameterError(
1066
+ `parameter "${name}" must be a number, got ${JSON.stringify(value)}`
1067
+ );
1068
+ }
1069
+ const stringValue2 = String(value).trim();
1070
+ if (stringValue2 === "") {
1071
+ throw new AgentParameterError(
1072
+ `parameter "${name}" must be a number, got ${JSON.stringify(value)}`
1073
+ );
1074
+ }
1075
+ return asNumber;
1076
+ }
1077
+
1078
+ // src/agent/state.ts
1079
+ var STATE_MAX_BYTES = 64 * 1024;
1080
+ var AgentStateError = class extends Error {
1081
+ constructor(code, message) {
1082
+ super(message);
1083
+ this.code = code;
1084
+ this.name = "AgentStateError";
1085
+ }
1086
+ };
1087
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1088
+ function checkState(value) {
1089
+ if (value === void 0 || value === null) return void 0;
1090
+ if (!isPlainObject(value)) {
1091
+ throw new AgentStateError(
1092
+ "agent_call_failed",
1093
+ `the state must be a JSON object, got ${Array.isArray(value) ? "an array" : typeof value}`
1094
+ );
1095
+ }
1096
+ let encoded;
1097
+ try {
1098
+ encoded = JSON.stringify(value);
1099
+ } catch (error) {
1100
+ throw new AgentStateError(
1101
+ "agent_call_failed",
1102
+ `the state is not JSON: ${error instanceof Error ? error.message : String(error)}`
1103
+ );
1104
+ }
1105
+ const sizeBytes = Buffer.byteLength(encoded, "utf8");
1106
+ if (sizeBytes > STATE_MAX_BYTES) {
1107
+ throw new AgentStateError(
1108
+ "agent_payload_too_large",
1109
+ `The state is ${sizeBytes} bytes, above the limit of ${STATE_MAX_BYTES} bytes.`
1110
+ );
1111
+ }
1112
+ return value;
1113
+ }
1114
+
978
1115
  // src/agent/client.ts
1116
+ var kindRefusal = (runtime, kind) => {
1117
+ if (kind === "turn") return null;
1118
+ if (kind !== "setup" && kind !== "teardown") {
1119
+ return { code: "agent_call_failed", message: `unknown call kind "${kind}"` };
1120
+ }
1121
+ if (runtime.hooks[kind]) return null;
1122
+ return {
1123
+ code: "hook_not_declared",
1124
+ message: `agent "${runtime.name}" declares no ${kind} hook`
1125
+ };
1126
+ };
1127
+ var errorCodeOf = (error) => error instanceof AgentParameterError || error instanceof AgentStateError ? error.code : "agent_call_failed";
1128
+ var answerCall = async ({
1129
+ runtime,
1130
+ frame,
1131
+ call
1132
+ }) => {
1133
+ const hook = frame.kind === "setup" || frame.kind === "teardown" ? runtime.hooks[frame.kind] : void 0;
1134
+ if (!hook) {
1135
+ const result = await runtime.run(call);
1136
+ return { ...result, state: checkState(result.state) };
1137
+ }
1138
+ const state = await hook({ run: frame.run, state: frame.state, params: call.params });
1139
+ return { state: checkState(frame.kind === "setup" ? state ?? {} : state) };
1140
+ };
979
1141
  var FAILURE_NOTICE_INTERVAL_MS = 5 * 6e4;
980
1142
  var NOT_CONNECTED = "not connected to LangWatch";
981
1143
  function refusalAdvice(frame) {
982
1144
  switch (frame.code) {
983
1145
  case "project_required": {
984
- const projects = Array.isArray(frame.meta?.projects) ? frame.meta.projects : [];
985
- const listed = projects.map((project) => {
986
- const entry = project;
987
- const id = typeof entry.id === "string" ? entry.id : "";
988
- const name = typeof entry.name === "string" ? entry.name : "";
989
- return name && id ? `${name} (${id})` : id || name;
990
- }).filter((line) => line !== "");
1146
+ const listed = projectNames(frame.meta?.projects);
991
1147
  return `the API key reaches more than one project. Set LANGWATCH_PROJECT_ID to one of: ${listed.length > 0 ? listed.join(", ") : "the projects the key reaches"}.`;
992
1148
  }
993
1149
  case "api_key_invalid":
@@ -1007,8 +1163,30 @@ function refusalAdvice(frame) {
1007
1163
  return `${frame.message} (${frame.code})`;
1008
1164
  }
1009
1165
  }
1166
+ function projectNames(value) {
1167
+ if (!Array.isArray(value)) return [];
1168
+ return value.map(projectName).filter((name) => name !== "");
1169
+ }
1170
+ function projectName(value) {
1171
+ if (!isRecord3(value)) return "";
1172
+ const id = stringValue(value.id);
1173
+ const name = stringValue(value.name);
1174
+ if (name && id) return `${name} (${id})`;
1175
+ return id || name;
1176
+ }
1177
+ function isRecord3(value) {
1178
+ return typeof value === "object" && value !== null;
1179
+ }
1180
+ function stringValue(value) {
1181
+ return typeof value === "string" ? value : "";
1182
+ }
1010
1183
  var SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
1011
1184
  var CLOSE_GRACE_MS = 500;
1185
+ var readCallTraceContext = (frame) => {
1186
+ const parent = frame.traceparent ? propagation.extract(context.active(), { traceparent: frame.traceparent }) : context.active();
1187
+ const traceId = trace.getSpanContext(parent)?.traceId ?? traceIdFromTraceparent(frame.traceparent) ?? "";
1188
+ return { parent, traceId };
1189
+ };
1012
1190
  var AgentClient = class {
1013
1191
  constructor(config) {
1014
1192
  this.agents = [];
@@ -1049,7 +1227,7 @@ var AgentClient = class {
1049
1227
  get isRegistered() {
1050
1228
  return this.registered;
1051
1229
  }
1052
- /** True once the client gave up: refused, or no socket implementation. No timer is left behind. */
1230
+ /** True once the client gave up: refused, or no socket implementation available. */
1053
1231
  get isStopped() {
1054
1232
  return this.stopped;
1055
1233
  }
@@ -1066,7 +1244,9 @@ var AgentClient = class {
1066
1244
  addAgent(runtime) {
1067
1245
  this.agents.push(runtime);
1068
1246
  if (this.gaveUp) {
1069
- this.logger.debug(`agent "${runtime.name}" ${NOT_CONNECTED}: the connection gave up earlier in this process`);
1247
+ this.logger.debug(
1248
+ `agent "${runtime.name}" ${NOT_CONNECTED}: the connection gave up earlier in this process`
1249
+ );
1070
1250
  return;
1071
1251
  }
1072
1252
  this.stopped = false;
@@ -1076,6 +1256,10 @@ var AgentClient = class {
1076
1256
  }
1077
1257
  this.scheduleConnect(0);
1078
1258
  }
1259
+ /** Registers the agents again after one changed: a hook declared once the socket opened. */
1260
+ refreshAgents() {
1261
+ if (this.socket) this.restartSocket();
1262
+ }
1079
1263
  /** Closes the socket and connects again at once, keeping the reconnect loop. */
1080
1264
  restartSocket() {
1081
1265
  const socket = this.socket;
@@ -1235,7 +1419,10 @@ var AgentClient = class {
1235
1419
  * connection is lost, and silence while the retries run: the same notice
1236
1420
  * repeats only after the notice interval, and a reconnect resets it.
1237
1421
  */
1238
- noteDisconnected({ wasRegistered, code }) {
1422
+ noteDisconnected({
1423
+ wasRegistered,
1424
+ code
1425
+ }) {
1239
1426
  const now = Date.now();
1240
1427
  if (wasRegistered) {
1241
1428
  this.logger.warn(
@@ -1328,7 +1515,8 @@ var AgentClient = class {
1328
1515
  this.logger.info(`connected to LangWatch over HTTP long polling at ${this.httpUrl}`);
1329
1516
  }
1330
1517
  this.heartbeatIntervalMs = frame.heartbeatIntervalMs;
1331
- if (frame.instanceId && frame.instanceId !== this.instance.id) this.instance.id = frame.instanceId;
1518
+ if (frame.instanceId && frame.instanceId !== this.instance.id)
1519
+ this.instance.id = frame.instanceId;
1332
1520
  this.byId.clear();
1333
1521
  for (const entry of frame.agents) {
1334
1522
  const runtime = this.agents.find(
@@ -1355,21 +1543,9 @@ var AgentClient = class {
1355
1543
  });
1356
1544
  return;
1357
1545
  }
1358
- const busy = [...this.inFlight.values()].filter((call2) => call2.runtime === runtime).length;
1359
- if (busy >= runtime.concurrency) {
1360
- this.sendError({
1361
- callId: frame.callId,
1362
- code: "agent_busy",
1363
- message: `agent "${runtime.name}" has ${busy} call${busy === 1 ? "" : "s"} in flight, its limit`
1364
- });
1365
- return;
1366
- }
1367
- if (frame.deadlineAt !== null && frame.deadlineAt <= Date.now()) {
1368
- this.sendError({
1369
- callId: frame.callId,
1370
- code: "agent_call_timeout",
1371
- message: "the call deadline passed before it started"
1372
- });
1546
+ const refusal = this.callRefusal({ frame, runtime });
1547
+ if (refusal) {
1548
+ this.sendError({ callId: frame.callId, ...refusal });
1373
1549
  return;
1374
1550
  }
1375
1551
  const entry = { runtime, cancelled: false, timer: null };
@@ -1392,18 +1568,18 @@ var AgentClient = class {
1392
1568
  }
1393
1569
  this.send({ type: "ack", protocol: PROTOCOL_VERSION, callId: frame.callId });
1394
1570
  this.armCallDeadline({ frame, entry, runtime });
1395
- const parent = frame.traceparent ? propagation.extract(context.active(), { traceparent: frame.traceparent }) : context.active();
1396
- const traceId = trace.getSpanContext(parent)?.traceId ?? traceIdFromTraceparent(frame.traceparent) ?? "";
1571
+ const { parent, traceId } = readCallTraceContext(frame);
1397
1572
  const call = {
1398
1573
  messages: frame.messages,
1399
1574
  newMessages: frame.newMessages,
1400
1575
  threadId: frame.threadId,
1401
1576
  session: frame.session,
1577
+ state: frame.state,
1402
1578
  params,
1403
1579
  traceId
1404
1580
  };
1405
1581
  try {
1406
- const result = await context.with(parent, () => runtime.run(call));
1582
+ const result = await context.with(parent, () => answerCall({ runtime, frame, call }));
1407
1583
  if (entry.cancelled) return;
1408
1584
  this.releaseCall({ callId: frame.callId, entry });
1409
1585
  await this.flushSpans();
@@ -1411,12 +1587,13 @@ var AgentClient = class {
1411
1587
  type: "result",
1412
1588
  protocol: PROTOCOL_VERSION,
1413
1589
  callId: frame.callId,
1414
- output: result.output,
1415
- ...result.session === void 0 ? {} : { session: result.session }
1590
+ ...result.output === void 0 ? {} : { output: result.output },
1591
+ ...result.session === void 0 ? {} : { session: result.session },
1592
+ ...result.state === void 0 ? {} : { state: result.state }
1416
1593
  });
1417
1594
  } catch (error) {
1418
1595
  if (entry.cancelled) return;
1419
- const code = error instanceof AgentParameterError ? error.code : "agent_call_failed";
1596
+ const code = errorCodeOf(error);
1420
1597
  const message = describeError(error);
1421
1598
  this.logger.warn(`agent "${runtime.name}" call ${frame.callId} failed: ${message}`);
1422
1599
  this.releaseCall({ callId: frame.callId, entry });
@@ -1426,15 +1603,32 @@ var AgentClient = class {
1426
1603
  this.releaseCall({ callId: frame.callId, entry });
1427
1604
  }
1428
1605
  }
1606
+ /** Why the runtime cannot take this call now, or null when it can. */
1607
+ callRefusal({
1608
+ frame,
1609
+ runtime
1610
+ }) {
1611
+ const kind = kindRefusal(runtime, frame.kind);
1612
+ if (kind) return kind;
1613
+ const busy = [...this.inFlight.values()].filter((call) => call.runtime === runtime).length;
1614
+ if (busy >= runtime.concurrency) {
1615
+ return {
1616
+ code: "agent_busy",
1617
+ message: `agent "${runtime.name}" has ${busy} call${busy === 1 ? "" : "s"} in flight, its limit`
1618
+ };
1619
+ }
1620
+ if (frame.deadlineAt !== null && frame.deadlineAt <= Date.now()) {
1621
+ return {
1622
+ code: "agent_call_timeout",
1623
+ message: "the call deadline passed before it started"
1624
+ };
1625
+ }
1626
+ return null;
1627
+ }
1429
1628
  /**
1430
- * Exports the spans of the call now instead of at the exporter's next
1431
- * schedule. The judge reads the agent's spans right after the last turn,
1432
- * and a batch exporter would otherwise hold them for seconds, which is what
1433
- * made the judge report the spans missing.
1434
- *
1435
- * The call awaits this before it sends its result or its error: the frame is
1436
- * what tells the platform the turn is over, so a frame that goes out first
1437
- * lets the judge read the call while its spans are still in the exporter.
1629
+ * Exports the spans of the call now, since a batch exporter would
1630
+ * otherwise hold them for seconds after the judge reads them. Awaited
1631
+ * before the result or error frame goes out.
1438
1632
  */
1439
1633
  async flushSpans() {
1440
1634
  const provider = trace.getTracerProvider();
@@ -1466,26 +1660,33 @@ var AgentClient = class {
1466
1660
  runtime
1467
1661
  }) {
1468
1662
  const fromDeadline = frame.deadlineAt === null ? Infinity : frame.deadlineAt - Date.now();
1469
- const limit = Math.min(fromDeadline, runtime.timeoutMs);
1663
+ const limit = frame.kind !== "turn" && frame.deadlineAt !== null ? fromDeadline : Math.min(fromDeadline, runtime.timeoutMs);
1470
1664
  if (!Number.isFinite(limit)) return;
1471
- const timer = setTimeout(() => {
1472
- entry.timer = null;
1473
- if (entry.cancelled) return;
1474
- entry.cancelled = true;
1475
- this.releaseCall({ callId: frame.callId, entry });
1476
- this.logger.warn(
1477
- `agent "${runtime.name}" call ${frame.callId} passed its ${limit} ms limit`
1478
- );
1479
- this.sendError({
1480
- callId: frame.callId,
1481
- code: "agent_call_timeout",
1482
- message: `the call passed the ${limit} ms limit of agent "${runtime.name}"`
1483
- });
1484
- }, Math.max(0, limit));
1665
+ const timer = setTimeout(
1666
+ () => {
1667
+ entry.timer = null;
1668
+ if (entry.cancelled) return;
1669
+ entry.cancelled = true;
1670
+ this.releaseCall({ callId: frame.callId, entry });
1671
+ this.logger.warn(
1672
+ `agent "${runtime.name}" call ${frame.callId} passed its ${limit} ms limit`
1673
+ );
1674
+ this.sendError({
1675
+ callId: frame.callId,
1676
+ code: "agent_call_timeout",
1677
+ message: `the call passed the ${limit} ms limit of agent "${runtime.name}"`
1678
+ });
1679
+ },
1680
+ Math.max(0, limit)
1681
+ );
1485
1682
  timer.unref();
1486
1683
  entry.timer = timer;
1487
1684
  }
1488
- sendError({ callId, code, message }) {
1685
+ sendError({
1686
+ callId,
1687
+ code,
1688
+ message
1689
+ }) {
1489
1690
  this.send({ type: "result", protocol: PROTOCOL_VERSION, callId, error: { code, message } });
1490
1691
  }
1491
1692
  guard(action) {
@@ -1528,7 +1729,11 @@ var installShutdownHooks = () => {
1528
1729
  }
1529
1730
  process.on("beforeExit", onBeforeExit);
1530
1731
  };
1531
- function warnOnce2({ logger, key, message }) {
1732
+ function warnOnce2({
1733
+ logger,
1734
+ key,
1735
+ message
1736
+ }) {
1532
1737
  if (noticesGiven.has(key)) {
1533
1738
  logger.debug(message);
1534
1739
  return;
@@ -1571,22 +1776,33 @@ var DEFAULT_CONCURRENCY = 10;
1571
1776
  var isMessage = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.role === "string";
1572
1777
  function normalizeReply(reply) {
1573
1778
  if (typeof reply === "string") return { output: reply };
1574
- if (Array.isArray(reply) && reply.every(isMessage)) return { output: reply };
1779
+ if (Array.isArray(reply)) {
1780
+ if (reply.every(isMessage)) return { output: reply };
1781
+ }
1575
1782
  if (isMessage(reply)) return { output: reply };
1576
1783
  if (typeof reply === "object" && reply !== null && "output" in reply) {
1577
- const { output, session } = reply;
1578
- const normalized = normalizeReply(output);
1579
- return session === void 0 ? normalized : { output: normalized.output, session };
1784
+ const { output, session, state } = reply;
1785
+ return {
1786
+ output: normalizeReply(output).output,
1787
+ ...session === void 0 ? {} : { session },
1788
+ ...state === void 0 ? {} : { state }
1789
+ };
1580
1790
  }
1581
1791
  throw new Error(
1582
- "the agent handler must return a string, a message, a list of messages, or { output, session }"
1792
+ "the agent handler must return a string, a message, a list of messages, or { output, session, state }"
1583
1793
  );
1584
1794
  }
1585
- var clampTimeout = ({ timeoutMs, logger, name }) => {
1795
+ var clampTimeout = ({
1796
+ timeoutMs,
1797
+ logger,
1798
+ name
1799
+ }) => {
1586
1800
  if (timeoutMs === void 0) return DEFAULT_TIMEOUT_MS;
1587
1801
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return DEFAULT_TIMEOUT_MS;
1588
1802
  if (timeoutMs > MAX_TIMEOUT_MS) {
1589
- logger.warn(`agent "${name}": timeoutMs ${timeoutMs} is above the ${MAX_TIMEOUT_MS} cap, using the cap`);
1803
+ logger.warn(
1804
+ `agent "${name}": timeoutMs ${timeoutMs} is above the ${MAX_TIMEOUT_MS} cap, using the cap`
1805
+ );
1590
1806
  return MAX_TIMEOUT_MS;
1591
1807
  }
1592
1808
  return Math.floor(timeoutMs);
@@ -1617,10 +1833,12 @@ function connectAgent(options, handler) {
1617
1833
  newMessages: call.newMessages ?? call.messages,
1618
1834
  threadId: call.threadId ?? `local_${Date.now().toString(36)}`,
1619
1835
  session: call.session ?? null,
1836
+ state: call.state ?? null,
1620
1837
  params,
1621
1838
  traceId: call.traceId ?? ""
1622
1839
  });
1623
1840
  };
1841
+ const hooks = {};
1624
1842
  const runtime = {
1625
1843
  name,
1626
1844
  environment,
@@ -1630,14 +1848,29 @@ function connectAgent(options, handler) {
1630
1848
  parameters,
1631
1849
  concurrency,
1632
1850
  timeoutMs,
1633
- ...options.sticky ? { sticky: true } : {}
1851
+ ...options.sticky ? { sticky: true } : {},
1852
+ hooks: { setup: false, teardown: false }
1634
1853
  },
1635
1854
  readParams: readParams2,
1636
1855
  concurrency,
1637
1856
  timeoutMs,
1638
- run: runHandler
1857
+ run: runHandler,
1858
+ hooks
1639
1859
  };
1640
1860
  let detach;
1861
+ let refresh;
1862
+ const scenario = {
1863
+ setup: (hook) => {
1864
+ hooks.setup = async ({ run, params }) => hook(run, params);
1865
+ runtime.register.hooks = { setup: true, teardown: Boolean(hooks.teardown) };
1866
+ refresh?.();
1867
+ },
1868
+ teardown: (hook) => {
1869
+ hooks.teardown = async ({ run, state, params }) => hook(run, state, params);
1870
+ runtime.register.hooks = { setup: Boolean(hooks.setup), teardown: true };
1871
+ refresh?.();
1872
+ }
1873
+ };
1641
1874
  if (!resolveEnabled({ explicit: options.enabled })) {
1642
1875
  logger.debug(`agent "${name}" not connected to LangWatch: the connection is disabled`);
1643
1876
  } else {
@@ -1659,15 +1892,18 @@ function connectAgent(options, handler) {
1659
1892
  });
1660
1893
  client.addAgent(runtime);
1661
1894
  detach = () => client.removeAgent(runtime);
1895
+ refresh = () => client.refreshAgents();
1662
1896
  }
1663
1897
  }
1664
1898
  const connected = Object.assign(invoke, {
1665
1899
  environment,
1666
1900
  parameters,
1901
+ scenario,
1667
1902
  disconnect: async () => {
1668
1903
  if (!detach) return;
1669
1904
  const release = detach;
1670
1905
  detach = void 0;
1906
+ refresh = void 0;
1671
1907
  await release();
1672
1908
  }
1673
1909
  });
@@ -1677,9 +1913,11 @@ function connectAgent(options, handler) {
1677
1913
  export {
1678
1914
  AGENT_TRANSPORTS,
1679
1915
  AgentParameterError,
1916
+ AgentStateError,
1680
1917
  DEFAULT_TIMEOUT_MS,
1681
1918
  MAX_TIMEOUT_MS,
1682
1919
  PROTOCOL_VERSION,
1920
+ STATE_MAX_BYTES,
1683
1921
  connectAgent,
1684
1922
  normalizeReply,
1685
1923
  parameterSpecsFromSchema,