@aparte/runner-ag-ui 1.5.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,24 +210,139 @@ 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;
229
236
  }
230
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
+ }
343
+ }
344
+ return out;
345
+ }
231
346
  function compare(a, b) {
232
347
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
233
348
  const d = (a[i] ?? 0) - (b[i] ?? 0);
@@ -357,10 +472,10 @@ function createVerifier(relaxed) {
357
472
  };
358
473
  const stillOpen = () => [...text.keys()].map((id) => `text message "${id}"`).concat([...reasoning.keys()].map((id) => `reasoning message "${id}"`)).concat([...tools.keys()].map((id) => `tool call "${id}"`)).concat([...steps].map((key) => `step "${key.split("\0")[1]}"`)).concat([...subagents].map((id) => `subagent "${id}"`));
359
474
  const agrees = (id, opened, event) => {
360
- const by = event["subagentRunId"];
361
- if (by === void 0) return;
475
+ const by2 = event["subagentRunId"];
476
+ if (by2 === void 0) return;
362
477
  const at = opened.get(id);
363
- if (at !== void 0 && at !== by) throw malformed(`${event.type} for "${id}" is attributed to "${String(by)}", but "${id}" started attributed to "${String(at)}"`);
478
+ if (at !== void 0 && at !== by2) throw malformed(`${event.type} for "${id}" is attributed to "${String(by2)}", but "${id}" started attributed to "${String(at)}"`);
364
479
  };
365
480
  const verify = ((event) => {
366
481
  const type = event.type;
@@ -573,14 +688,25 @@ function readTicket(token) {
573
688
  return void 0;
574
689
  }
575
690
  }
691
+ const PARENT = "";
692
+ function by(owner) {
693
+ return owner ? { agentId: owner } : {};
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
+ }
576
702
  function interruptKind(interrupt) {
577
703
  if (interrupt.reason === "tool_call" || interrupt.reason === "confirmation" || interrupt.reason === "approval_required") return "approval";
578
704
  if (/client_tool/.test(interrupt.reason)) return "external";
579
705
  return void 0;
580
706
  }
581
- function readUsage(value) {
707
+ function readUsage(value, pick = "last") {
582
708
  const entries = (Array.isArray(value) ? value : value && typeof value === "object" ? [value] : []).filter((u) => Boolean(u) && typeof u === "object");
583
- const last = entries.at(-1);
709
+ const last = pick === "first" ? entries[0] : entries.at(-1);
584
710
  if (!last) return void 0;
585
711
  const num = (key) => typeof last[key] === "number" ? last[key] : 0;
586
712
  const cached = num("cachedInputTokens");
@@ -658,16 +784,23 @@ function createAgUiRunner(options) {
658
784
  const runId = `aparte-run-${uuid()}`;
659
785
  const interrupted = new Set((ticket?.interrupts ?? []).map((i) => i.toolCallId));
660
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();
661
790
  for (const [toolCallId, outcome] of Object.entries(outcomes)) {
662
791
  if (interrupted.has(toolCallId)) continue;
663
792
  if (outcome.kind === "decision" && outcome.approved) continue;
664
- messages.push({
793
+ const answer = {
665
794
  id: `aparte-result-${toolCallId}`,
666
795
  role: "tool",
667
796
  toolCallId,
668
797
  content: outcome.kind === "failed" ? `Error: ${outcome.error}` : outcome.kind === "decision" ? rejectionText(outcome) : outcome.content
669
- });
798
+ };
799
+ const owner = ticket?.agents?.[toolCallId];
800
+ if (owner !== void 0 && sentBack.has(owner)) answers.set(toolCallId, answer);
801
+ else messages.push(answer);
670
802
  }
803
+ messages.push(...subagentMessages(pausedAgents, answers));
671
804
  const left = ticket ? [] : unanswered.get(threadId) ?? [];
672
805
  unanswered.delete(threadId);
673
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;
@@ -708,32 +841,112 @@ function createAgUiRunner(options) {
708
841
  const answered = /* @__PURE__ */ new Set();
709
842
  const rejected = /* @__PURE__ */ new Set();
710
843
  const settledByPage = /* @__PURE__ */ new Set();
711
- let textOpen = false;
712
844
  let runError;
713
845
  let usage;
714
846
  let finished;
715
847
  const emit = opts.emitter;
848
+ const lanes = /* @__PURE__ */ new Map();
849
+ const lane = (owner) => {
850
+ let at = lanes.get(owner);
851
+ if (!at) {
852
+ at = { textOpen: false, stepEnded: false };
853
+ lanes.set(owner, at);
854
+ }
855
+ return at;
856
+ };
857
+ const messageOwners = /* @__PURE__ */ new Map();
858
+ const chunkMessages = /* @__PURE__ */ new Map();
859
+ const lastCall = /* @__PURE__ */ new Map();
860
+ const ticketAgents = new Map(Object.entries(ticket?.agents ?? {}).filter((entry) => typeof entry[1] === "string"));
861
+ const agents = /* @__PURE__ */ new Map();
862
+ const transcripts = createSubagentHistory(pausedAgents);
863
+ let tanstackRun = false;
864
+ let ownStep = false;
865
+ const tagOf = (event) => typeof event["subagentRunId"] === "string" ? event["subagentRunId"] : void 0;
866
+ const messageOwner = (event) => {
867
+ let id = typeof event["messageId"] === "string" ? event["messageId"] : void 0;
868
+ if (event.type.endsWith("_CHUNK")) {
869
+ const key = `${event.type}\0${tagOf(event) ?? PARENT}`;
870
+ if (id !== void 0) chunkMessages.set(key, id);
871
+ else id = chunkMessages.get(key);
872
+ }
873
+ const opened = id !== void 0 ? messageOwners.get(id) : void 0;
874
+ if (opened !== void 0) return opened;
875
+ const owner = tagOf(event) ?? PARENT;
876
+ if (id !== void 0) messageOwners.set(id, owner);
877
+ return owner;
878
+ };
879
+ const newCallOwner = (event) => tagOf(event) ?? (typeof event["parentMessageId"] === "string" ? messageOwners.get(event["parentMessageId"]) : void 0) ?? PARENT;
880
+ const callOwner = (toolCallId) => calls.get(toolCallId)?.owner ?? ticketAgents.get(toolCallId);
881
+ const flush = (owner) => {
882
+ const at = lanes.get(owner);
883
+ if (at?.textOpen) {
884
+ emit({ type: "text-flush", ...by(owner) });
885
+ at.textOpen = false;
886
+ }
887
+ };
888
+ const meet = (agentId) => {
889
+ if (agents.has(agentId)) return;
890
+ agents.set(agentId, { open: true });
891
+ emit({ type: "agent-start", agentId });
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
+ };
921
+ const endAgent = (agentId, status, detail = {}) => {
922
+ closeSteps(agentId);
923
+ const agent = agents.get(agentId);
924
+ if (agent) agent.open = false;
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);
927
+ };
928
+ const failOpenAgents = (error) => {
929
+ for (const [agentId, agent] of [...agents].reverse()) {
930
+ if (!agent.open) continue;
931
+ flush(agentId);
932
+ endAgent(agentId, "failed", { error });
933
+ }
934
+ };
716
935
  const startTool = (toolCallId) => {
717
936
  const call = calls.get(toolCallId);
718
937
  if (!call || started.has(toolCallId) || known.has(toolCallId)) return;
719
938
  const input = parseArgs(call.args);
720
- started.set(toolCallId, { name: call.name, input });
721
- if (textOpen) {
722
- emit({ type: "text-flush" });
723
- textOpen = false;
724
- }
725
- emit({ type: "tool-start", toolCallId, name: call.name, input });
726
- stepEnded = true;
939
+ started.set(toolCallId, { name: call.name, input, owner: call.owner });
940
+ flush(call.owner);
941
+ emit({ type: "tool-start", toolCallId, name: call.name, input, ...by(call.owner) });
942
+ lane(call.owner).stepEnded = true;
727
943
  };
728
- let stepEnded = false;
729
- const nextStep = () => {
730
- if (!stepEnded) return;
731
- stepEnded = false;
732
- if (textOpen) {
733
- emit({ type: "text-flush" });
734
- textOpen = false;
735
- }
736
- emit({ type: "turn-start" });
944
+ const nextStep = (owner) => {
945
+ const at = lane(owner);
946
+ if (!at.stepEnded) return;
947
+ at.stepEnded = false;
948
+ flush(owner);
949
+ emit({ type: "turn-start", ...by(owner) });
737
950
  };
738
951
  const callOf = (interrupt) => {
739
952
  const here = started.get(interrupt.toolCallId) ?? history.get(interrupt.toolCallId);
@@ -751,25 +964,28 @@ function createAgUiRunner(options) {
751
964
  emit({ type: "run-start" });
752
965
  emit({ type: "turn-start" });
753
966
  for (const [toolCallId, outcome] of Object.entries(outcomes)) {
967
+ const owner = ticketAgents.get(toolCallId) ?? PARENT;
968
+ if (owner) lane(owner).stepEnded = true;
754
969
  if (outcome.kind === "result") {
755
970
  emit({
756
971
  type: "tool-resolved",
757
972
  toolCallId,
758
973
  result: outcome.content,
759
- ...outcome.structuredContent !== void 0 ? { structuredResult: outcome.structuredContent } : {}
974
+ ...outcome.structuredContent !== void 0 ? { structuredResult: outcome.structuredContent } : {},
975
+ ...by(owner)
760
976
  });
761
977
  settledByPage.add(toolCallId);
762
978
  continue;
763
979
  }
764
980
  if (outcome.kind === "failed") {
765
- emit({ type: "tool-failed", toolCallId, error: outcome.error });
981
+ emit({ type: "tool-failed", toolCallId, error: outcome.error, ...by(owner) });
766
982
  settledByPage.add(toolCallId);
767
983
  continue;
768
984
  }
769
985
  if (outcome.approved) {
770
- emit({ type: "tool-approved", toolCallId });
986
+ emit({ type: "tool-approved", toolCallId, ...by(owner) });
771
987
  } else {
772
- emit({ type: "tool-rejected", toolCallId, ...toolRefusal(outcome) });
988
+ emit({ type: "tool-rejected", toolCallId, ...toolRefusal(outcome), ...by(owner) });
773
989
  rejected.add(toolCallId);
774
990
  }
775
991
  }
@@ -788,15 +1004,20 @@ function createAgUiRunner(options) {
788
1004
  verify(event);
789
1005
  begin();
790
1006
  if (/^(TEXT_MESSAGE|REASONING|TOOL_CALL)_/.test(event.type)) endedOnFinish = false;
1007
+ const tag = tagOf(event);
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;
791
1011
  switch (event.type) {
792
1012
  case "RUN_STARTED": {
793
1013
  if (runError) {
794
1014
  warn(`the agent's run failed ("${runError.message}") and the stream ran it again; the reply shows the new run.`);
795
- for (const [toolCallId] of started) {
1015
+ for (const [toolCallId, call] of started) {
796
1016
  if (answered.has(toolCallId)) continue;
797
- emit({ type: "tool-aborted", toolCallId });
1017
+ emit({ type: "tool-aborted", toolCallId, ...by(call.owner) });
798
1018
  answered.add(toolCallId);
799
1019
  }
1020
+ failOpenAgents(runError.message);
800
1021
  runError = void 0;
801
1022
  }
802
1023
  const declared = event["protocolVersion"];
@@ -809,52 +1030,75 @@ function createAgUiRunner(options) {
809
1030
  }
810
1031
  case "TEXT_MESSAGE_START":
811
1032
  case "REASONING_START":
812
- case "REASONING_MESSAGE_START":
813
- nextStep();
1033
+ case "REASONING_MESSAGE_START": {
1034
+ const owner = messageOwner(event);
1035
+ if (owner === PARENT) ownStep = true;
1036
+ nextStep(owner);
814
1037
  break;
1038
+ }
815
1039
  case "TEXT_MESSAGE_CONTENT":
816
1040
  case "TEXT_MESSAGE_CHUNK": {
1041
+ const owner = messageOwner(event);
817
1042
  const delta = typeof event["delta"] === "string" ? event["delta"] : "";
1043
+ if (owner === PARENT) ownStep = true;
818
1044
  if (delta) {
819
- nextStep();
820
- emit({ type: "text-delta", delta });
821
- textOpen = true;
1045
+ nextStep(owner);
1046
+ emit({ type: "text-delta", delta, ...by(owner) });
1047
+ lane(owner).textOpen = true;
822
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);
823
1051
  break;
824
1052
  }
825
- case "TEXT_MESSAGE_END": {
826
- if (textOpen) {
827
- emit({ type: "text-flush" });
828
- textOpen = false;
829
- }
1053
+ case "TEXT_MESSAGE_END":
1054
+ flush(messageOwner(event));
830
1055
  break;
831
- }
832
1056
  case "REASONING_MESSAGE_CONTENT":
833
1057
  case "REASONING_MESSAGE_CHUNK": {
1058
+ const owner = messageOwner(event);
834
1059
  const delta = typeof event["delta"] === "string" ? event["delta"] : "";
835
1060
  if (delta) {
836
- nextStep();
837
- emit({ type: "thinking-delta", delta });
1061
+ nextStep(owner);
1062
+ emit({ type: "thinking-delta", delta, ...by(owner) });
838
1063
  }
839
1064
  break;
840
1065
  }
841
1066
  case "TOOL_CALL_START": {
842
1067
  const id = String(event["toolCallId"]);
843
- calls.set(id, { name: String(event["toolCallName"] ?? ""), args: "" });
1068
+ if (!calls.has(id)) lastCall.set(tag ?? PARENT, id);
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);
844
1074
  break;
845
1075
  }
846
1076
  case "TOOL_CALL_ARGS": {
847
- const call = calls.get(String(event["toolCallId"]));
848
- 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
+ }
849
1083
  break;
850
1084
  }
851
1085
  case "TOOL_CALL_CHUNK": {
852
- const id = typeof event["toolCallId"] === "string" ? event["toolCallId"] : [...calls.keys()].at(-1);
1086
+ const id = typeof event["toolCallId"] === "string" ? event["toolCallId"] : lastCall.get(tag ?? PARENT);
853
1087
  if (!id) break;
854
- const call = calls.get(id) ?? { name: "", args: "" };
1088
+ let call = calls.get(id);
1089
+ if (!call) {
1090
+ call = { name: "", args: "", owner: newCallOwner(event) };
1091
+ calls.set(id, call);
1092
+ lastCall.set(tag ?? PARENT, id);
1093
+ }
855
1094
  if (typeof event["toolCallName"] === "string" && event["toolCallName"]) call.name = event["toolCallName"];
856
1095
  if (typeof event["delta"] === "string") call.args += event["delta"];
857
- calls.set(id, call);
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
+ }
858
1102
  break;
859
1103
  }
860
1104
  case "TOOL_CALL_END": {
@@ -866,13 +1110,66 @@ function createAgUiRunner(options) {
866
1110
  startTool(toolCallId);
867
1111
  if (rejected.has(toolCallId) || settledByPage.has(toolCallId)) break;
868
1112
  answered.add(toolCallId);
1113
+ const owner = callOwner(toolCallId) ?? tag ?? PARENT;
869
1114
  const content = resultText(event["content"]);
1115
+ if (owner !== PARENT) transcripts.result(owner, typeof event["messageId"] === "string" ? event["messageId"] : void 0, toolCallId, content);
870
1116
  const meta = event["metadata"];
871
1117
  if (meta?.tanstack?.state === "output-error") {
872
- emit({ type: "tool-failed", toolCallId, error: content });
1118
+ emit({ type: "tool-failed", toolCallId, error: content, ...by(owner) });
873
1119
  } else {
874
1120
  const parts = Array.isArray(event["content"]) ? { structuredResult: event["content"] } : {};
875
- emit({ type: "tool-resolved", toolCallId, result: content, ...parts });
1121
+ emit({ type: "tool-resolved", toolCallId, result: content, ...parts, ...by(owner) });
1122
+ }
1123
+ break;
1124
+ }
1125
+ case "SUBAGENT_STARTED": {
1126
+ const agentId = String(event["subagentRunId"]);
1127
+ const toolCallId = typeof event["parentToolCallId"] === "string" ? event["parentToolCallId"] : void 0;
1128
+ const parent = typeof event["parentSubagentRunId"] === "string" ? event["parentSubagentRunId"] : void 0;
1129
+ if (toolCallId) startTool(toolCallId);
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
+ }
1147
+ emit({
1148
+ type: "agent-start",
1149
+ agentId,
1150
+ name,
1151
+ ...typeof event["description"] === "string" ? { description: event["description"] } : {},
1152
+ ...toolCallId ? { toolCallId } : {},
1153
+ ...parent ? { parentAgentId: parent } : {}
1154
+ });
1155
+ break;
1156
+ }
1157
+ case "SUBAGENT_FINISHED":
1158
+ case "SUBAGENT_ERROR": {
1159
+ const agentId = String(event["subagentRunId"]);
1160
+ flush(agentId);
1161
+ for (const [id, call] of calls) if (call.owner === agentId) startTool(id);
1162
+ const outcome = event["outcome"];
1163
+ if (event.type === "SUBAGENT_ERROR") {
1164
+ transcripts.end(agentId, "error");
1165
+ endAgent(agentId, "failed", { error: typeof event["message"] === "string" && event["message"] ? event["message"] : "The sub-agent failed." });
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);
1169
+ endAgent(agentId, "waiting");
1170
+ } else {
1171
+ transcripts.end(agentId, "finished");
1172
+ endAgent(agentId, "done", event["result"] !== void 0 ? { result: resultText(event["result"]) } : {});
876
1173
  }
877
1174
  break;
878
1175
  }
@@ -884,6 +1181,9 @@ function createAgUiRunner(options) {
884
1181
  );
885
1182
  break;
886
1183
  }
1184
+ // State is run-scoped in AG-UI 1.0: a sub-agent's STATE_* updates the run's state
1185
+ // exactly as the parent's does — its `subagentRunId` is provenance, and the
1186
+ // protocol forbids withholding the update because a sub-agent sent it.
887
1187
  case "STATE_SNAPSHOT":
888
1188
  states.set(threadId, event["snapshot"]);
889
1189
  break;
@@ -892,12 +1192,12 @@ function createAgUiRunner(options) {
892
1192
  break;
893
1193
  case "RUN_FINISHED": {
894
1194
  for (const id of calls.keys()) startTool(id);
895
- if (textOpen) {
896
- emit({ type: "text-flush" });
897
- textOpen = false;
898
- }
899
- usage = readUsage(event["usage"]) ?? usage;
900
- stepEnded = true;
1195
+ for (const owner of lanes.keys()) flush(owner);
1196
+ if (agents.size === 0) usage = readUsage(event["usage"]) ?? usage;
1197
+ else if (ownStep) usage = readUsage(event["usage"], "first") ?? usage;
1198
+ ownStep = false;
1199
+ lane(PARENT).stepEnded = true;
1200
+ for (const at of lanes.values()) at.stepEnded = true;
901
1201
  endedOnFinish = true;
902
1202
  finished = {
903
1203
  runId: typeof event["runId"] === "string" ? event["runId"] : runId,
@@ -905,12 +1205,57 @@ function createAgUiRunner(options) {
905
1205
  };
906
1206
  break;
907
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
+ }
908
1251
  }
909
1252
  }
1253
+ closeSteps();
910
1254
  if (opts.signal.aborted) return usage;
911
1255
  if (runError) {
912
- for (const [toolCallId] of started) if (!answered.has(toolCallId)) emit({ type: "tool-aborted", toolCallId });
913
- if (textOpen) emit({ type: "text-flush" });
1256
+ for (const [toolCallId, call] of started) if (!answered.has(toolCallId)) emit({ type: "tool-aborted", toolCallId, ...by(call.owner) });
1257
+ for (const owner of lanes.keys()) flush(owner);
1258
+ failOpenAgents(runError.message);
914
1259
  throw runError;
915
1260
  }
916
1261
  if (!finished) {
@@ -920,7 +1265,7 @@ function createAgUiRunner(options) {
920
1265
  throw new AparteError("The agent stream was cut: it ended in the middle of a step, after a RUN_FINISHED.", AparteErrorCode.PROVIDER_ERROR);
921
1266
  }
922
1267
  verify.end();
923
- if (textOpen) emit({ type: "text-flush" });
1268
+ for (const owner of lanes.keys()) flush(owner);
924
1269
  const pending = [];
925
1270
  const interrupts = [];
926
1271
  const unanswerable = [];
@@ -932,11 +1277,12 @@ function createAgUiRunner(options) {
932
1277
  continue;
933
1278
  }
934
1279
  const call = callOf(interrupt);
1280
+ const owner = interrupt.subagentRunId ?? callOwner(interrupt.toolCallId) ?? PARENT;
935
1281
  if (kind === "approval") {
936
- emit({ type: "tool-awaiting-approval", toolCallId: interrupt.toolCallId, name: call.name, input: call.input });
1282
+ emit({ type: "tool-awaiting-approval", toolCallId: interrupt.toolCallId, name: call.name, input: call.input, ...by(owner) });
937
1283
  }
938
1284
  interrupts.push({ id: interrupt.id, toolCallId: interrupt.toolCallId, kind });
939
- pending.push({ toolCallId: interrupt.toolCallId, name: call.name, input: call.input, reason: kind });
1285
+ pending.push({ toolCallId: interrupt.toolCallId, name: call.name, input: call.input, reason: kind, ...by(owner) });
940
1286
  }
941
1287
  } else {
942
1288
  const outcome = finished.outcome;
@@ -944,13 +1290,25 @@ function createAgUiRunner(options) {
944
1290
  for (const [toolCallId, call] of started) {
945
1291
  if (answered.has(toolCallId)) continue;
946
1292
  const forPage = outcome?.type !== "cancelled" && (named ? named.has(toolCallId) : browserTools.has(call.name));
947
- if (forPage) pending.push({ toolCallId, name: call.name, input: call.input, reason: "external" });
948
- else emit({ type: "tool-aborted", toolCallId });
1293
+ if (forPage) pending.push({ toolCallId, name: call.name, input: call.input, reason: "external", ...by(call.owner) });
1294
+ else emit({ type: "tool-aborted", toolCallId, ...by(call.owner) });
949
1295
  }
950
1296
  }
1297
+ for (const [agentId, agent] of [...agents].reverse()) {
1298
+ if (agent.open) endAgent(agentId, pending.some((p) => p.agentId === agentId) ? "waiting" : "done");
1299
+ }
951
1300
  if (pending.length > 0) {
952
1301
  const cancelled = unanswerable.map((id) => ({ id, toolCallId: "", kind: "unanswerable" }));
953
- const next = { v: 1, threadId, runId: finished.runId, interrupts: [...interrupts, ...cancelled] };
1302
+ const owned = pending.filter((p) => p.agentId).map((p) => [p.toolCallId, p.agentId]);
1303
+ const next = {
1304
+ v: 1,
1305
+ threadId,
1306
+ runId: finished.runId,
1307
+ interrupts: [...interrupts, ...cancelled],
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) } } : {}
1311
+ };
954
1312
  track.suspended = true;
955
1313
  emit({ type: "run-suspended", pending, ...usage ? { usage } : {}, ticket: JSON.stringify(next) });
956
1314
  } else {
@@ -965,6 +1323,7 @@ function createAgUiRunner(options) {
965
1323
  return await runOnce(opts, track);
966
1324
  } finally {
967
1325
  if (track.threadId && !stableThread && !track.suspended) states.delete(track.threadId);
1326
+ track.closeSteps?.();
968
1327
  }
969
1328
  };
970
1329
  }