@aparte/runner-ag-ui 1.6.0 → 1.7.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
@@ -210,23 +210,138 @@ function step(doc, op) {
210
210
  }
211
211
  }
212
212
  function applyStateDelta(state, delta, warn) {
213
- if (!Array.isArray(delta)) {
214
- warn("Failed to apply state patch: STATE_DELTA carried no list of operations; the state is left as it was.");
215
- return state;
213
+ return applyPatch(state, delta, warn, { event: "STATE_DELTA", field: "delta", what: "state" });
214
+ }
215
+ function applyActivityDelta(content, patch, warn) {
216
+ return applyPatch(content, patch, warn, { event: "ACTIVITY_DELTA", field: "patch", what: "activity" });
217
+ }
218
+ function applyPatch(doc, patch, warn, names) {
219
+ const { event, field, what } = names;
220
+ if (!Array.isArray(patch)) {
221
+ warn(`Failed to apply ${what} patch: ${event} carried no list of operations; the ${what} is left as it was.`);
222
+ return doc;
216
223
  }
217
224
  const ops = [];
218
- delta.forEach((op, i) => {
225
+ patch.forEach((op, i) => {
219
226
  if (isContainer(op) && !Array.isArray(op) && OPS.has(String(op["op"]))) ops.push(op);
220
- else warn(`dropped /delta/${i}: ${JSON.stringify(op)} is not a JSON Patch operation.`);
227
+ else warn(`dropped /${field}/${i}: ${JSON.stringify(op)} is not a JSON Patch operation.`);
221
228
  });
222
229
  try {
223
- let doc = structuredClone(state);
224
- for (const op of ops) doc = step(doc, op);
225
- return doc;
230
+ let next = structuredClone(doc);
231
+ for (const op of ops) next = step(next, op);
232
+ return next;
226
233
  } catch (error) {
227
- warn(`Failed to apply state patch: ${error.message}; the state is left as it was.`);
228
- return state;
234
+ warn(`Failed to apply ${what} patch: ${error.message}; the ${what} is left as it was.`);
235
+ return doc;
236
+ }
237
+ }
238
+ function createSubagentHistory(from = []) {
239
+ const records = new Map(from.map((r) => [r.id, JSON.parse(JSON.stringify(r))]));
240
+ const record = (id) => {
241
+ let at = records.get(id);
242
+ if (!at) {
243
+ at = { id, name: "subagent", status: "running", messages: [] };
244
+ records.set(id, at);
245
+ }
246
+ return at;
247
+ };
248
+ const assistantCall = (owner, toolCallId) => {
249
+ for (const m of record(owner).messages) {
250
+ if (m.role !== "assistant") continue;
251
+ const call = m.toolCalls?.find((c) => c.id === toolCallId);
252
+ if (call) return call;
253
+ }
254
+ return void 0;
255
+ };
256
+ return {
257
+ /** Announced (again, by a resumed run: the same invocation going on). */
258
+ start(id, info) {
259
+ const at = record(id);
260
+ Object.assign(at, info);
261
+ at.status = "running";
262
+ delete at.interruptIds;
263
+ },
264
+ end(id, status, interruptIds) {
265
+ const at = record(id);
266
+ at.status = status;
267
+ if (interruptIds?.length) at.interruptIds = interruptIds;
268
+ else delete at.interruptIds;
269
+ },
270
+ text(owner, messageId, delta) {
271
+ const messages = record(owner).messages;
272
+ let at = messages.find((m) => m.role === "assistant" && m.id === messageId);
273
+ if (!at) {
274
+ at = { id: messageId, role: "assistant", content: "" };
275
+ messages.push(at);
276
+ }
277
+ at.content = (at.content ?? "") + delta;
278
+ },
279
+ /** A call joins the assistant message that ends the transcript, as one model step writes text then calls. */
280
+ call(owner, toolCallId, name) {
281
+ const known = assistantCall(owner, toolCallId);
282
+ if (known) {
283
+ if (name) known.function.name = name;
284
+ return;
285
+ }
286
+ const messages = record(owner).messages;
287
+ const last = messages.at(-1);
288
+ let host;
289
+ if (last?.role === "assistant") {
290
+ host = last;
291
+ } else {
292
+ host = { id: `${toolCallId}:call`, role: "assistant" };
293
+ messages.push(host);
294
+ }
295
+ host.toolCalls = [...host.toolCalls ?? [], { id: toolCallId, type: "function", function: { name, arguments: "" } }];
296
+ },
297
+ args(owner, toolCallId, delta) {
298
+ const call = assistantCall(owner, toolCallId);
299
+ if (call) call.function.arguments += delta;
300
+ },
301
+ result(owner, messageId, toolCallId, content) {
302
+ record(owner).messages.push({ id: messageId ?? `${toolCallId}:result`, role: "tool", toolCallId, content });
303
+ },
304
+ /** The run's sub-agents, `tanstack` set on each when the run was TanStack AI's. */
305
+ records(tanstack = false) {
306
+ return [...records.values()].map((r) => tanstack ? { ...r, tanstack: true } : r);
307
+ }
308
+ };
309
+ }
310
+ function subagentMessages(records, answers = /* @__PURE__ */ new Map()) {
311
+ return records.filter((r) => r.tanstack).flatMap((r) => withAnswers(r.messages, answers).map((m, i) => {
312
+ if (m.role !== "assistant" && m.role !== "tool") return m;
313
+ const card = i === 0 ? {
314
+ metadata: {
315
+ tanstack: {
316
+ subagent: {
317
+ name: r.name,
318
+ status: r.status,
319
+ ...r.description !== void 0 ? { description: r.description } : {},
320
+ ...r.parentToolCallId !== void 0 ? { parentToolCallId: r.parentToolCallId } : {},
321
+ ...r.parentSubagentRunId !== void 0 ? { parentSubagentRunId: r.parentSubagentRunId } : {},
322
+ ...r.interruptIds ? { interruptIds: r.interruptIds } : {},
323
+ ...r.metadata ? { metadata: r.metadata } : {}
324
+ }
325
+ }
326
+ }
327
+ } : {};
328
+ return { ...m, subagentRunId: r.id, ...card };
329
+ }));
330
+ }
331
+ function withAnswers(messages, answers) {
332
+ if (answers.size === 0) return [...messages];
333
+ const out = [];
334
+ for (let i = 0; i < messages.length; i++) {
335
+ const m = messages[i];
336
+ out.push(m);
337
+ if (m.role !== "assistant" || !m.toolCalls?.length) continue;
338
+ while (messages[i + 1]?.role === "tool") out.push(messages[++i]);
339
+ for (const call of m.toolCalls) {
340
+ const answer = answers.get(call.id);
341
+ if (answer) out.push(answer);
342
+ }
229
343
  }
344
+ return out;
230
345
  }
231
346
  function compare(a, b) {
232
347
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
@@ -577,14 +692,21 @@ const PARENT = "";
577
692
  function by(owner) {
578
693
  return owner ? { agentId: owner } : {};
579
694
  }
695
+ function routerPlanOf(event) {
696
+ const tanstack = event["metadata"]?.tanstack;
697
+ const steps = tanstack?.subagentPlan?.steps;
698
+ if (!Array.isArray(steps)) return void 0;
699
+ const names = steps.map((step2) => Array.isArray(step2?.names) ? step2.names.filter((n) => typeof n === "string" && n !== "") : []).filter((step2) => step2.length > 0);
700
+ return names.length ? names : void 0;
701
+ }
580
702
  function interruptKind(interrupt) {
581
703
  if (interrupt.reason === "tool_call" || interrupt.reason === "confirmation" || interrupt.reason === "approval_required") return "approval";
582
704
  if (/client_tool/.test(interrupt.reason)) return "external";
583
705
  return void 0;
584
706
  }
585
- function readUsage(value) {
707
+ function readUsage(value, pick = "last") {
586
708
  const entries = (Array.isArray(value) ? value : value && typeof value === "object" ? [value] : []).filter((u) => Boolean(u) && typeof u === "object");
587
- const last = entries.at(-1);
709
+ const last = pick === "first" ? entries[0] : entries.at(-1);
588
710
  if (!last) return void 0;
589
711
  const num = (key) => typeof last[key] === "number" ? last[key] : 0;
590
712
  const cached = num("cachedInputTokens");
@@ -662,16 +784,23 @@ function createAgUiRunner(options) {
662
784
  const runId = `aparte-run-${uuid()}`;
663
785
  const interrupted = new Set((ticket?.interrupts ?? []).map((i) => i.toolCallId));
664
786
  const messages = toAgUiMessages(request.messages, flatten, warn);
787
+ const pausedAgents = Array.isArray(ticket?.subagents) ? ticket.subagents : [];
788
+ const sentBack = new Set(pausedAgents.filter((r) => r.tanstack).map((r) => r.id));
789
+ const answers = /* @__PURE__ */ new Map();
665
790
  for (const [toolCallId, outcome] of Object.entries(outcomes)) {
666
791
  if (interrupted.has(toolCallId)) continue;
667
792
  if (outcome.kind === "decision" && outcome.approved) continue;
668
- messages.push({
793
+ const answer = {
669
794
  id: `aparte-result-${toolCallId}`,
670
795
  role: "tool",
671
796
  toolCallId,
672
797
  content: outcome.kind === "failed" ? `Error: ${outcome.error}` : outcome.kind === "decision" ? rejectionText(outcome) : outcome.content
673
- });
798
+ };
799
+ const owner = ticket?.agents?.[toolCallId];
800
+ if (owner !== void 0 && sentBack.has(owner)) answers.set(toolCallId, answer);
801
+ else messages.push(answer);
674
802
  }
803
+ messages.push(...subagentMessages(pausedAgents, answers));
675
804
  const left = ticket ? [] : unanswered.get(threadId) ?? [];
676
805
  unanswered.delete(threadId);
677
806
  const resume = ticket?.interrupts.length ? ticket.interrupts.map((interrupt) => resumeItem(interrupt, interrupt.kind === "unanswerable" ? void 0 : outcomes[interrupt.toolCallId])) : left.length ? left.map((interruptId) => ({ interruptId, status: "cancelled" })) : void 0;
@@ -730,6 +859,9 @@ function createAgUiRunner(options) {
730
859
  const lastCall = /* @__PURE__ */ new Map();
731
860
  const ticketAgents = new Map(Object.entries(ticket?.agents ?? {}).filter((entry) => typeof entry[1] === "string"));
732
861
  const agents = /* @__PURE__ */ new Map();
862
+ const transcripts = createSubagentHistory(pausedAgents);
863
+ let tanstackRun = false;
864
+ let ownStep = false;
733
865
  const tagOf = (event) => typeof event["subagentRunId"] === "string" ? event["subagentRunId"] : void 0;
734
866
  const messageOwner = (event) => {
735
867
  let id = typeof event["messageId"] === "string" ? event["messageId"] : void 0;
@@ -758,10 +890,40 @@ function createAgUiRunner(options) {
758
890
  agents.set(agentId, { open: true });
759
891
  emit({ type: "agent-start", agentId });
760
892
  };
893
+ const openSteps = [];
894
+ const closeSteps = (owner) => {
895
+ for (let i = openSteps.length - 1; i >= 0; i -= 1) {
896
+ const step2 = openSteps[i];
897
+ if (owner !== void 0 && step2.owner !== owner) continue;
898
+ openSteps.splice(i, 1);
899
+ emit({ type: "step-end", name: step2.name, ...by(step2.owner) });
900
+ }
901
+ };
902
+ track.closeSteps = closeSteps;
903
+ const activities = /* @__PURE__ */ new Map();
904
+ const showActivity = (id, at) => {
905
+ activities.set(id, at);
906
+ emit({ type: "activity", id, activityType: at.activityType, content: at.content, ...by(at.owner) });
907
+ };
908
+ const paused = ticket?.plan;
909
+ let routerPlan = paused && Array.isArray(paused.steps) ? { steps: paused.steps, status: new Map(Object.entries(paused.status ?? {})) } : void 0;
910
+ const planId = typeof paused?.id === "string" ? paused.id : `router-plan:${runId}`;
911
+ const tickRouterPlan = (name, status) => {
912
+ const at = name === void 0 ? -1 : routerPlan?.steps.findIndex((step2) => step2.includes(name)) ?? -1;
913
+ if (!routerPlan || at < 0) return;
914
+ if (status === "in-progress") {
915
+ for (const before of routerPlan.steps.slice(0, at).flat()) if (routerPlan.status.get(before) !== "failed") routerPlan.status.set(before, "done");
916
+ }
917
+ routerPlan.status.set(name, status);
918
+ const steps = routerPlan.steps.flat().map((title) => ({ title, status: routerPlan.status.get(title) ?? "pending" }));
919
+ showActivity(planId, { activityType: "plan", content: { steps }, owner: PARENT });
920
+ };
761
921
  const endAgent = (agentId, status, detail = {}) => {
922
+ closeSteps(agentId);
762
923
  const agent = agents.get(agentId);
763
924
  if (agent) agent.open = false;
764
925
  emit({ type: "agent-end", agentId, status, ...detail, ...agent?.parent ? { parentAgentId: agent.parent } : {} });
926
+ if (agent && !agent.parent) tickRouterPlan(agent.name, status === "waiting" ? "blocked" : status);
765
927
  };
766
928
  const failOpenAgents = (error) => {
767
929
  for (const [agentId, agent] of [...agents].reverse()) {
@@ -844,6 +1006,8 @@ function createAgUiRunner(options) {
844
1006
  if (/^(TEXT_MESSAGE|REASONING|TOOL_CALL)_/.test(event.type)) endedOnFinish = false;
845
1007
  const tag = tagOf(event);
846
1008
  if (tag !== void 0 && !event.type.startsWith("SUBAGENT_")) meet(tag);
1009
+ const metadata = event.metadata;
1010
+ if (metadata && typeof metadata === "object" && typeof metadata.tanstack === "object") tanstackRun = true;
847
1011
  switch (event.type) {
848
1012
  case "RUN_STARTED": {
849
1013
  if (runError) {
@@ -866,18 +1030,24 @@ function createAgUiRunner(options) {
866
1030
  }
867
1031
  case "TEXT_MESSAGE_START":
868
1032
  case "REASONING_START":
869
- case "REASONING_MESSAGE_START":
870
- nextStep(messageOwner(event));
1033
+ case "REASONING_MESSAGE_START": {
1034
+ const owner = messageOwner(event);
1035
+ if (owner === PARENT) ownStep = true;
1036
+ nextStep(owner);
871
1037
  break;
1038
+ }
872
1039
  case "TEXT_MESSAGE_CONTENT":
873
1040
  case "TEXT_MESSAGE_CHUNK": {
874
1041
  const owner = messageOwner(event);
875
1042
  const delta = typeof event["delta"] === "string" ? event["delta"] : "";
1043
+ if (owner === PARENT) ownStep = true;
876
1044
  if (delta) {
877
1045
  nextStep(owner);
878
1046
  emit({ type: "text-delta", delta, ...by(owner) });
879
1047
  lane(owner).textOpen = true;
880
1048
  }
1049
+ const id = typeof event["messageId"] === "string" ? event["messageId"] : chunkMessages.get(`${event.type}\0${tag ?? PARENT}`);
1050
+ if (owner !== PARENT && delta && id !== void 0) transcripts.text(owner, id, delta);
881
1051
  break;
882
1052
  }
883
1053
  case "TEXT_MESSAGE_END":
@@ -896,12 +1066,20 @@ function createAgUiRunner(options) {
896
1066
  case "TOOL_CALL_START": {
897
1067
  const id = String(event["toolCallId"]);
898
1068
  if (!calls.has(id)) lastCall.set(tag ?? PARENT, id);
899
- calls.set(id, { name: String(event["toolCallName"] ?? ""), args: "", owner: newCallOwner(event) });
1069
+ const owner = newCallOwner(event);
1070
+ const name = String(event["toolCallName"] ?? "");
1071
+ calls.set(id, { name, args: "", owner });
1072
+ if (owner === PARENT) ownStep = true;
1073
+ else transcripts.call(owner, id, name);
900
1074
  break;
901
1075
  }
902
1076
  case "TOOL_CALL_ARGS": {
903
- const call = calls.get(String(event["toolCallId"]));
904
- if (call && typeof event["delta"] === "string") call.args += event["delta"];
1077
+ const id = String(event["toolCallId"]);
1078
+ const call = calls.get(id);
1079
+ if (call && typeof event["delta"] === "string") {
1080
+ call.args += event["delta"];
1081
+ if (call.owner !== PARENT) transcripts.args(call.owner, id, event["delta"]);
1082
+ }
905
1083
  break;
906
1084
  }
907
1085
  case "TOOL_CALL_CHUNK": {
@@ -915,6 +1093,12 @@ function createAgUiRunner(options) {
915
1093
  }
916
1094
  if (typeof event["toolCallName"] === "string" && event["toolCallName"]) call.name = event["toolCallName"];
917
1095
  if (typeof event["delta"] === "string") call.args += event["delta"];
1096
+ if (call.owner === PARENT) {
1097
+ ownStep = true;
1098
+ } else {
1099
+ transcripts.call(call.owner, id, call.name);
1100
+ if (typeof event["delta"] === "string") transcripts.args(call.owner, id, event["delta"]);
1101
+ }
918
1102
  break;
919
1103
  }
920
1104
  case "TOOL_CALL_END": {
@@ -928,6 +1112,7 @@ function createAgUiRunner(options) {
928
1112
  answered.add(toolCallId);
929
1113
  const owner = callOwner(toolCallId) ?? tag ?? PARENT;
930
1114
  const content = resultText(event["content"]);
1115
+ if (owner !== PARENT) transcripts.result(owner, typeof event["messageId"] === "string" ? event["messageId"] : void 0, toolCallId, content);
931
1116
  const meta = event["metadata"];
932
1117
  if (meta?.tanstack?.state === "output-error") {
933
1118
  emit({ type: "tool-failed", toolCallId, error: content, ...by(owner) });
@@ -942,11 +1127,27 @@ function createAgUiRunner(options) {
942
1127
  const toolCallId = typeof event["parentToolCallId"] === "string" ? event["parentToolCallId"] : void 0;
943
1128
  const parent = typeof event["parentSubagentRunId"] === "string" ? event["parentSubagentRunId"] : void 0;
944
1129
  if (toolCallId) startTool(toolCallId);
945
- agents.set(agentId, { open: true, ...parent ? { parent } : {} });
1130
+ const name = String(event["name"]);
1131
+ agents.set(agentId, { open: true, name, ...parent ? { parent } : {} });
1132
+ const metadata2 = event["metadata"];
1133
+ transcripts.start(agentId, {
1134
+ name,
1135
+ ...typeof event["description"] === "string" ? { description: event["description"] } : {},
1136
+ ...toolCallId ? { parentToolCallId: toolCallId } : {},
1137
+ ...parent ? { parentSubagentRunId: parent } : {},
1138
+ ...metadata2 && typeof metadata2 === "object" && !Array.isArray(metadata2) ? { metadata: metadata2 } : {}
1139
+ });
1140
+ if (!parent) {
1141
+ if (!routerPlan) {
1142
+ const steps = routerPlanOf(event);
1143
+ if (steps) routerPlan = { steps, status: /* @__PURE__ */ new Map() };
1144
+ }
1145
+ tickRouterPlan(name, "in-progress");
1146
+ }
946
1147
  emit({
947
1148
  type: "agent-start",
948
1149
  agentId,
949
- name: String(event["name"]),
1150
+ name,
950
1151
  ...typeof event["description"] === "string" ? { description: event["description"] } : {},
951
1152
  ...toolCallId ? { toolCallId } : {},
952
1153
  ...parent ? { parentAgentId: parent } : {}
@@ -958,11 +1159,16 @@ function createAgUiRunner(options) {
958
1159
  const agentId = String(event["subagentRunId"]);
959
1160
  flush(agentId);
960
1161
  for (const [id, call] of calls) if (call.owner === agentId) startTool(id);
1162
+ const outcome = event["outcome"];
961
1163
  if (event.type === "SUBAGENT_ERROR") {
1164
+ transcripts.end(agentId, "error");
962
1165
  endAgent(agentId, "failed", { error: typeof event["message"] === "string" && event["message"] ? event["message"] : "The sub-agent failed." });
963
- } else if (event["outcome"]?.type === "suspended") {
1166
+ } else if (outcome?.type === "suspended") {
1167
+ const ids = Array.isArray(outcome.interruptIds) ? outcome.interruptIds.filter((id) => typeof id === "string") : [];
1168
+ transcripts.end(agentId, "suspended", ids);
964
1169
  endAgent(agentId, "waiting");
965
1170
  } else {
1171
+ transcripts.end(agentId, "finished");
966
1172
  endAgent(agentId, "done", event["result"] !== void 0 ? { result: resultText(event["result"]) } : {});
967
1173
  }
968
1174
  break;
@@ -987,7 +1193,9 @@ function createAgUiRunner(options) {
987
1193
  case "RUN_FINISHED": {
988
1194
  for (const id of calls.keys()) startTool(id);
989
1195
  for (const owner of lanes.keys()) flush(owner);
990
- usage = readUsage(event["usage"]) ?? usage;
1196
+ if (agents.size === 0) usage = readUsage(event["usage"]) ?? usage;
1197
+ else if (ownStep) usage = readUsage(event["usage"], "first") ?? usage;
1198
+ ownStep = false;
991
1199
  lane(PARENT).stepEnded = true;
992
1200
  for (const at of lanes.values()) at.stepEnded = true;
993
1201
  endedOnFinish = true;
@@ -997,8 +1205,52 @@ function createAgUiRunner(options) {
997
1205
  };
998
1206
  break;
999
1207
  }
1208
+ // A step is a name with a start and an end, its owner's: shown while it lasts
1209
+ // (the waiting indicator, a sub-agent's line). Steps may nest and overlap, and
1210
+ // one ends the latest open step of its name under the same owner.
1211
+ case "STEP_STARTED": {
1212
+ const owner = tag ?? PARENT;
1213
+ const name = String(event["stepName"] ?? "");
1214
+ openSteps.push({ owner, name });
1215
+ emit({ type: "step-start", name, ...by(owner) });
1216
+ break;
1217
+ }
1218
+ case "STEP_FINISHED": {
1219
+ const owner = tag ?? PARENT;
1220
+ const name = String(event["stepName"] ?? "");
1221
+ for (let i = openSteps.length - 1; i >= 0; i -= 1) {
1222
+ if (openSteps[i].owner !== owner || openSteps[i].name !== name) continue;
1223
+ openSteps.splice(i, 1);
1224
+ emit({ type: "step-end", name, ...by(owner) });
1225
+ break;
1226
+ }
1227
+ break;
1228
+ }
1229
+ // An activity is structured progress, drawn by the renderer the app registered for
1230
+ // its type. A snapshot overwrites it unless it says `replace: false` (AG-UI 1.0:
1231
+ // "only an explicit false asks a consumer to leave what is already there") — its
1232
+ // type included; a delta patches what is held. Either way the page gets it whole.
1233
+ case "ACTIVITY_SNAPSHOT": {
1234
+ const id = String(event["messageId"] ?? "");
1235
+ const held = activities.get(id);
1236
+ if (held && event["replace"] === false) break;
1237
+ showActivity(id, { activityType: String(event["activityType"] ?? ""), content: event["content"], owner: held?.owner ?? tag ?? PARENT });
1238
+ break;
1239
+ }
1240
+ case "ACTIVITY_DELTA": {
1241
+ const id = String(event["messageId"] ?? "");
1242
+ const held = activities.get(id);
1243
+ if (!held) {
1244
+ warn(`ACTIVITY_DELTA for "${id}", an activity no ACTIVITY_SNAPSHOT created: skipped.`);
1245
+ break;
1246
+ }
1247
+ const content = applyActivityDelta(held.content, event["patch"], warn);
1248
+ if (content !== held.content) showActivity(id, { ...held, content });
1249
+ break;
1250
+ }
1000
1251
  }
1001
1252
  }
1253
+ closeSteps();
1002
1254
  if (opts.signal.aborted) return usage;
1003
1255
  if (runError) {
1004
1256
  for (const [toolCallId, call] of started) if (!answered.has(toolCallId)) emit({ type: "tool-aborted", toolCallId, ...by(call.owner) });
@@ -1053,7 +1305,9 @@ function createAgUiRunner(options) {
1053
1305
  threadId,
1054
1306
  runId: finished.runId,
1055
1307
  interrupts: [...interrupts, ...cancelled],
1056
- ...owned.length ? { agents: Object.fromEntries(owned) } : {}
1308
+ ...owned.length ? { agents: Object.fromEntries(owned) } : {},
1309
+ ...transcripts.records().length ? { subagents: transcripts.records(tanstackRun) } : {},
1310
+ ...routerPlan ? { plan: { id: planId, steps: routerPlan.steps, status: Object.fromEntries(routerPlan.status) } } : {}
1057
1311
  };
1058
1312
  track.suspended = true;
1059
1313
  emit({ type: "run-suspended", pending, ...usage ? { usage } : {}, ticket: JSON.stringify(next) });
@@ -1069,6 +1323,7 @@ function createAgUiRunner(options) {
1069
1323
  return await runOnce(opts, track);
1070
1324
  } finally {
1071
1325
  if (track.threadId && !stableThread && !track.suspended) states.delete(track.threadId);
1326
+ track.closeSteps?.();
1072
1327
  }
1073
1328
  };
1074
1329
  }