@kortyx/agent 0.14.1 → 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
@@ -21,11 +21,15 @@ var index_exports = {};
21
21
  __export(index_exports, {
22
22
  createAgent: () => createAgent,
23
23
  createChatRouteHandler: () => createChatRouteHandler,
24
+ createCheckpointRouteHandler: () => createCheckpointRouteHandler,
24
25
  extractLatestUserMessage: () => extractLatestUserMessage,
25
26
  handleChatRequestBody: () => handleChatRequestBody,
27
+ handleCheckpointRequestBody: () => handleCheckpointRequestBody,
26
28
  orchestrateGraphStream: () => orchestrateGraphStream,
27
29
  parseChatRequestBody: () => parseChatRequestBody,
30
+ parseCheckpointRequestBody: () => parseCheckpointRequestBody,
28
31
  parseResumeMeta: () => parseResumeMeta,
32
+ projectWorkflowTopology: () => projectWorkflowTopology,
29
33
  streamChat: () => streamChat,
30
34
  streamChatFromRoute: () => streamChatFromRoute,
31
35
  transformGraphStreamForUI: () => transformGraphStreamForUI,
@@ -50,6 +54,25 @@ var chatRequestBodySchema = import_zod.z.looseObject({
50
54
  context: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
51
55
  messages: import_zod.z.array(chatMessageSchema)
52
56
  });
57
+ var checkpointRequestBodySchema = import_zod.z.discriminatedUnion("action", [
58
+ import_zod.z.object({
59
+ action: import_zod.z.literal("list"),
60
+ sessionId: import_zod.z.string().min(1)
61
+ }).strict(),
62
+ import_zod.z.object({
63
+ action: import_zod.z.literal("get"),
64
+ checkpointId: import_zod.z.string().min(1)
65
+ }).strict(),
66
+ import_zod.z.object({
67
+ action: import_zod.z.literal("rollback"),
68
+ checkpointId: import_zod.z.string().min(1)
69
+ }).strict(),
70
+ import_zod.z.object({
71
+ action: import_zod.z.literal("fork"),
72
+ checkpointId: import_zod.z.string().min(1),
73
+ newSessionId: import_zod.z.string().min(1).optional()
74
+ }).strict()
75
+ ]);
53
76
  var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
54
77
  function parseChatRequestBody(value) {
55
78
  const parsed = chatRequestBodySchema.safeParse(value);
@@ -67,6 +90,12 @@ function parseChatRequestBody(value) {
67
90
  messages: parsed.data.messages
68
91
  };
69
92
  }
93
+ function parseCheckpointRequestBody(value) {
94
+ const parsed = checkpointRequestBodySchema.safeParse(value);
95
+ if (parsed.success) return parsed.data;
96
+ const [firstIssue] = parsed.error.issues;
97
+ throw new Error(firstIssue.message);
98
+ }
70
99
  async function handleChatRequestBody(args) {
71
100
  const { agent, body } = args;
72
101
  const stream = await agent.streamChat(body.messages, {
@@ -105,6 +134,38 @@ function createChatRouteHandler(args) {
105
134
  }
106
135
  };
107
136
  }
137
+ async function handleCheckpointRequestBody(args) {
138
+ const { agent, body } = args;
139
+ const result = body.action === "list" ? await agent.listCheckpoints(body.sessionId) : body.action === "get" ? await agent.getCheckpoint(body.checkpointId) : body.action === "rollback" ? await agent.rollbackTo(body.checkpointId) : await agent.fork(body.checkpointId, {
140
+ ...body.newSessionId ? { newSessionId: body.newSessionId } : {}
141
+ });
142
+ return new Response(JSON.stringify(result), {
143
+ headers: {
144
+ "content-type": "application/json"
145
+ }
146
+ });
147
+ }
148
+ function createCheckpointRouteHandler(args) {
149
+ const { agent, errorStatus = 400 } = args;
150
+ return async function POST(request) {
151
+ try {
152
+ const body = parseCheckpointRequestBody(await request.json());
153
+ return await handleCheckpointRequestBody({ agent, body });
154
+ } catch (error) {
155
+ return new Response(
156
+ JSON.stringify({
157
+ error: toErrorMessage(error)
158
+ }),
159
+ {
160
+ status: errorStatus,
161
+ headers: {
162
+ "content-type": "application/json"
163
+ }
164
+ }
165
+ );
166
+ }
167
+ };
168
+ }
108
169
 
109
170
  // src/adapters/http-client.ts
110
171
  var import_browser = require("@kortyx/stream/browser");
@@ -127,6 +188,266 @@ var import_providers = require("@kortyx/providers");
127
188
  var import_runtime4 = require("@kortyx/runtime");
128
189
  var import_zod2 = require("zod");
129
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
+
130
451
  // src/chat/process-chat.ts
131
452
  var import_runtime3 = require("@kortyx/runtime");
132
453
 
@@ -139,10 +460,12 @@ var import_runtime = require("@kortyx/runtime");
139
460
  var import_langgraph = require("@langchain/langgraph");
140
461
 
141
462
  // src/stream/transform-graph-stream-for-ui.ts
463
+ var isGraphStateOutput = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && "currentWorkflow" in value;
142
464
  async function* transformGraphStreamForUI(stream, options = {}) {
143
465
  const { debug = false, emitStatus = debug } = options;
144
466
  const startedNodes = /* @__PURE__ */ new Set();
145
467
  const endedNodes = /* @__PURE__ */ new Set();
468
+ let latestGraphState = null;
146
469
  for await (const event of stream) {
147
470
  const { event: type, name, data } = event ?? {};
148
471
  if (debug) console.log(`[debug:event]`, JSON.stringify(event, null, 2));
@@ -167,6 +490,7 @@ async function* transformGraphStreamForUI(stream, options = {}) {
167
490
  JSON.stringify(output, null, 2)
168
491
  );
169
492
  if (!output || nodeName?.startsWith("ChannelWrite")) break;
493
+ if (isGraphStateOutput(output)) latestGraphState = output;
170
494
  if (nodeName !== "__start__" && nodeName !== "__end__") {
171
495
  if (!endedNodes.has(nodeName)) {
172
496
  if (emitStatus) {
@@ -183,7 +507,11 @@ async function* transformGraphStreamForUI(stream, options = {}) {
183
507
  case "on_graph_end": {
184
508
  if (debug)
185
509
  console.log(`[debug:on_graph_end]`, JSON.stringify(data, null, 2));
186
- yield { type: "done", data: data?.output ?? null };
510
+ const output = data?.output;
511
+ yield {
512
+ type: "done",
513
+ data: isGraphStateOutput(output) ? output : latestGraphState
514
+ };
187
515
  break;
188
516
  }
189
517
  default:
@@ -196,42 +524,71 @@ async function* transformGraphStreamForUI(stream, options = {}) {
196
524
  }
197
525
 
198
526
  // src/orchestrator.ts
199
- var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
527
+ var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
528
+ var isGraphState = (value) => isRecord3(value) && typeof value.currentWorkflow === "string";
529
+ var getGraphCheckpointId = (snapshot) => {
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;
533
+ const checkpointId = configurable?.checkpoint_id;
534
+ return typeof checkpointId === "string" && checkpointId.length > 0 ? checkpointId : void 0;
535
+ };
200
536
  var getTraceAdapter = (config) => {
201
- const telemetry = isRecord(config.telemetry) ? config.telemetry : void 0;
537
+ const telemetry = isRecord3(config.telemetry) ? config.telemetry : void 0;
202
538
  const traceAdapter = telemetry?.trace ?? config.reasonTrace;
203
- return isRecord(traceAdapter) ? traceAdapter : void 0;
539
+ return isRecord3(traceAdapter) ? traceAdapter : void 0;
204
540
  };
205
541
  async function orchestrateGraphStream({
206
542
  sessionId,
207
543
  runId,
208
544
  graph,
209
545
  state,
210
- config,
546
+ config: initialConfig,
211
547
  selectWorkflow,
548
+ knownWorkflowIds,
212
549
  frameworkAdapter
213
550
  }) {
214
551
  const out = new import_node_stream.PassThrough({ objectMode: true });
552
+ let config = initialConfig;
215
553
  let currentGraph = graph;
216
554
  let currentState = state;
217
555
  let finished = false;
218
556
  let structuredSeq = 0;
219
557
  const debugEnabled = Boolean(config?.features?.tracing);
220
558
  const traceAdapter = getTraceAdapter(config);
221
- const contextMeta = isRecord(config.context) ? config.context : {};
222
- 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 : {};
223
578
  const runSpanArgs = {
224
579
  name: "kortyx.run",
225
580
  attributes: {
226
581
  ...sessionId ? { sessionId } : {},
227
582
  runId,
228
583
  workflowId: state.currentWorkflow,
584
+ ...typeof telemetryCorrelation.topologyHash === "string" ? { topologyHash: telemetryCorrelation.topologyHash } : {},
585
+ ...typeof telemetryCorrelation.workflowRevisionId === "string" ? { workflowRevisionId: telemetryCorrelation.workflowRevisionId } : {},
229
586
  ...typeof contextMeta.userId === "string" ? { userId: contextMeta.userId } : {},
230
587
  ...typeof contextMeta.tenantId === "string" ? { tenantId: contextMeta.tenantId } : {}
231
588
  },
232
589
  telemetry: {
233
590
  metadata: {
234
- ...isRecord(telemetryConfig.metadata) ? telemetryConfig.metadata : {},
591
+ ...isRecord3(telemetryConfig.metadata) ? telemetryConfig.metadata : {},
235
592
  ...contextMeta
236
593
  },
237
594
  tags: Array.isArray(telemetryConfig.tags) ? telemetryConfig.tags : void 0,
@@ -240,6 +597,10 @@ async function orchestrateGraphStream({
240
597
  }
241
598
  };
242
599
  const namespacesUsed = /* @__PURE__ */ new Set();
600
+ const touchedNodes = /* @__PURE__ */ new Set();
601
+ const structuredStreamIds = /* @__PURE__ */ new Set();
602
+ const activePendingRequests = /* @__PURE__ */ new Map();
603
+ const pendingRequestWrites = [];
243
604
  try {
244
605
  const sid = config?.session?.id;
245
606
  if (sid && typeof sid === "string") {
@@ -249,7 +610,9 @@ async function orchestrateGraphStream({
249
610
  }
250
611
  const pending = {
251
612
  to: null,
252
- payload: {}
613
+ payload: {},
614
+ sourceNodeId: void 0,
615
+ sourceWorkflowId: void 0
253
616
  };
254
617
  let lastStatusMsg = "";
255
618
  let lastStatusAt = 0;
@@ -315,10 +678,12 @@ async function orchestrateGraphStream({
315
678
  createdAt: Date.now(),
316
679
  ttlMs: pendingTtlMs
317
680
  };
681
+ activePendingRequests.set(record.token, record);
318
682
  if (pendingStore) {
319
- pendingStore.save(record).catch((error) => {
683
+ const savePromise = pendingStore.save(record).catch((error) => {
320
684
  console.error("[orchestrator] failed to save pending request", error);
321
685
  });
686
+ pendingRequestWrites.push(savePromise);
322
687
  }
323
688
  const clientMeta = record.schema.meta && typeof record.schema.meta === "object" && !Array.isArray(record.schema.meta) ? Object.fromEntries(
324
689
  Object.entries(record.schema.meta).filter(
@@ -349,6 +714,34 @@ async function orchestrateGraphStream({
349
714
  }))
350
715
  }
351
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
+ });
352
745
  wroteHumanInput = true;
353
746
  };
354
747
  const forwardEmit = (event, payload) => {
@@ -376,6 +769,7 @@ async function orchestrateGraphStream({
376
769
  if (event === "text-start") {
377
770
  const node = payload?.node;
378
771
  if (!node) return;
772
+ touchedNodes.add(node);
379
773
  out.write({
380
774
  type: "text-start",
381
775
  node,
@@ -389,6 +783,7 @@ async function orchestrateGraphStream({
389
783
  const node = payload?.node;
390
784
  const delta = String(payload?.delta ?? "");
391
785
  if (!node || !delta) return;
786
+ touchedNodes.add(node);
392
787
  appendAccumulatedOutput(payload, node, delta);
393
788
  out.write({
394
789
  type: "text-delta",
@@ -403,6 +798,7 @@ async function orchestrateGraphStream({
403
798
  if (event === "text-end") {
404
799
  const node = payload?.node;
405
800
  if (!node) return;
801
+ touchedNodes.add(node);
406
802
  out.write({
407
803
  type: "text-end",
408
804
  node,
@@ -426,6 +822,7 @@ async function orchestrateGraphStream({
426
822
  ...payloadObj.opId ? { opId: payloadObj.opId } : {},
427
823
  ...payloadObj.input !== void 0 ? { input: payloadObj.input } : {}
428
824
  });
825
+ if (payloadObj.node) touchedNodes.add(payloadObj.node);
429
826
  return;
430
827
  }
431
828
  if (event === "tool-call-result") {
@@ -444,6 +841,7 @@ async function orchestrateGraphStream({
444
841
  ...payloadObj.structuredContent !== void 0 ? { structuredContent: payloadObj.structuredContent } : {},
445
842
  ...typeof payloadObj.isError === "boolean" ? { isError: payloadObj.isError } : {}
446
843
  });
844
+ if (payloadObj.node) touchedNodes.add(payloadObj.node);
447
845
  return;
448
846
  }
449
847
  if (event === "tool-call-error") {
@@ -460,21 +858,26 @@ async function orchestrateGraphStream({
460
858
  ...payloadObj.opId ? { opId: payloadObj.opId } : {},
461
859
  message: String(payloadObj.message ?? "")
462
860
  });
861
+ if (payloadObj.node) touchedNodes.add(payloadObj.node);
463
862
  return;
464
863
  }
465
864
  if (event === "message") {
466
865
  const node = payload?.node;
467
866
  const text = String(payload?.content ?? "");
867
+ if (node) touchedNodes.add(node);
468
868
  out.write({ type: "message", node, content: text });
469
869
  return;
470
870
  }
471
871
  if (event === "structured_data") {
472
872
  const payloadObj = payload;
473
873
  const kind = payloadObj.kind === "set" || payloadObj.kind === "append" || payloadObj.kind === "text-delta" || payloadObj.kind === "final" ? payloadObj.kind : "final";
874
+ const streamId = typeof payloadObj.streamId === "string" && payloadObj.streamId ? payloadObj.streamId : nextStructuredStreamId();
875
+ structuredStreamIds.add(streamId);
876
+ if (payloadObj.node) touchedNodes.add(payloadObj.node);
474
877
  out.write({
475
878
  type: "structured-data",
476
879
  node: payloadObj.node,
477
- streamId: typeof payloadObj.streamId === "string" && payloadObj.streamId ? payloadObj.streamId : nextStructuredStreamId(),
880
+ streamId,
478
881
  dataType: typeof payloadObj.dataType === "string" && payloadObj.dataType ? payloadObj.dataType : "generic",
479
882
  kind,
480
883
  ...typeof payloadObj.schemaId === "string" ? { schemaId: payloadObj.schemaId } : {},
@@ -504,10 +907,14 @@ async function orchestrateGraphStream({
504
907
  });
505
908
  pending.to = payload?.transitionTo ?? null;
506
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;
507
913
  return;
508
914
  }
509
915
  if (event === "interrupt") {
510
916
  const p = payload;
917
+ if (typeof p?.node === "string") touchedNodes.add(p.node);
511
918
  const local = {
512
919
  node: p?.node,
513
920
  workflow: p?.workflow,
@@ -522,11 +929,39 @@ async function orchestrateGraphStream({
522
929
  const runLoop = async (runTraceSpan) => {
523
930
  while (true) {
524
931
  let workflowFinalState = null;
932
+ let workflowFinalGraphCheckpointId;
933
+ let sawWorkflowDone = false;
934
+ let workflowDoneData;
525
935
  currentGraph.config = currentGraph.config || {};
526
936
  currentGraph.config.emit = forwardEmit;
527
937
  const threadId = currentGraph.config?.session?.id || sessionId || "anonymous-session";
528
- const checkpointNs = String(currentState.currentWorkflow || "default");
938
+ const checkpointNs = "";
529
939
  namespacesUsed.add(checkpointNs);
940
+ const readLatestGraphCheckpointId = async () => {
941
+ const checkpointer = currentGraph.config?.checkpointer;
942
+ return checkpointer?.getLatestCheckpointId?.(runId, checkpointNs);
943
+ };
944
+ const readLatestGraphSnapshot = async () => {
945
+ if (typeof currentGraph.getState !== "function") {
946
+ const checkpointId2 = await readLatestGraphCheckpointId();
947
+ return checkpointId2 ? { state: currentState, checkpointId: checkpointId2 } : null;
948
+ }
949
+ const snapshot = await currentGraph.getState({
950
+ configurable: {
951
+ thread_id: runId,
952
+ checkpoint_ns: checkpointNs
953
+ }
954
+ });
955
+ const values = isRecord3(snapshot) && "values" in snapshot ? snapshot.values : snapshot;
956
+ const checkpointId = getGraphCheckpointId(snapshot) ?? await readLatestGraphCheckpointId();
957
+ if (!isGraphState(values)) {
958
+ return checkpointId ? { state: currentState, checkpointId } : null;
959
+ }
960
+ return {
961
+ state: values,
962
+ ...checkpointId ? { checkpointId } : {}
963
+ };
964
+ };
530
965
  if (debugEnabled) {
531
966
  out.write({
532
967
  type: "status",
@@ -536,13 +971,14 @@ async function orchestrateGraphStream({
536
971
  const isResume = Boolean(currentGraph.config?.resume);
537
972
  const resumeUpdate = currentGraph.config?.resumeUpdate;
538
973
  const resumeValue = currentGraph.config?.resumeValue;
974
+ const resumeCheckpointId = currentGraph.config?.resumeCheckpointId;
539
975
  const invokeState = isResume ? resumeValue !== void 0 && resumeUpdate ? new import_langgraph.Command({ resume: resumeValue, update: resumeUpdate }) : resumeValue !== void 0 ? new import_langgraph.Command({ resume: resumeValue }) : resumeUpdate ? new import_langgraph.Command({ update: resumeUpdate }) : null : currentState;
540
976
  const runtimeStream = currentGraph.streamEvents(invokeState, {
541
977
  version: "v2",
542
978
  configurable: {
543
979
  thread_id: runId,
544
- // Use a stable namespace so checkpoints survive across recompiles of same workflow
545
- checkpoint_ns: checkpointNs
980
+ checkpoint_ns: checkpointNs,
981
+ ...isResume && resumeCheckpointId ? { checkpoint_id: resumeCheckpointId } : {}
546
982
  }
547
983
  });
548
984
  if (debugEnabled) {
@@ -557,20 +993,58 @@ async function orchestrateGraphStream({
557
993
  });
558
994
  for await (const chunk of uiStream) {
559
995
  if (finished) break;
560
- out.write(chunk);
996
+ if ("node" in chunk && typeof chunk.node === "string") {
997
+ touchedNodes.add(chunk.node);
998
+ }
561
999
  if (chunk.type === "done") {
562
- workflowFinalState = chunk.data ?? null;
563
- break;
1000
+ sawWorkflowDone = true;
1001
+ workflowDoneData = chunk.data;
1002
+ continue;
564
1003
  }
1004
+ out.write(chunk);
565
1005
  }
566
1006
  if (finished) return;
1007
+ if (sawWorkflowDone) {
1008
+ const graphSnapshot = await readLatestGraphSnapshot();
1009
+ workflowFinalGraphCheckpointId = graphSnapshot?.checkpointId;
1010
+ workflowFinalState = isGraphState(workflowDoneData) ? workflowDoneData : graphSnapshot?.state ?? currentState;
1011
+ }
567
1012
  const transitionTo = pending.to;
568
1013
  const transitionPayload = pending.payload;
1014
+ const sourceNodeId = pending.sourceNodeId;
1015
+ const sourceWorkflowId = pending.sourceWorkflowId;
569
1016
  pending.to = null;
570
1017
  pending.payload = {};
1018
+ pending.sourceNodeId = void 0;
1019
+ pending.sourceWorkflowId = void 0;
571
1020
  if (transitionTo) {
572
1021
  try {
573
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
+ });
574
1048
  const nextGraph = await (0, import_runtime.createExecutionGraph)(nextWorkflow, {
575
1049
  ...config,
576
1050
  emit: forwardEmit
@@ -585,6 +1059,7 @@ async function orchestrateGraphStream({
585
1059
  currentState = {
586
1060
  ...currentState,
587
1061
  currentWorkflow: transitionTo,
1062
+ config,
588
1063
  input: newInput,
589
1064
  data: mergedData,
590
1065
  ui: {}
@@ -607,12 +1082,27 @@ async function orchestrateGraphStream({
607
1082
  }
608
1083
  if (workflowFinalState) {
609
1084
  if (workflowFinalState && pendingRecordToken) {
1085
+ if (pendingRequestWrites.length > 0) {
1086
+ await Promise.all(pendingRequestWrites);
1087
+ }
610
1088
  if (pendingStore) {
611
1089
  await pendingStore.update(pendingRecordToken, {
612
- state: workflowFinalState
1090
+ state: workflowFinalState,
1091
+ ...workflowFinalGraphCheckpointId ? { graphCheckpointId: workflowFinalGraphCheckpointId } : {}
1092
+ });
1093
+ }
1094
+ const pendingRequest = activePendingRequests.get(pendingRecordToken);
1095
+ if (pendingRequest) {
1096
+ activePendingRequests.set(pendingRecordToken, {
1097
+ ...pendingRequest,
1098
+ state: workflowFinalState,
1099
+ ...workflowFinalGraphCheckpointId ? { graphCheckpointId: workflowFinalGraphCheckpointId } : {}
613
1100
  });
614
1101
  }
615
1102
  }
1103
+ if (!pendingRecordToken && pendingRequestWrites.length > 0) {
1104
+ await Promise.all(pendingRequestWrites);
1105
+ }
616
1106
  const shouldKeepFrameworkState = Boolean(pendingRecordToken) || Boolean(workflowFinalState?.awaitingHumanInput);
617
1107
  if (!shouldKeepFrameworkState) {
618
1108
  try {
@@ -637,15 +1127,106 @@ async function orchestrateGraphStream({
637
1127
  "kortyx.run.final_workflow": String(currentState.currentWorkflow)
638
1128
  });
639
1129
  runTraceSpan.end?.(runTraceEndArgs());
1130
+ const resolvedSessionId2 = config?.session?.id || sessionId || "";
1131
+ if (resolvedSessionId2 && frameworkAdapter?.sessionCheckpoints) {
1132
+ try {
1133
+ const checkpoint = await frameworkAdapter.sessionCheckpoints.append(
1134
+ {
1135
+ sessionId: resolvedSessionId2,
1136
+ runId,
1137
+ ...workflowFinalGraphCheckpointId ? { graphCheckpointId: workflowFinalGraphCheckpointId } : {},
1138
+ workflow: String(workflowFinalState.currentWorkflow),
1139
+ state: workflowFinalState,
1140
+ nodes: Array.from(touchedNodes),
1141
+ structuredStreamIds: Array.from(structuredStreamIds),
1142
+ pendingRequests: Array.from(activePendingRequests.values())
1143
+ }
1144
+ );
1145
+ out.write({
1146
+ type: "checkpoint",
1147
+ id: checkpoint.id,
1148
+ sessionId: checkpoint.sessionId,
1149
+ turnIndex: checkpoint.turnIndex,
1150
+ ...checkpoint.label ? { label: checkpoint.label } : {}
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
+ });
1162
+ } catch (error) {
1163
+ console.error("[orchestrator] session checkpoint failed", error);
1164
+ }
1165
+ }
640
1166
  out.write({ type: "done", data: workflowFinalState });
641
1167
  out.end();
642
1168
  return;
643
1169
  }
1170
+ const naturalGraphSnapshot = await readLatestGraphSnapshot();
1171
+ const naturalFinalState = naturalGraphSnapshot?.state ?? currentState;
1172
+ if (pendingRequestWrites.length > 0) {
1173
+ await Promise.all(pendingRequestWrites);
1174
+ }
1175
+ if (pendingRecordToken && naturalGraphSnapshot?.checkpointId) {
1176
+ if (pendingStore) {
1177
+ await pendingStore.update(pendingRecordToken, {
1178
+ state: naturalFinalState,
1179
+ graphCheckpointId: naturalGraphSnapshot.checkpointId
1180
+ });
1181
+ }
1182
+ const pendingRequest = activePendingRequests.get(pendingRecordToken);
1183
+ if (pendingRequest) {
1184
+ activePendingRequests.set(pendingRecordToken, {
1185
+ ...pendingRequest,
1186
+ state: naturalFinalState,
1187
+ graphCheckpointId: naturalGraphSnapshot.checkpointId
1188
+ });
1189
+ }
1190
+ }
1191
+ const resolvedSessionId = config?.session?.id || sessionId || "";
1192
+ if (resolvedSessionId && frameworkAdapter?.sessionCheckpoints) {
1193
+ try {
1194
+ const checkpoint = await frameworkAdapter.sessionCheckpoints.append({
1195
+ sessionId: resolvedSessionId,
1196
+ runId,
1197
+ ...naturalGraphSnapshot?.checkpointId ? { graphCheckpointId: naturalGraphSnapshot.checkpointId } : {},
1198
+ workflow: String(naturalFinalState.currentWorkflow),
1199
+ state: naturalFinalState,
1200
+ nodes: Array.from(touchedNodes),
1201
+ structuredStreamIds: Array.from(structuredStreamIds),
1202
+ pendingRequests: Array.from(activePendingRequests.values())
1203
+ });
1204
+ out.write({
1205
+ type: "checkpoint",
1206
+ id: checkpoint.id,
1207
+ sessionId: checkpoint.sessionId,
1208
+ turnIndex: checkpoint.turnIndex,
1209
+ ...checkpoint.label ? { label: checkpoint.label } : {}
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
+ });
1221
+ } catch (error) {
1222
+ console.error("[orchestrator] session checkpoint failed", error);
1223
+ }
1224
+ }
644
1225
  runTraceSpan.setAttributes?.({
645
- "kortyx.run.final_workflow": String(currentState.currentWorkflow)
1226
+ "kortyx.run.final_workflow": String(naturalFinalState.currentWorkflow)
646
1227
  });
647
1228
  runTraceSpan.end?.(runTraceEndArgs());
648
- out.write({ type: "done" });
1229
+ out.write({ type: "done", data: naturalFinalState });
649
1230
  out.end();
650
1231
  return;
651
1232
  }
@@ -671,6 +1252,7 @@ async function orchestrateGraphStream({
671
1252
  }) : runLoop(fallbackRunSpan ?? {});
672
1253
  runPromise.catch((err) => {
673
1254
  fallbackRunSpan?.fail?.(err);
1255
+ emitResumeFailure(err);
674
1256
  console.error("[error:orchestrateGraphStream]", err);
675
1257
  out.write({
676
1258
  type: "error",
@@ -683,11 +1265,16 @@ async function orchestrateGraphStream({
683
1265
  }
684
1266
 
685
1267
  // src/interrupt/resume-handler.ts
686
- 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
+ };
687
1274
  function parseResumeMeta(msg) {
688
1275
  if (!msg || !msg.metadata) return null;
689
1276
  const raw = msg.metadata.resume;
690
- if (!isRecord2(raw)) return null;
1277
+ if (!isRecord4(raw)) return null;
691
1278
  const token = typeof raw.token === "string" ? raw.token : "";
692
1279
  const requestId = typeof raw.requestId === "string" ? raw.requestId : "";
693
1280
  const cancel = raw.cancel === true;
@@ -695,10 +1282,10 @@ function parseResumeMeta(msg) {
695
1282
  const rawSelected = raw.selected;
696
1283
  if (typeof rawSelected === "string") selected = [rawSelected];
697
1284
  else if (Array.isArray(rawSelected)) selected = rawSelected.map(String);
698
- else if (isRecord2(raw.choice) && typeof raw.choice.id === "string")
1285
+ else if (isRecord4(raw.choice) && typeof raw.choice.id === "string")
699
1286
  selected = [raw.choice.id];
700
1287
  else if (Array.isArray(raw.choices))
701
- 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");
702
1289
  if (!token || !requestId) return null;
703
1290
  return { token, requestId, selected, cancel };
704
1291
  }
@@ -707,6 +1294,7 @@ async function tryPrepareResumeStream({
707
1294
  sessionId,
708
1295
  config,
709
1296
  selectWorkflow,
1297
+ knownWorkflowIds,
710
1298
  defaultWorkflowId,
711
1299
  applyResumeSelection,
712
1300
  frameworkAdapter
@@ -724,6 +1312,21 @@ async function tryPrepareResumeStream({
724
1312
  }
725
1313
  if (meta.cancel) {
726
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
+ });
727
1330
  return null;
728
1331
  }
729
1332
  console.log(
@@ -732,10 +1335,10 @@ async function tryPrepareResumeStream({
732
1335
  )} sessionId=${sessionId}`
733
1336
  );
734
1337
  const resumeData = applyResumeSelection ? applyResumeSelection({ pending, selected: meta.selected }) : meta.selected?.length ? { coordinates: String(meta.selected[0]) } : {};
735
- const resumeDataPatch = isRecord2(resumeData) ? resumeData : {};
736
- const pendingMeta = isRecord2(pending.schema?.meta) ? pending.schema.meta : {};
737
- const resumeStatePatch = isRecord2(pendingMeta.__kortyxResumeStatePatch) ? pendingMeta.__kortyxResumeStatePatch : void 0;
738
- 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 : {};
739
1342
  const workflowId = typeof pending.workflow === "string" && pending.workflow.trim() ? pending.workflow : typeof defaultWorkflowId === "string" && defaultWorkflowId.trim() ? defaultWorkflowId : "job-search";
740
1343
  const resumedState = {
741
1344
  // For static breakpoints, resume with null input (set in orchestrator),
@@ -753,6 +1356,14 @@ async function tryPrepareResumeStream({
753
1356
  }
754
1357
  };
755
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;
756
1367
  const resumeUpdate = {};
757
1368
  if (Object.keys(resumeDataPatch).length > 0) {
758
1369
  resumeUpdate.data = {
@@ -765,25 +1376,54 @@ async function tryPrepareResumeStream({
765
1376
  }
766
1377
  const hasResumeUpdate = Object.keys(resumeUpdate).length > 0;
767
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;
1379
+ const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
768
1380
  const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
769
- ...config,
1381
+ ...telemetryConfig,
770
1382
  resume: true,
771
1383
  ...resumeValue !== void 0 ? { resumeValue } : {},
1384
+ ...resumeCheckpointId ? { resumeCheckpointId } : {},
772
1385
  ...hasResumeUpdate ? { resumeUpdate } : {}
773
1386
  });
774
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
+ });
775
1411
  const args = {
776
1412
  sessionId,
777
1413
  runId: pending.runId,
778
1414
  graph: resumedGraph,
779
1415
  state: resumedState,
780
1416
  config: {
781
- ...config,
1417
+ ...telemetryConfig,
782
1418
  resume: true,
1419
+ telemetryInterruptId: pending.requestId,
1420
+ telemetryInterruptNodeId: pending.node,
783
1421
  ...resumeValue !== void 0 ? { resumeValue } : {},
1422
+ ...resumeCheckpointId ? { resumeCheckpointId } : {},
784
1423
  ...hasResumeUpdate ? { resumeUpdate } : {}
785
1424
  },
786
1425
  selectWorkflow,
1426
+ knownWorkflowIds,
787
1427
  frameworkAdapter
788
1428
  };
789
1429
  const stream = await orchestrateGraphStream(args);
@@ -811,12 +1451,13 @@ async function streamChat({
811
1451
  loadRuntimeConfig,
812
1452
  selectWorkflow,
813
1453
  workflowRegistry,
1454
+ knownWorkflowIds,
814
1455
  frameworkAdapter,
815
1456
  getProvider,
816
1457
  applyResumeSelection
817
1458
  }) {
818
1459
  const config = await loadRuntimeConfig(options);
819
- const runtimeConfig = {
1460
+ let runtimeConfig = {
820
1461
  ...config,
821
1462
  getProvider,
822
1463
  ...frameworkAdapter ? { checkpointer: frameworkAdapter.checkpointer } : {}
@@ -859,22 +1500,60 @@ async function streamChat({
859
1500
  sessionId: resolvedSessionId,
860
1501
  config: runtimeConfig,
861
1502
  selectWorkflow: workflowSelector,
1503
+ knownWorkflowIds,
862
1504
  ...frameworkAdapter ? { frameworkAdapter } : {},
863
1505
  ...defaultWorkflowId ? { defaultWorkflowId } : {},
864
1506
  ...applyResumeSelection ? { applyResumeSelection } : {}
865
1507
  });
866
1508
  if (resumeStream) return resumeStream;
867
1509
  const runId = (0, import_runtime3.makeRequestId)("run");
868
- const currentWorkflow = baseState.currentWorkflow;
1510
+ let headCheckpoint = !isResumeRequest && frameworkAdapter?.sessionCheckpoints ? await frameworkAdapter.sessionCheckpoints.getHead(resolvedSessionId) : null;
1511
+ if (!isResumeRequest && !headCheckpoint && frameworkAdapter?.sessionCheckpoints) {
1512
+ headCheckpoint = await frameworkAdapter.sessionCheckpoints.append({
1513
+ sessionId: resolvedSessionId,
1514
+ runId,
1515
+ workflow: String(baseState.currentWorkflow),
1516
+ state: {
1517
+ ...baseState,
1518
+ input: "",
1519
+ awaitingHumanInput: false
1520
+ },
1521
+ nodes: [],
1522
+ structuredStreamIds: [],
1523
+ pendingRequests: [],
1524
+ label: "session start"
1525
+ });
1526
+ }
1527
+ const currentWorkflow = requestedWorkflowId ? baseState.currentWorkflow : headCheckpoint?.state.currentWorkflow ?? baseState.currentWorkflow;
869
1528
  const selectedWorkflow = await workflowSelector(currentWorkflow);
1529
+ runtimeConfig = prepareWorkflowTelemetry({
1530
+ config: runtimeConfig,
1531
+ workflow: selectedWorkflow,
1532
+ runId,
1533
+ sessionId: resolvedSessionId,
1534
+ knownWorkflowIds
1535
+ });
870
1536
  const graph = await (0, import_runtime3.createExecutionGraph)(selectedWorkflow, runtimeConfig);
1537
+ const initialState = headCheckpoint ? {
1538
+ ...headCheckpoint.state,
1539
+ input,
1540
+ config: runtimeConfig,
1541
+ currentWorkflow,
1542
+ awaitingHumanInput: false,
1543
+ runtime: {
1544
+ ...headCheckpoint.state.runtime ?? {},
1545
+ ...runtime.requestedWorkflow ? { requestedWorkflow: runtime.requestedWorkflow } : {},
1546
+ ...runtime.priorMessages ? { priorMessages: runtime.priorMessages } : {}
1547
+ }
1548
+ } : { ...baseState, config: runtimeConfig, currentWorkflow };
871
1549
  const orchestratedStream = await orchestrateGraphStream({
872
1550
  sessionId: resolvedSessionId,
873
1551
  runId,
874
1552
  graph,
875
- state: { ...baseState, currentWorkflow },
1553
+ state: initialState,
876
1554
  config: runtimeConfig,
877
1555
  selectWorkflow: workflowSelector,
1556
+ knownWorkflowIds,
878
1557
  ...frameworkAdapter ? { frameworkAdapter } : {}
879
1558
  });
880
1559
  return orchestratedStream;
@@ -928,6 +1607,32 @@ var parseAgentProcessOptions = (value) => {
928
1607
  if (value === void 0) return void 0;
929
1608
  return parseSchema(agentProcessOptionsSchema, value);
930
1609
  };
1610
+ var hydratePendingGraphCheckpoint = async (frameworkAdapter, request) => {
1611
+ if (request.graphCheckpointId) return request;
1612
+ const checkpointer = frameworkAdapter.checkpointer;
1613
+ const graphCheckpointId = await checkpointer?.getLatestCheckpointId?.(
1614
+ request.runId,
1615
+ request.workflow
1616
+ ) ?? await checkpointer?.getLatestCheckpointId?.(request.runId, "");
1617
+ return graphCheckpointId ? { ...request, graphCheckpointId } : request;
1618
+ };
1619
+ var clearPendingGraphWrites = async (frameworkAdapter, request) => {
1620
+ if (!request.graphCheckpointId) return;
1621
+ const checkpointer = frameworkAdapter.checkpointer;
1622
+ if (!checkpointer?.deleteCheckpointWrites) return;
1623
+ await Promise.all([
1624
+ checkpointer.deleteCheckpointWrites(
1625
+ request.runId,
1626
+ request.workflow,
1627
+ request.graphCheckpointId
1628
+ ),
1629
+ checkpointer.deleteCheckpointWrites(
1630
+ request.runId,
1631
+ "",
1632
+ request.graphCheckpointId
1633
+ )
1634
+ ]);
1635
+ };
931
1636
  function createAgent(args) {
932
1637
  const parsedArgs = parseCreateAgentArgs(args);
933
1638
  const {
@@ -962,6 +1667,22 @@ function createAgent(args) {
962
1667
  fallbackId: resolvedDefaultWorkflowId ?? "general-chat"
963
1668
  });
964
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
+ };
965
1686
  const streamChat2 = async (messages, options) => {
966
1687
  const parsedOptions = parseAgentProcessOptions(options);
967
1688
  const registry = await registryPromise;
@@ -970,11 +1691,18 @@ function createAgent(args) {
970
1691
  "createAgent requires workflows, workflowsDir, or workflowRegistry."
971
1692
  );
972
1693
  }
1694
+ const registeredWorkflows = await registeredWorkflowsPromise.catch(
1695
+ () => void 0
1696
+ );
1697
+ const knownWorkflowIds = registeredWorkflows?.map(
1698
+ (workflow) => workflow.id
1699
+ );
973
1700
  return streamChat({
974
1701
  ...resolvedDefaultWorkflowId ? { defaultWorkflowId: resolvedDefaultWorkflowId } : {},
975
1702
  messages,
976
1703
  options: parsedOptions,
977
1704
  workflowRegistry: registry,
1705
+ ...knownWorkflowIds ? { knownWorkflowIds } : {},
978
1706
  frameworkAdapter: resolvedFrameworkAdapter,
979
1707
  getProvider: resolvedGetProvider,
980
1708
  loadRuntimeConfig: (runtimeOptions) => ({
@@ -988,17 +1716,107 @@ function createAgent(args) {
988
1716
  })
989
1717
  });
990
1718
  };
991
- return { streamChat: streamChat2 };
1719
+ const listCheckpoints = (sessionId) => resolvedFrameworkAdapter.sessionCheckpoints.list(sessionId);
1720
+ const getCheckpoint = (id) => resolvedFrameworkAdapter.sessionCheckpoints.get(id);
1721
+ const rollbackTo = async (id) => {
1722
+ const target = await resolvedFrameworkAdapter.sessionCheckpoints.get(id);
1723
+ const result = await resolvedFrameworkAdapter.sessionCheckpoints.rollbackTo(id);
1724
+ const activePendingRequests = await Promise.all(
1725
+ result.activePendingRequests.map(
1726
+ (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
1727
+ )
1728
+ );
1729
+ await Promise.all(
1730
+ activePendingRequests.map(
1731
+ (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
1732
+ )
1733
+ );
1734
+ await Promise.all([
1735
+ ...result.invalidatedInterruptTokens.map(
1736
+ (token) => resolvedFrameworkAdapter.pendingRequests.delete(token)
1737
+ ),
1738
+ ...activePendingRequests.map(
1739
+ (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
1740
+ )
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
+ }
1757
+ return { ...result, activePendingRequests };
1758
+ };
1759
+ const fork = async (id, options) => {
1760
+ const result = await resolvedFrameworkAdapter.sessionCheckpoints.fork(
1761
+ id,
1762
+ options
1763
+ );
1764
+ const activePendingRequests = await Promise.all(
1765
+ result.checkpoint.activePendingRequests.map(
1766
+ (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
1767
+ )
1768
+ );
1769
+ await Promise.all(
1770
+ activePendingRequests.map(
1771
+ (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
1772
+ )
1773
+ );
1774
+ await Promise.all(
1775
+ activePendingRequests.map(
1776
+ (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
1777
+ )
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
+ });
1793
+ return {
1794
+ ...result,
1795
+ checkpoint: { ...result.checkpoint, activePendingRequests }
1796
+ };
1797
+ };
1798
+ return {
1799
+ projectTopology,
1800
+ streamChat: streamChat2,
1801
+ listCheckpoints,
1802
+ getCheckpoint,
1803
+ rollbackTo,
1804
+ fork
1805
+ };
992
1806
  }
993
1807
  // Annotate the CommonJS export names for ESM import in node:
994
1808
  0 && (module.exports = {
995
1809
  createAgent,
996
1810
  createChatRouteHandler,
1811
+ createCheckpointRouteHandler,
997
1812
  extractLatestUserMessage,
998
1813
  handleChatRequestBody,
1814
+ handleCheckpointRequestBody,
999
1815
  orchestrateGraphStream,
1000
1816
  parseChatRequestBody,
1817
+ parseCheckpointRequestBody,
1001
1818
  parseResumeMeta,
1819
+ projectWorkflowTopology,
1002
1820
  streamChat,
1003
1821
  streamChatFromRoute,
1004
1822
  transformGraphStreamForUI,