@kortyx/agent 0.14.2 → 0.15.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/dist/index.js CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  parseChatRequestBody: () => parseChatRequestBody,
30
30
  parseCheckpointRequestBody: () => parseCheckpointRequestBody,
31
31
  parseResumeMeta: () => parseResumeMeta,
32
+ projectWorkflowTopology: () => projectWorkflowTopology,
32
33
  streamChat: () => streamChat,
33
34
  streamChatFromRoute: () => streamChatFromRoute,
34
35
  transformGraphStreamForUI: () => transformGraphStreamForUI,
@@ -187,6 +188,266 @@ var import_providers = require("@kortyx/providers");
187
188
  var import_runtime4 = require("@kortyx/runtime");
188
189
  var import_zod2 = require("zod");
189
190
 
191
+ // src/telemetry/events.ts
192
+ var import_node_crypto = require("crypto");
193
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
194
+ var createId = () => {
195
+ try {
196
+ return (0, import_node_crypto.randomUUID)();
197
+ } catch {
198
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
199
+ }
200
+ };
201
+ var shouldCaptureTelemetryContent = (value, side) => {
202
+ if (value === true) return true;
203
+ return Boolean(
204
+ value && typeof value === "object" && !Array.isArray(value) && value[side] === true
205
+ );
206
+ };
207
+ var emitTelemetryEvent = (args) => {
208
+ const telemetry = isRecord(args.config.telemetry) ? args.config.telemetry : void 0;
209
+ const base = telemetry?.correlation;
210
+ const runId = args.correlation?.runId ?? base?.runId;
211
+ const workflowId = args.correlation?.workflowId ?? base?.workflowId;
212
+ if (!telemetry?.reporter || !telemetry.environment || !telemetry.service || !runId || !workflowId) {
213
+ return;
214
+ }
215
+ const context = isRecord(args.config.context) ? args.config.context : {};
216
+ const event = {
217
+ schemaVersion: 1,
218
+ eventId: createId(),
219
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
220
+ environment: telemetry.environment,
221
+ service: telemetry.service,
222
+ correlation: {
223
+ runId,
224
+ workflowId,
225
+ ...base?.sessionId ? { sessionId: base.sessionId } : {},
226
+ ...base?.workflowRevisionId ? { workflowRevisionId: base.workflowRevisionId } : {},
227
+ ...base?.topologyHash ? { topologyHash: base.topologyHash } : {},
228
+ ...base?.nodeId ? { nodeId: base.nodeId } : {},
229
+ ...args.correlation ?? {}
230
+ },
231
+ ...telemetry.metadata || telemetry.tags || context.userId || context.tenantId ? {
232
+ context: {
233
+ ...typeof context.userId === "string" ? { userId: context.userId } : {},
234
+ ...typeof context.tenantId === "string" ? { tenantId: context.tenantId } : {},
235
+ ...telemetry.tags ? { tags: telemetry.tags } : {},
236
+ ...telemetry.metadata ? { metadata: telemetry.metadata } : {}
237
+ }
238
+ } : {},
239
+ type: args.type,
240
+ payload: args.payload
241
+ };
242
+ try {
243
+ void Promise.resolve(telemetry.reporter.emit([event])).then(async () => {
244
+ if (!args.flush) return;
245
+ const flush = telemetry.flush;
246
+ if (typeof flush === "function") await flush.call(telemetry);
247
+ }).catch(() => {
248
+ });
249
+ } catch {
250
+ }
251
+ };
252
+
253
+ // src/telemetry/topology.ts
254
+ var import_node_crypto2 = require("crypto");
255
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
256
+ var stableValue = (value) => {
257
+ if (Array.isArray(value)) return value.map(stableValue);
258
+ if (!isRecord2(value)) return value;
259
+ return Object.fromEntries(
260
+ Object.entries(value).filter(([, nested]) => typeof nested !== "function").sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, stableValue(nested)])
261
+ );
262
+ };
263
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
264
+ var metadata = (value) => isRecord2(value) ? stableValue(value) : void 0;
265
+ var tagsFrom = (workflow) => {
266
+ const raw = isRecord2(workflow.metadata) ? workflow.metadata.tags : void 0;
267
+ if (!Array.isArray(raw)) return void 0;
268
+ const tags = raw.filter((tag) => typeof tag === "string").sort();
269
+ return tags.length > 0 ? tags : void 0;
270
+ };
271
+ var wordsFrom = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").split(/[^a-zA-Z0-9]+/).map((word) => word.toLowerCase()).filter(Boolean);
272
+ var camelFromWorkflowId = (workflowId) => {
273
+ const [first = "", ...rest] = wordsFrom(workflowId);
274
+ return [
275
+ first,
276
+ ...rest.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
277
+ ].join("");
278
+ };
279
+ var resolveMemberTransitionTarget = (memberName, knownWorkflowIds) => {
280
+ const exact = knownWorkflowIds.find(
281
+ (workflowId) => camelFromWorkflowId(workflowId) === memberName
282
+ );
283
+ if (exact) return exact;
284
+ const memberWords = new Set(wordsFrom(memberName));
285
+ const candidates = knownWorkflowIds.map((workflowId) => {
286
+ const workflowWords = wordsFrom(workflowId);
287
+ const matched = workflowWords.filter((word) => memberWords.has(word));
288
+ return {
289
+ workflowId,
290
+ matched: matched.length,
291
+ total: workflowWords.length
292
+ };
293
+ }).filter(
294
+ (candidate) => candidate.total > 0 && candidate.matched === candidate.total
295
+ ).sort(
296
+ (a, b) => b.matched - a.matched || a.workflowId.localeCompare(b.workflowId)
297
+ );
298
+ if (candidates.length !== 1) return void 0;
299
+ return candidates[0]?.workflowId;
300
+ };
301
+ var transitionTargetsFromRun = (run, knownWorkflowIds) => {
302
+ if (typeof run !== "function") return [];
303
+ const source = Function.prototype.toString.call(run);
304
+ const targets = /* @__PURE__ */ new Set();
305
+ for (const match of source.matchAll(
306
+ /\btransitionTo\s*:\s*(['"`])([^'"`$]+)\1/g
307
+ )) {
308
+ const target = match[2]?.trim();
309
+ if (target) targets.add(target);
310
+ }
311
+ for (const match of source.matchAll(
312
+ /\btransitionTo\s*:\s*(?:[A-Za-z_$][\w$]*\.)+([A-Za-z_$][\w$]*)/g
313
+ )) {
314
+ const memberName = match[1];
315
+ const target = resolveMemberTransitionTarget(memberName, knownWorkflowIds);
316
+ if (target) targets.add(target);
317
+ }
318
+ for (const match of source.matchAll(
319
+ /\btransitionTo\s*:\s*(?:[A-Za-z_$][\w$]*\.)*[A-Za-z_$][\w$]*\[['"`]([A-Za-z_$][\w$]*)['"`]\]/g
320
+ )) {
321
+ const memberName = match[1];
322
+ const target = resolveMemberTransitionTarget(memberName, knownWorkflowIds);
323
+ if (target) targets.add(target);
324
+ }
325
+ return [...targets].sort();
326
+ };
327
+ var transitionsFromNodes = (workflow, knownWorkflowIds) => {
328
+ const transitions = /* @__PURE__ */ new Map();
329
+ for (const [sourceNodeId, node] of Object.entries(
330
+ workflow.nodes
331
+ )) {
332
+ for (const targetWorkflowId of transitionTargetsFromRun(
333
+ node.run,
334
+ knownWorkflowIds
335
+ )) {
336
+ if (targetWorkflowId === workflow.id) continue;
337
+ const key = `${sourceNodeId}:${targetWorkflowId}`;
338
+ transitions.set(key, { sourceNodeId, targetWorkflowId });
339
+ }
340
+ }
341
+ return [...transitions.values()].sort(
342
+ (a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))
343
+ );
344
+ };
345
+ var projectWorkflowTopology = (args) => {
346
+ const nodes = Object.entries(
347
+ args.workflow.nodes
348
+ ).map(([id, node]) => {
349
+ const nodeMetadata = metadata(node.metadata);
350
+ const params = metadata(node.params);
351
+ const model = isRecord2(node.params) ? isRecord2(node.params.model) ? node.params.model : void 0 : void 0;
352
+ const provider = isRecord2(model) ? asString(model.provider) ?? asString(model.providerId) : void 0;
353
+ const modelName = isRecord2(model) ? asString(model.name) ?? asString(model.modelId) : void 0;
354
+ return {
355
+ id,
356
+ ...asString(nodeMetadata?.label) ? { label: asString(nodeMetadata?.label) } : {},
357
+ ...asString(nodeMetadata?.type) ? { type: asString(nodeMetadata?.type) } : {},
358
+ ...provider ? { provider } : {},
359
+ ...modelName ? { model: modelName } : {},
360
+ ...nodeMetadata ? { metadata: nodeMetadata } : {},
361
+ // These fields feed the hash but are intentionally not sent as Studio
362
+ // display metadata. They are stripped below after hashing.
363
+ _execution: {
364
+ ...typeof node.run === "string" ? { run: node.run } : {},
365
+ ...params ? { params } : {},
366
+ ...node.behavior ? { behavior: stableValue(node.behavior) } : {}
367
+ }
368
+ };
369
+ }).sort((a, b) => a.id.localeCompare(b.id));
370
+ const edges = args.workflow.edges.map((edge) => {
371
+ const condition = isRecord2(edge[2]) ? asString(edge[2].when) : void 0;
372
+ return {
373
+ sourceNodeId: edge[0],
374
+ targetNodeId: edge[1],
375
+ ...condition ? { condition } : {}
376
+ };
377
+ }).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
378
+ const transitions = transitionsFromNodes(
379
+ args.workflow,
380
+ args.knownWorkflowIds ?? []
381
+ );
382
+ const tags = tagsFrom(args.workflow);
383
+ const hashInput = stableValue({
384
+ workflowId: args.workflow.id,
385
+ nodes: nodes.map((node) => ({ id: node.id, ...node._execution })),
386
+ edges,
387
+ transitions
388
+ });
389
+ const topologyHash = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(hashInput)).digest("hex");
390
+ return {
391
+ schemaVersion: 1,
392
+ environment: args.environment,
393
+ service: args.service,
394
+ workflow: {
395
+ id: args.workflow.id,
396
+ declaredVersion: args.workflow.version,
397
+ ...args.workflow.description ? { description: args.workflow.description } : {},
398
+ ...tags ? { tags } : {},
399
+ topologyHash,
400
+ nodes: nodes.map(({ _execution: _ignored, ...node }) => node),
401
+ edges,
402
+ ...transitions.length > 0 ? { transitions } : {}
403
+ }
404
+ };
405
+ };
406
+ var prepareWorkflowTelemetry = (args) => {
407
+ const telemetry = isRecord2(args.config.telemetry) ? args.config.telemetry : void 0;
408
+ if (!telemetry?.reporter || !telemetry.environment || !telemetry.service) {
409
+ return args.config;
410
+ }
411
+ let snapshot;
412
+ try {
413
+ snapshot = projectWorkflowTopology({
414
+ workflow: args.workflow,
415
+ environment: telemetry.environment,
416
+ service: telemetry.service,
417
+ knownWorkflowIds: args.knownWorkflowIds
418
+ });
419
+ } catch {
420
+ return args.config;
421
+ }
422
+ let workflowRevisionId;
423
+ try {
424
+ workflowRevisionId = telemetry.reporter.getWorkflowRevisionId?.({
425
+ environment: snapshot.environment,
426
+ workflowId: snapshot.workflow.id,
427
+ topologyHash: snapshot.workflow.topologyHash
428
+ });
429
+ void Promise.resolve(
430
+ telemetry.reporter.ensureWorkflowTopology(snapshot)
431
+ ).catch(() => {
432
+ });
433
+ } catch {
434
+ }
435
+ return {
436
+ ...args.config,
437
+ telemetry: {
438
+ ...telemetry,
439
+ correlation: {
440
+ ...telemetry.correlation ?? {},
441
+ runId: args.runId,
442
+ ...args.sessionId ? { sessionId: args.sessionId } : {},
443
+ workflowId: snapshot.workflow.id,
444
+ topologyHash: snapshot.workflow.topologyHash,
445
+ ...workflowRevisionId ? { workflowRevisionId } : {}
446
+ }
447
+ }
448
+ };
449
+ };
450
+
190
451
  // src/chat/process-chat.ts
191
452
  var import_runtime3 = require("@kortyx/runtime");
192
453
 
@@ -263,50 +524,71 @@ async function* transformGraphStreamForUI(stream, options = {}) {
263
524
  }
264
525
 
265
526
  // src/orchestrator.ts
266
- var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
267
- var isGraphState = (value) => isRecord(value) && typeof value.currentWorkflow === "string";
527
+ var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
528
+ var isGraphState = (value) => isRecord3(value) && typeof value.currentWorkflow === "string";
268
529
  var getGraphCheckpointId = (snapshot) => {
269
- if (!isRecord(snapshot)) return void 0;
270
- const config = isRecord(snapshot.config) ? snapshot.config : void 0;
271
- const configurable = config && isRecord(config.configurable) ? config.configurable : void 0;
530
+ if (!isRecord3(snapshot)) return void 0;
531
+ const config = isRecord3(snapshot.config) ? snapshot.config : void 0;
532
+ const configurable = config && isRecord3(config.configurable) ? config.configurable : void 0;
272
533
  const checkpointId = configurable?.checkpoint_id;
273
534
  return typeof checkpointId === "string" && checkpointId.length > 0 ? checkpointId : void 0;
274
535
  };
275
536
  var getTraceAdapter = (config) => {
276
- const telemetry = isRecord(config.telemetry) ? config.telemetry : void 0;
537
+ const telemetry = isRecord3(config.telemetry) ? config.telemetry : void 0;
277
538
  const traceAdapter = telemetry?.trace ?? config.reasonTrace;
278
- return isRecord(traceAdapter) ? traceAdapter : void 0;
539
+ return isRecord3(traceAdapter) ? traceAdapter : void 0;
279
540
  };
280
541
  async function orchestrateGraphStream({
281
542
  sessionId,
282
543
  runId,
283
544
  graph,
284
545
  state,
285
- config,
546
+ config: initialConfig,
286
547
  selectWorkflow,
548
+ knownWorkflowIds,
287
549
  frameworkAdapter
288
550
  }) {
289
551
  const out = new import_node_stream.PassThrough({ objectMode: true });
552
+ let config = initialConfig;
290
553
  let currentGraph = graph;
291
554
  let currentState = state;
292
555
  let finished = false;
293
556
  let structuredSeq = 0;
294
557
  const debugEnabled = Boolean(config?.features?.tracing);
295
558
  const traceAdapter = getTraceAdapter(config);
296
- const contextMeta = isRecord(config.context) ? config.context : {};
297
- const telemetryConfig = isRecord(config.telemetry) ? config.telemetry : {};
559
+ const contextMeta = isRecord3(config.context) ? config.context : {};
560
+ const telemetryConfig = isRecord3(config.telemetry) ? config.telemetry : {};
561
+ const emitResumeFailure = (resumeError) => {
562
+ const interruptId = typeof config.telemetryInterruptId === "string" ? config.telemetryInterruptId : void 0;
563
+ if (!interruptId) return;
564
+ emitTelemetryEvent({
565
+ config,
566
+ type: "interrupt.resolved",
567
+ correlation: typeof config.telemetryInterruptNodeId === "string" ? { nodeId: config.telemetryInterruptNodeId } : void 0,
568
+ payload: {
569
+ interruptId,
570
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
571
+ resumeOutcome: "failed",
572
+ resumeError: resumeError instanceof Error ? resumeError.message : String(resumeError)
573
+ },
574
+ flush: true
575
+ });
576
+ };
577
+ const telemetryCorrelation = isRecord3(telemetryConfig.correlation) ? telemetryConfig.correlation : {};
298
578
  const runSpanArgs = {
299
579
  name: "kortyx.run",
300
580
  attributes: {
301
581
  ...sessionId ? { sessionId } : {},
302
582
  runId,
303
583
  workflowId: state.currentWorkflow,
584
+ ...typeof telemetryCorrelation.topologyHash === "string" ? { topologyHash: telemetryCorrelation.topologyHash } : {},
585
+ ...typeof telemetryCorrelation.workflowRevisionId === "string" ? { workflowRevisionId: telemetryCorrelation.workflowRevisionId } : {},
304
586
  ...typeof contextMeta.userId === "string" ? { userId: contextMeta.userId } : {},
305
587
  ...typeof contextMeta.tenantId === "string" ? { tenantId: contextMeta.tenantId } : {}
306
588
  },
307
589
  telemetry: {
308
590
  metadata: {
309
- ...isRecord(telemetryConfig.metadata) ? telemetryConfig.metadata : {},
591
+ ...isRecord3(telemetryConfig.metadata) ? telemetryConfig.metadata : {},
310
592
  ...contextMeta
311
593
  },
312
594
  tags: Array.isArray(telemetryConfig.tags) ? telemetryConfig.tags : void 0,
@@ -328,7 +610,9 @@ async function orchestrateGraphStream({
328
610
  }
329
611
  const pending = {
330
612
  to: null,
331
- payload: {}
613
+ payload: {},
614
+ sourceNodeId: void 0,
615
+ sourceWorkflowId: void 0
332
616
  };
333
617
  let lastStatusMsg = "";
334
618
  let lastStatusAt = 0;
@@ -430,6 +714,34 @@ async function orchestrateGraphStream({
430
714
  }))
431
715
  }
432
716
  });
717
+ emitTelemetryEvent({
718
+ config,
719
+ type: "interrupt.created",
720
+ correlation: { nodeId: record.node },
721
+ payload: {
722
+ interruptId: record.requestId,
723
+ requestId: record.requestId,
724
+ kind: record.schema.kind,
725
+ interactionMode: record.schema.kind === "text" ? "freeform" : record.options.length > 0 ? "static-options" : record.schema.schemaId ? "dynamic-picker" : "unknown",
726
+ ...record.schema.schemaId ? { schemaId: record.schema.schemaId } : {},
727
+ ...record.schema.schemaVersion ? { schemaVersion: record.schema.schemaVersion } : {},
728
+ ...typeof record.schema.question === "string" && shouldCaptureTelemetryContent(telemetryConfig.captureContent, "output") ? { question: record.schema.question } : {},
729
+ ...shouldCaptureTelemetryContent(
730
+ telemetryConfig.captureContent,
731
+ "output"
732
+ ) ? {
733
+ options: record.options.map((option) => ({
734
+ id: option.id,
735
+ label: option.label,
736
+ ...option.description ? { description: option.description } : {}
737
+ }))
738
+ } : {},
739
+ optionCount: record.options.length,
740
+ nodeId: record.node,
741
+ expiresAt: new Date(record.createdAt + record.ttlMs).toISOString()
742
+ },
743
+ flush: true
744
+ });
433
745
  wroteHumanInput = true;
434
746
  };
435
747
  const forwardEmit = (event, payload) => {
@@ -595,6 +907,9 @@ async function orchestrateGraphStream({
595
907
  });
596
908
  pending.to = payload?.transitionTo ?? null;
597
909
  pending.payload = payload?.payload ?? {};
910
+ const transition = payload;
911
+ pending.sourceNodeId = typeof transition.node === "string" ? transition.node : void 0;
912
+ pending.sourceWorkflowId = typeof transition.workflow === "string" ? transition.workflow : void 0;
598
913
  return;
599
914
  }
600
915
  if (event === "interrupt") {
@@ -637,7 +952,7 @@ async function orchestrateGraphStream({
637
952
  checkpoint_ns: checkpointNs
638
953
  }
639
954
  });
640
- const values = isRecord(snapshot) && "values" in snapshot ? snapshot.values : snapshot;
955
+ const values = isRecord3(snapshot) && "values" in snapshot ? snapshot.values : snapshot;
641
956
  const checkpointId = getGraphCheckpointId(snapshot) ?? await readLatestGraphCheckpointId();
642
957
  if (!isGraphState(values)) {
643
958
  return checkpointId ? { state: currentState, checkpointId } : null;
@@ -696,11 +1011,40 @@ async function orchestrateGraphStream({
696
1011
  }
697
1012
  const transitionTo = pending.to;
698
1013
  const transitionPayload = pending.payload;
1014
+ const sourceNodeId = pending.sourceNodeId;
1015
+ const sourceWorkflowId = pending.sourceWorkflowId;
699
1016
  pending.to = null;
700
1017
  pending.payload = {};
1018
+ pending.sourceNodeId = void 0;
1019
+ pending.sourceWorkflowId = void 0;
701
1020
  if (transitionTo) {
702
1021
  try {
703
1022
  const nextWorkflow = await selectWorkflow(transitionTo);
1023
+ const sourceTelemetry = isRecord3(config.telemetry) ? config.telemetry : {};
1024
+ config = prepareWorkflowTelemetry({
1025
+ config,
1026
+ workflow: nextWorkflow,
1027
+ runId,
1028
+ sessionId,
1029
+ knownWorkflowIds
1030
+ });
1031
+ const targetTelemetry = isRecord3(config.telemetry) ? config.telemetry : {};
1032
+ emitTelemetryEvent({
1033
+ config,
1034
+ type: "workflow.transitioned",
1035
+ ...sourceNodeId ? { correlation: { nodeId: sourceNodeId } } : {},
1036
+ payload: {
1037
+ sourceNodeId: sourceNodeId ?? "",
1038
+ sourceWorkflowId: sourceWorkflowId ?? String(currentState.currentWorkflow),
1039
+ ...typeof sourceTelemetry.correlation === "object" && sourceTelemetry.correlation && typeof sourceTelemetry.correlation.workflowRevisionId === "string" ? {
1040
+ sourceWorkflowRevisionId: sourceTelemetry.correlation.workflowRevisionId
1041
+ } : {},
1042
+ targetWorkflowId: transitionTo,
1043
+ ...typeof targetTelemetry.correlation === "object" && targetTelemetry.correlation && typeof targetTelemetry.correlation.workflowRevisionId === "string" ? {
1044
+ targetWorkflowRevisionId: targetTelemetry.correlation.workflowRevisionId
1045
+ } : {}
1046
+ }
1047
+ });
704
1048
  const nextGraph = await (0, import_runtime.createExecutionGraph)(nextWorkflow, {
705
1049
  ...config,
706
1050
  emit: forwardEmit
@@ -715,6 +1059,7 @@ async function orchestrateGraphStream({
715
1059
  currentState = {
716
1060
  ...currentState,
717
1061
  currentWorkflow: transitionTo,
1062
+ config,
718
1063
  input: newInput,
719
1064
  data: mergedData,
720
1065
  ui: {}
@@ -804,6 +1149,16 @@ async function orchestrateGraphStream({
804
1149
  turnIndex: checkpoint.turnIndex,
805
1150
  ...checkpoint.label ? { label: checkpoint.label } : {}
806
1151
  });
1152
+ emitTelemetryEvent({
1153
+ config,
1154
+ type: "session.checkpointed",
1155
+ payload: {
1156
+ checkpointId: checkpoint.id,
1157
+ turnIndex: checkpoint.turnIndex,
1158
+ ...checkpoint.parentCheckpointId ? { parentCheckpointId: checkpoint.parentCheckpointId } : {},
1159
+ nodes: checkpoint.nodes
1160
+ }
1161
+ });
807
1162
  } catch (error) {
808
1163
  console.error("[orchestrator] session checkpoint failed", error);
809
1164
  }
@@ -853,6 +1208,16 @@ async function orchestrateGraphStream({
853
1208
  turnIndex: checkpoint.turnIndex,
854
1209
  ...checkpoint.label ? { label: checkpoint.label } : {}
855
1210
  });
1211
+ emitTelemetryEvent({
1212
+ config,
1213
+ type: "session.checkpointed",
1214
+ payload: {
1215
+ checkpointId: checkpoint.id,
1216
+ turnIndex: checkpoint.turnIndex,
1217
+ ...checkpoint.parentCheckpointId ? { parentCheckpointId: checkpoint.parentCheckpointId } : {},
1218
+ nodes: checkpoint.nodes
1219
+ }
1220
+ });
856
1221
  } catch (error) {
857
1222
  console.error("[orchestrator] session checkpoint failed", error);
858
1223
  }
@@ -887,6 +1252,7 @@ async function orchestrateGraphStream({
887
1252
  }) : runLoop(fallbackRunSpan ?? {});
888
1253
  runPromise.catch((err) => {
889
1254
  fallbackRunSpan?.fail?.(err);
1255
+ emitResumeFailure(err);
890
1256
  console.error("[error:orchestrateGraphStream]", err);
891
1257
  out.write({
892
1258
  type: "error",
@@ -899,11 +1265,16 @@ async function orchestrateGraphStream({
899
1265
  }
900
1266
 
901
1267
  // src/interrupt/resume-handler.ts
902
- var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1268
+ var isRecord4 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1269
+ var responseFromSelection = (selected) => {
1270
+ if (selected.length === 0) return void 0;
1271
+ if (selected.length === 1) return selected[0];
1272
+ return JSON.stringify(selected);
1273
+ };
903
1274
  function parseResumeMeta(msg) {
904
1275
  if (!msg || !msg.metadata) return null;
905
1276
  const raw = msg.metadata.resume;
906
- if (!isRecord2(raw)) return null;
1277
+ if (!isRecord4(raw)) return null;
907
1278
  const token = typeof raw.token === "string" ? raw.token : "";
908
1279
  const requestId = typeof raw.requestId === "string" ? raw.requestId : "";
909
1280
  const cancel = raw.cancel === true;
@@ -911,10 +1282,10 @@ function parseResumeMeta(msg) {
911
1282
  const rawSelected = raw.selected;
912
1283
  if (typeof rawSelected === "string") selected = [rawSelected];
913
1284
  else if (Array.isArray(rawSelected)) selected = rawSelected.map(String);
914
- else if (isRecord2(raw.choice) && typeof raw.choice.id === "string")
1285
+ else if (isRecord4(raw.choice) && typeof raw.choice.id === "string")
915
1286
  selected = [raw.choice.id];
916
1287
  else if (Array.isArray(raw.choices))
917
- selected = raw.choices.map((c) => isRecord2(c) ? c.id : void 0).filter((id) => typeof id === "string");
1288
+ selected = raw.choices.map((c) => isRecord4(c) ? c.id : void 0).filter((id) => typeof id === "string");
918
1289
  if (!token || !requestId) return null;
919
1290
  return { token, requestId, selected, cancel };
920
1291
  }
@@ -923,6 +1294,7 @@ async function tryPrepareResumeStream({
923
1294
  sessionId,
924
1295
  config,
925
1296
  selectWorkflow,
1297
+ knownWorkflowIds,
926
1298
  defaultWorkflowId,
927
1299
  applyResumeSelection,
928
1300
  frameworkAdapter
@@ -940,6 +1312,21 @@ async function tryPrepareResumeStream({
940
1312
  }
941
1313
  if (meta.cancel) {
942
1314
  await store.delete(pending.token);
1315
+ emitTelemetryEvent({
1316
+ config,
1317
+ type: "interrupt.cancelled",
1318
+ correlation: {
1319
+ runId: pending.runId,
1320
+ sessionId,
1321
+ workflowId: pending.workflow,
1322
+ nodeId: pending.node
1323
+ },
1324
+ payload: {
1325
+ interruptId: pending.requestId,
1326
+ reason: "cancelled_by_client"
1327
+ },
1328
+ flush: true
1329
+ });
943
1330
  return null;
944
1331
  }
945
1332
  console.log(
@@ -948,10 +1335,10 @@ async function tryPrepareResumeStream({
948
1335
  )} sessionId=${sessionId}`
949
1336
  );
950
1337
  const resumeData = applyResumeSelection ? applyResumeSelection({ pending, selected: meta.selected }) : meta.selected?.length ? { coordinates: String(meta.selected[0]) } : {};
951
- const resumeDataPatch = isRecord2(resumeData) ? resumeData : {};
952
- const pendingMeta = isRecord2(pending.schema?.meta) ? pending.schema.meta : {};
953
- const resumeStatePatch = isRecord2(pendingMeta.__kortyxResumeStatePatch) ? pendingMeta.__kortyxResumeStatePatch : void 0;
954
- const pendingData = isRecord2(pending.state?.data) ? pending.state?.data : {};
1338
+ const resumeDataPatch = isRecord4(resumeData) ? resumeData : {};
1339
+ const pendingMeta = isRecord4(pending.schema?.meta) ? pending.schema.meta : {};
1340
+ const resumeStatePatch = isRecord4(pendingMeta.__kortyxResumeStatePatch) ? pendingMeta.__kortyxResumeStatePatch : void 0;
1341
+ const pendingData = isRecord4(pending.state?.data) ? pending.state?.data : {};
955
1342
  const workflowId = typeof pending.workflow === "string" && pending.workflow.trim() ? pending.workflow : typeof defaultWorkflowId === "string" && defaultWorkflowId.trim() ? defaultWorkflowId : "job-search";
956
1343
  const resumedState = {
957
1344
  // For static breakpoints, resume with null input (set in orchestrator),
@@ -969,6 +1356,14 @@ async function tryPrepareResumeStream({
969
1356
  }
970
1357
  };
971
1358
  const wf = await selectWorkflow(resumedState.currentWorkflow);
1359
+ const telemetryConfig = prepareWorkflowTelemetry({
1360
+ config,
1361
+ workflow: wf,
1362
+ runId: pending.runId,
1363
+ sessionId,
1364
+ knownWorkflowIds
1365
+ });
1366
+ resumedState.config = telemetryConfig;
972
1367
  const resumeUpdate = {};
973
1368
  if (Object.keys(resumeDataPatch).length > 0) {
974
1369
  resumeUpdate.data = {
@@ -983,26 +1378,52 @@ async function tryPrepareResumeStream({
983
1378
  const resumeValue = meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
984
1379
  const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
985
1380
  const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
986
- ...config,
1381
+ ...telemetryConfig,
987
1382
  resume: true,
988
1383
  ...resumeValue !== void 0 ? { resumeValue } : {},
989
1384
  ...resumeCheckpointId ? { resumeCheckpointId } : {},
990
1385
  ...hasResumeUpdate ? { resumeUpdate } : {}
991
1386
  });
992
1387
  await store.delete(pending.token);
1388
+ const response = responseFromSelection(meta.selected);
1389
+ const telemetry = isRecord4(telemetryConfig.telemetry) ? telemetryConfig.telemetry : {};
1390
+ const responseCaptured = Boolean(
1391
+ response && shouldCaptureTelemetryContent(telemetry.captureContent, "input")
1392
+ );
1393
+ emitTelemetryEvent({
1394
+ config: telemetryConfig,
1395
+ type: "interrupt.resolved",
1396
+ correlation: {
1397
+ runId: pending.runId,
1398
+ sessionId,
1399
+ workflowId,
1400
+ nodeId: pending.node
1401
+ },
1402
+ payload: {
1403
+ interruptId: pending.requestId,
1404
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
1405
+ resumeOutcome: "resumed",
1406
+ responseCaptured,
1407
+ ...responseCaptured && response ? { response } : {}
1408
+ },
1409
+ flush: true
1410
+ });
993
1411
  const args = {
994
1412
  sessionId,
995
1413
  runId: pending.runId,
996
1414
  graph: resumedGraph,
997
1415
  state: resumedState,
998
1416
  config: {
999
- ...config,
1417
+ ...telemetryConfig,
1000
1418
  resume: true,
1419
+ telemetryInterruptId: pending.requestId,
1420
+ telemetryInterruptNodeId: pending.node,
1001
1421
  ...resumeValue !== void 0 ? { resumeValue } : {},
1002
1422
  ...resumeCheckpointId ? { resumeCheckpointId } : {},
1003
1423
  ...hasResumeUpdate ? { resumeUpdate } : {}
1004
1424
  },
1005
1425
  selectWorkflow,
1426
+ knownWorkflowIds,
1006
1427
  frameworkAdapter
1007
1428
  };
1008
1429
  const stream = await orchestrateGraphStream(args);
@@ -1030,12 +1451,13 @@ async function streamChat({
1030
1451
  loadRuntimeConfig,
1031
1452
  selectWorkflow,
1032
1453
  workflowRegistry,
1454
+ knownWorkflowIds,
1033
1455
  frameworkAdapter,
1034
1456
  getProvider,
1035
1457
  applyResumeSelection
1036
1458
  }) {
1037
1459
  const config = await loadRuntimeConfig(options);
1038
- const runtimeConfig = {
1460
+ let runtimeConfig = {
1039
1461
  ...config,
1040
1462
  getProvider,
1041
1463
  ...frameworkAdapter ? { checkpointer: frameworkAdapter.checkpointer } : {}
@@ -1078,6 +1500,7 @@ async function streamChat({
1078
1500
  sessionId: resolvedSessionId,
1079
1501
  config: runtimeConfig,
1080
1502
  selectWorkflow: workflowSelector,
1503
+ knownWorkflowIds,
1081
1504
  ...frameworkAdapter ? { frameworkAdapter } : {},
1082
1505
  ...defaultWorkflowId ? { defaultWorkflowId } : {},
1083
1506
  ...applyResumeSelection ? { applyResumeSelection } : {}
@@ -1103,6 +1526,13 @@ async function streamChat({
1103
1526
  }
1104
1527
  const currentWorkflow = requestedWorkflowId ? baseState.currentWorkflow : headCheckpoint?.state.currentWorkflow ?? baseState.currentWorkflow;
1105
1528
  const selectedWorkflow = await workflowSelector(currentWorkflow);
1529
+ runtimeConfig = prepareWorkflowTelemetry({
1530
+ config: runtimeConfig,
1531
+ workflow: selectedWorkflow,
1532
+ runId,
1533
+ sessionId: resolvedSessionId,
1534
+ knownWorkflowIds
1535
+ });
1106
1536
  const graph = await (0, import_runtime3.createExecutionGraph)(selectedWorkflow, runtimeConfig);
1107
1537
  const initialState = headCheckpoint ? {
1108
1538
  ...headCheckpoint.state,
@@ -1115,7 +1545,7 @@ async function streamChat({
1115
1545
  ...runtime.requestedWorkflow ? { requestedWorkflow: runtime.requestedWorkflow } : {},
1116
1546
  ...runtime.priorMessages ? { priorMessages: runtime.priorMessages } : {}
1117
1547
  }
1118
- } : { ...baseState, currentWorkflow };
1548
+ } : { ...baseState, config: runtimeConfig, currentWorkflow };
1119
1549
  const orchestratedStream = await orchestrateGraphStream({
1120
1550
  sessionId: resolvedSessionId,
1121
1551
  runId,
@@ -1123,6 +1553,7 @@ async function streamChat({
1123
1553
  state: initialState,
1124
1554
  config: runtimeConfig,
1125
1555
  selectWorkflow: workflowSelector,
1556
+ knownWorkflowIds,
1126
1557
  ...frameworkAdapter ? { frameworkAdapter } : {}
1127
1558
  });
1128
1559
  return orchestratedStream;
@@ -1236,6 +1667,22 @@ function createAgent(args) {
1236
1667
  fallbackId: resolvedDefaultWorkflowId ?? "general-chat"
1237
1668
  });
1238
1669
  })();
1670
+ const registeredWorkflowsPromise = registryPromise.then(
1671
+ (registry) => registry?.list?.()
1672
+ );
1673
+ const projectTopology = async (options) => {
1674
+ const registeredWorkflows = await registeredWorkflowsPromise;
1675
+ if (!registeredWorkflows?.length) return [];
1676
+ const knownWorkflowIds = registeredWorkflows.map((workflow) => workflow.id);
1677
+ return registeredWorkflows.map(
1678
+ (workflow) => projectWorkflowTopology({
1679
+ workflow,
1680
+ environment: options.environment,
1681
+ service: options.service,
1682
+ knownWorkflowIds
1683
+ })
1684
+ );
1685
+ };
1239
1686
  const streamChat2 = async (messages, options) => {
1240
1687
  const parsedOptions = parseAgentProcessOptions(options);
1241
1688
  const registry = await registryPromise;
@@ -1244,11 +1691,18 @@ function createAgent(args) {
1244
1691
  "createAgent requires workflows, workflowsDir, or workflowRegistry."
1245
1692
  );
1246
1693
  }
1694
+ const registeredWorkflows = await registeredWorkflowsPromise.catch(
1695
+ () => void 0
1696
+ );
1697
+ const knownWorkflowIds = registeredWorkflows?.map(
1698
+ (workflow) => workflow.id
1699
+ );
1247
1700
  return streamChat({
1248
1701
  ...resolvedDefaultWorkflowId ? { defaultWorkflowId: resolvedDefaultWorkflowId } : {},
1249
1702
  messages,
1250
1703
  options: parsedOptions,
1251
1704
  workflowRegistry: registry,
1705
+ ...knownWorkflowIds ? { knownWorkflowIds } : {},
1252
1706
  frameworkAdapter: resolvedFrameworkAdapter,
1253
1707
  getProvider: resolvedGetProvider,
1254
1708
  loadRuntimeConfig: (runtimeOptions) => ({
@@ -1265,6 +1719,7 @@ function createAgent(args) {
1265
1719
  const listCheckpoints = (sessionId) => resolvedFrameworkAdapter.sessionCheckpoints.list(sessionId);
1266
1720
  const getCheckpoint = (id) => resolvedFrameworkAdapter.sessionCheckpoints.get(id);
1267
1721
  const rollbackTo = async (id) => {
1722
+ const target = await resolvedFrameworkAdapter.sessionCheckpoints.get(id);
1268
1723
  const result = await resolvedFrameworkAdapter.sessionCheckpoints.rollbackTo(id);
1269
1724
  const activePendingRequests = await Promise.all(
1270
1725
  result.activePendingRequests.map(
@@ -1284,6 +1739,21 @@ function createAgent(args) {
1284
1739
  (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
1285
1740
  )
1286
1741
  ]);
1742
+ if (target) {
1743
+ emitTelemetryEvent({
1744
+ config: { ...telemetry ? { telemetry } : {} },
1745
+ type: "session.rolled_back",
1746
+ correlation: {
1747
+ runId: target.runId,
1748
+ sessionId: target.sessionId,
1749
+ workflowId: target.workflow
1750
+ },
1751
+ payload: {
1752
+ checkpointId: target.id,
1753
+ invalidatedInterruptIds: result.invalidatedInterruptTokens
1754
+ }
1755
+ });
1756
+ }
1287
1757
  return { ...result, activePendingRequests };
1288
1758
  };
1289
1759
  const fork = async (id, options) => {
@@ -1306,12 +1776,33 @@ function createAgent(args) {
1306
1776
  (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
1307
1777
  )
1308
1778
  );
1779
+ emitTelemetryEvent({
1780
+ config: { ...telemetry ? { telemetry } : {} },
1781
+ type: "session.forked",
1782
+ correlation: {
1783
+ runId: result.checkpoint.runId,
1784
+ sessionId: result.checkpoint.sessionId,
1785
+ workflowId: result.checkpoint.workflow
1786
+ },
1787
+ payload: {
1788
+ newSessionId: result.sessionId,
1789
+ parentSessionId: result.parentSessionId,
1790
+ forkedFromCheckpointId: result.forkedFrom
1791
+ }
1792
+ });
1309
1793
  return {
1310
1794
  ...result,
1311
1795
  checkpoint: { ...result.checkpoint, activePendingRequests }
1312
1796
  };
1313
1797
  };
1314
- return { streamChat: streamChat2, listCheckpoints, getCheckpoint, rollbackTo, fork };
1798
+ return {
1799
+ projectTopology,
1800
+ streamChat: streamChat2,
1801
+ listCheckpoints,
1802
+ getCheckpoint,
1803
+ rollbackTo,
1804
+ fork
1805
+ };
1315
1806
  }
1316
1807
  // Annotate the CommonJS export names for ESM import in node:
1317
1808
  0 && (module.exports = {
@@ -1325,6 +1816,7 @@ function createAgent(args) {
1325
1816
  parseChatRequestBody,
1326
1817
  parseCheckpointRequestBody,
1327
1818
  parseResumeMeta,
1819
+ projectWorkflowTopology,
1328
1820
  streamChat,
1329
1821
  streamChatFromRoute,
1330
1822
  transformGraphStreamForUI,