@aparte/core 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/client/agent-runner.d.ts +27 -0
  2. package/dist/client/agent-runner.d.ts.map +1 -0
  3. package/dist/client/aparte-client.d.ts +77 -2
  4. package/dist/client/aparte-client.d.ts.map +1 -1
  5. package/dist/client/stream-adapter.d.ts.map +1 -1
  6. package/dist/client/wire-history.d.ts +33 -0
  7. package/dist/client/wire-history.d.ts.map +1 -0
  8. package/dist/conversations/conversation-controller.d.ts +1 -0
  9. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  10. package/dist/conversations/index.d.ts +1 -1
  11. package/dist/conversations/index.d.ts.map +1 -1
  12. package/dist/conversations/types.d.ts +14 -0
  13. package/dist/conversations/types.d.ts.map +1 -1
  14. package/dist/custom-elements.json +10757 -9967
  15. package/dist/{dialog-triggers-DwTGKWeU.js → dialog-triggers-Dad9V_ag.js} +1051 -385
  16. package/dist/dialog-triggers-Dad9V_ag.js.map +1 -0
  17. package/dist/index.d.ts +5 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +47 -45
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.node.d.ts +5 -4
  22. package/dist/index.node.d.ts.map +1 -1
  23. package/dist/index.node.js +35 -33
  24. package/dist/index.node.js.map +1 -1
  25. package/dist/interop/element-props.d.ts +1 -1
  26. package/dist/interop/element-props.d.ts.map +1 -1
  27. package/dist/parsers/aparte-event-stream.d.ts.map +1 -1
  28. package/dist/transport/agent-handler.d.ts +104 -0
  29. package/dist/transport/agent-handler.d.ts.map +1 -0
  30. package/dist/transport/agent-ticket.d.ts +48 -0
  31. package/dist/transport/agent-ticket.d.ts.map +1 -0
  32. package/dist/transport/index.d.ts +1 -0
  33. package/dist/transport/index.d.ts.map +1 -1
  34. package/dist/types/event-map.d.ts +4 -1
  35. package/dist/types/event-map.d.ts.map +1 -1
  36. package/dist/types/events.d.ts +16 -0
  37. package/dist/types/events.d.ts.map +1 -1
  38. package/dist/types/index.d.ts +1 -1
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/segments.d.ts +11 -0
  41. package/dist/types/segments.d.ts.map +1 -1
  42. package/dist/types/tools.d.ts +6 -0
  43. package/dist/types/tools.d.ts.map +1 -1
  44. package/dist/utils/ndjson.d.ts +13 -0
  45. package/dist/utils/ndjson.d.ts.map +1 -0
  46. package/dist/utils/segments.d.ts +11 -3
  47. package/dist/utils/segments.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/dist/dialog-triggers-DwTGKWeU.js.map +0 -1
@@ -1,5 +1,5 @@
1
+ import { unansweredTail, runStreamAgent } from "@aparte/engine";
1
2
  import { A as APARTE_ICON_GLYPHS } from "./glyphs-C8z2oJUg.js";
2
- import { runStreamAgent } from "@aparte/engine";
3
3
  function contentToText(content) {
4
4
  if (typeof content === "string") return content;
5
5
  return content.filter((p) => p.type === "text").map((p) => p.text).join("");
@@ -725,40 +725,53 @@ function parseMarkdownToSegments(content, options) {
725
725
  const finalSegments = parser.finalize();
726
726
  return [...result.segments, ...finalSegments];
727
727
  }
728
- function parseAparteEventStream(stream) {
728
+ async function* readNdjson(stream, signal) {
729
+ const reader = stream.getReader();
729
730
  const decoder = new TextDecoder();
730
731
  let buffer = "";
731
- let sawDone = false;
732
+ const parse = (line) => {
733
+ const trimmed = line.trim();
734
+ if (!trimmed) return void 0;
735
+ try {
736
+ const value = JSON.parse(trimmed);
737
+ return value && typeof value.type === "string" ? value : void 0;
738
+ } catch {
739
+ return void 0;
740
+ }
741
+ };
742
+ try {
743
+ while (!signal?.aborted) {
744
+ const { done, value } = await reader.read();
745
+ if (done) break;
746
+ buffer += decoder.decode(value, { stream: true });
747
+ const lines = buffer.split("\n");
748
+ buffer = lines.pop() ?? "";
749
+ for (const line of lines) {
750
+ if (signal?.aborted) return;
751
+ const event = parse(line);
752
+ if (event) yield event;
753
+ }
754
+ }
755
+ if (signal?.aborted) return;
756
+ const last = parse(buffer + decoder.decode());
757
+ if (last) yield last;
758
+ } finally {
759
+ reader.releaseLock();
760
+ }
761
+ }
762
+ function parseAparteEventStream(stream) {
732
763
  return new ReadableStream({
733
764
  async start(controller) {
734
- const reader = stream.getReader();
735
- const emit = (line) => {
736
- const trimmed = line.trim();
737
- if (!trimmed) return;
738
- try {
739
- const event = JSON.parse(trimmed);
740
- if (event && typeof event.type === "string") {
741
- controller.enqueue(event);
742
- if (event.type === "done") sawDone = true;
743
- }
744
- } catch {
745
- }
746
- };
765
+ let sawDone = false;
747
766
  try {
748
- while (true) {
749
- const { done, value } = await reader.read();
750
- if (done) break;
751
- buffer += decoder.decode(value, { stream: true });
752
- const lines = buffer.split("\n");
753
- buffer = lines.pop() ?? "";
754
- for (const line of lines) emit(line);
767
+ for await (const event of readNdjson(stream)) {
768
+ controller.enqueue(event);
769
+ if (event.type === "done") sawDone = true;
755
770
  }
756
- if (buffer.trim()) emit(buffer);
757
771
  if (!sawDone) controller.enqueue({ type: "done" });
758
772
  } catch (err) {
759
773
  controller.enqueue({ type: "error", message: err?.message ?? "Stream error" });
760
774
  } finally {
761
- reader.releaseLock();
762
775
  controller.close();
763
776
  }
764
777
  }
@@ -795,29 +808,29 @@ function createAparteChatHandler(options) {
795
808
  return async function handler(req) {
796
809
  const verdict = await options.authorize(req);
797
810
  if (verdict instanceof Response) return verdict;
798
- if (!verdict) return jsonError(401, "Unauthorized.");
811
+ if (!verdict) return jsonError$1(401, "Unauthorized.");
799
812
  let providerId;
800
813
  let request;
801
814
  try {
802
815
  const parsed = await req.json();
803
816
  if (typeof parsed?.providerId !== "string" || !parsed?.request || typeof parsed.request !== "object") {
804
- return jsonError(400, "Body must be { providerId: string, request: AparteChatRequest }.");
817
+ return jsonError$1(400, "Body must be { providerId: string, request: AparteChatRequest }.");
805
818
  }
806
819
  providerId = parsed.providerId;
807
820
  request = parsed.request;
808
821
  } catch {
809
- return jsonError(400, "Invalid JSON body.");
822
+ return jsonError$1(400, "Invalid JSON body.");
810
823
  }
811
824
  const adapter = Object.hasOwn(options.providers, providerId) ? options.providers[providerId] : void 0;
812
825
  if (!adapter) {
813
- return jsonError(400, `Unknown providerId "${providerId}". Register it in the handler's providers map.`);
826
+ return jsonError$1(400, `Unknown providerId "${providerId}". Register it in the handler's providers map.`);
814
827
  }
815
828
  if (!isFormatAdapter(adapter)) {
816
- return jsonError(500, `Provider "${providerId}" is not a format adapter (needs buildRequest + parseStream).`);
829
+ return jsonError$1(500, `Provider "${providerId}" is not a format adapter (needs buildRequest + parseStream).`);
817
830
  }
818
831
  const built = adapter.buildRequest(request);
819
832
  if (!built.path.startsWith("/") || built.path.startsWith("//")) {
820
- return jsonError(500, `Provider "${providerId}" produced a non-rooted request path.`);
833
+ return jsonError$1(500, `Provider "${providerId}" produced a non-rooted request path.`);
821
834
  }
822
835
  const key = await options.resolveKey?.(providerId);
823
836
  let url = `${adapter.defaultEndpoint}${built.path}`;
@@ -838,7 +851,7 @@ function createAparteChatHandler(options) {
838
851
  });
839
852
  } catch (err) {
840
853
  console.error("[aparte] vendor request failed", err);
841
- return jsonError(502, "Vendor request failed.");
854
+ return jsonError$1(502, "Vendor request failed.");
842
855
  }
843
856
  if (!vendor.ok) {
844
857
  const raw = await vendor.text().catch(() => "");
@@ -869,10 +882,10 @@ function createAparteChatHandler(options) {
869
882
  });
870
883
  }
871
884
  if (!vendor.body) {
872
- return jsonError(502, "Vendor returned no response body for a streaming request.");
885
+ return jsonError$1(502, "Vendor returned no response body for a streaming request.");
873
886
  }
874
887
  const events = adapter.parseStream(vendor.body).getReader();
875
- const encoder = new TextEncoder();
888
+ const encoder2 = new TextEncoder();
876
889
  const ndjson = new ReadableStream({
877
890
  async pull(controller) {
878
891
  try {
@@ -881,7 +894,7 @@ function createAparteChatHandler(options) {
881
894
  controller.close();
882
895
  return;
883
896
  }
884
- controller.enqueue(encoder.encode(JSON.stringify(value) + "\n"));
897
+ controller.enqueue(encoder2.encode(JSON.stringify(value) + "\n"));
885
898
  } catch (err) {
886
899
  controller.error(err);
887
900
  }
@@ -893,6 +906,584 @@ function createAparteChatHandler(options) {
893
906
  return new Response(ndjson, { headers: { "Content-Type": "application/x-ndjson" } });
894
907
  };
895
908
  }
909
+ function jsonError$1(status, message) {
910
+ return new Response(JSON.stringify({ error: { message } }), {
911
+ status,
912
+ headers: { "Content-Type": "application/json" }
913
+ });
914
+ }
915
+ const OPEN_TOOL_STATUSES = /* @__PURE__ */ new Set(["pending", "awaiting-approval"]);
916
+ const ACTIVITY_KEYS = ["content", "output", "result"];
917
+ function isActivity(updates) {
918
+ return ACTIVITY_KEYS.some((key) => key in updates);
919
+ }
920
+ function stampSegmentOnInsert(segments, segment, messageId, defaults) {
921
+ const base = applyDefaults(segment, defaults);
922
+ return {
923
+ ...base,
924
+ messageId: segment.messageId ?? messageId,
925
+ index: segment.index ?? segments.length,
926
+ // A start already present is kept — a segment a consumer rebuilt from their own
927
+ // storage keeps the number it was persisted with. `adoptSegment` is the path
928
+ // that never writes one at all.
929
+ ...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
930
+ };
931
+ }
932
+ function adoptSegment(segments, segment, messageId, live = false) {
933
+ return {
934
+ ...segment,
935
+ messageId,
936
+ index: segments.length,
937
+ isStreaming: false,
938
+ ...segment.type === "tool_call" && (segment.status === "awaiting-approval" || segment.status === "pending" && !live) && !segment.deferred?.ticket ? { status: "aborted" } : {}
939
+ };
940
+ }
941
+ const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
942
+ function segmentTiming(segment) {
943
+ return segment.meta?.aparte;
944
+ }
945
+ function timingPatch(segment, timing) {
946
+ return { meta: { ...segment.meta, aparte: { ...segment.meta?.aparte, ...timing } } };
947
+ }
948
+ function applyDefaults(segment, defaults) {
949
+ if (!defaults) return segment;
950
+ const out = { ...segment };
951
+ for (const [key, value] of Object.entries(defaults)) {
952
+ if (RESERVED.has(key) || key in segment) continue;
953
+ if (key === "meta") {
954
+ const { aparte: _forged, ...rest } = value ?? {};
955
+ out.meta = rest;
956
+ continue;
957
+ }
958
+ out[key] = value;
959
+ }
960
+ return out;
961
+ }
962
+ function isSegmentSettled(segment) {
963
+ if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
964
+ return segment.isStreaming === false;
965
+ }
966
+ function stampSegmentOnUpdate(segment, updates) {
967
+ if (isSegmentSettled(segment)) return updates;
968
+ const merged = { ...segment, ...updates };
969
+ if (isSegmentSettled(merged)) {
970
+ const ended = segmentTiming(segment)?.endedAt;
971
+ return { ...updates, ...timingPatch(segment, { endedAt: ended ?? Date.now() }) };
972
+ }
973
+ return isActivity(updates) ? { ...updates, ...timingPatch(segment, { endedAt: Date.now() }) } : updates;
974
+ }
975
+ function mergeSegmentUpdate(segment, updates) {
976
+ const merged = { ...segment, ...updates };
977
+ if (!("meta" in updates)) return merged;
978
+ return {
979
+ ...merged,
980
+ meta: {
981
+ ...segment.meta,
982
+ ...updates.meta,
983
+ // Neither side's timing wins by accident: whichever one carries it, wins,
984
+ // and a consumer writing `meta` without an `aparte` key keeps ours.
985
+ ...segment.meta?.aparte || updates.meta?.aparte ? { aparte: { ...segment.meta?.aparte, ...updates.meta?.aparte } } : {}
986
+ }
987
+ };
988
+ }
989
+ function segmentDuration(segment) {
990
+ const { startedAt, endedAt } = segmentTiming(segment) ?? {};
991
+ if (startedAt == null || endedAt == null) return void 0;
992
+ return endedAt - startedAt;
993
+ }
994
+ function stampSegmentActivity(segment) {
995
+ return isSegmentSettled(segment) ? {} : timingPatch(segment, { endedAt: Date.now() });
996
+ }
997
+ function segmentContentUpdate(segment) {
998
+ const content = segment.content;
999
+ return {
1000
+ content,
1001
+ ...segment.isStreaming === false ? { isStreaming: false } : {}
1002
+ };
1003
+ }
1004
+ function isTerminalStatus(status) {
1005
+ return status != null && status !== "streaming" && status !== "pending";
1006
+ }
1007
+ function openSegmentIds(segments) {
1008
+ return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
1009
+ }
1010
+ function adoptMessageSegments(message) {
1011
+ if (!message.segments?.length) return { ...message };
1012
+ const live = message.status === "streaming" || message.status === "pending";
1013
+ return {
1014
+ ...message,
1015
+ segments: message.segments.reduce((acc, segment) => {
1016
+ acc.push(adoptSegment(acc, segment, message.id, live));
1017
+ return acc;
1018
+ }, [])
1019
+ };
1020
+ }
1021
+ function renumberSegments(segments) {
1022
+ for (let i = 0; i < segments.length; i++) {
1023
+ const segment = segments[i];
1024
+ if (segment.index === i) continue;
1025
+ segments[i] = { ...segment, index: i };
1026
+ }
1027
+ }
1028
+ function dispatchLifecycleEvent(target, name, detail, targetId) {
1029
+ const id = targetId ?? (target.id || void 0);
1030
+ const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
1031
+ target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
1032
+ }
1033
+ function createStreamAdapter(opts) {
1034
+ const { target, config, messageId, targetId } = opts;
1035
+ let parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
1036
+ let streaming = /* @__PURE__ */ new Set();
1037
+ let thinkingId = null;
1038
+ const toolSegments = /* @__PURE__ */ new Map();
1039
+ const addedTools = /* @__PURE__ */ new Set();
1040
+ const settle = (toolCallId, update) => {
1041
+ const id = `tool-${toolCallId}`;
1042
+ target.updateSegment?.(id, toolSegments.has(id) ? update : { ...update, deferred: void 0 });
1043
+ };
1044
+ let thinkingContent = "";
1045
+ let thinkingCollapsed = false;
1046
+ return (e) => {
1047
+ switch (e.type) {
1048
+ case "run-start":
1049
+ target.updateMessage?.(messageId, { status: "streaming" });
1050
+ break;
1051
+ case "turn-start":
1052
+ parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
1053
+ streaming = /* @__PURE__ */ new Set();
1054
+ thinkingId = null;
1055
+ thinkingContent = "";
1056
+ thinkingCollapsed = false;
1057
+ break;
1058
+ case "thinking-delta": {
1059
+ thinkingContent += e.delta;
1060
+ if (!thinkingId) {
1061
+ const seg = {
1062
+ id: `think-${uuid()}`,
1063
+ type: "thinking",
1064
+ content: thinkingContent,
1065
+ collapsed: true,
1066
+ label: "Thinking"
1067
+ };
1068
+ thinkingId = seg.id;
1069
+ streaming.add(seg.id);
1070
+ target.addSegment?.(seg);
1071
+ } else {
1072
+ target.updateSegment?.(thinkingId, { content: thinkingContent });
1073
+ }
1074
+ break;
1075
+ }
1076
+ case "text-delta": {
1077
+ if (thinkingId && !thinkingCollapsed) {
1078
+ target.updateSegment?.(thinkingId, { collapsed: true, isStreaming: false });
1079
+ thinkingCollapsed = true;
1080
+ }
1081
+ const result = parser.parse(e.delta);
1082
+ for (const segment of result.segments) {
1083
+ if (!streaming.has(segment.id)) {
1084
+ target.addSegment?.(segment);
1085
+ streaming.add(segment.id);
1086
+ } else if ("content" in segment) {
1087
+ target.updateSegment?.(segment.id, segmentContentUpdate(segment));
1088
+ }
1089
+ }
1090
+ const active = parser.getState().activeSegment;
1091
+ if (active) {
1092
+ if (!streaming.has(active.id)) {
1093
+ target.addSegment?.(active);
1094
+ streaming.add(active.id);
1095
+ } else {
1096
+ target.updateSegment?.(active.id, segmentContentUpdate(active));
1097
+ }
1098
+ } else if (result.segments.length === 0) ;
1099
+ break;
1100
+ }
1101
+ case "text-flush": {
1102
+ const finals = parser.finalize();
1103
+ for (const s of finals) {
1104
+ if (!streaming.has(s.id)) target.addSegment?.(s);
1105
+ else if ("content" in s) target.updateSegment?.(s.id, segmentContentUpdate(s));
1106
+ }
1107
+ break;
1108
+ }
1109
+ case "tool-start": {
1110
+ const toolSeg = {
1111
+ id: `tool-${e.toolCallId}`,
1112
+ type: "tool_call",
1113
+ toolCall: { id: e.toolCallId, name: e.name, input: e.input },
1114
+ status: "pending"
1115
+ };
1116
+ toolSegments.set(toolSeg.id, toolSeg);
1117
+ const renderer = config.getToolRenderer(e.name);
1118
+ if (renderer) {
1119
+ injectToolRendererStyles(e.name, renderer);
1120
+ const html = renderer.render(toolSeg);
1121
+ if (html) {
1122
+ target.addSegment?.(toolSeg);
1123
+ addedTools.add(toolSeg.id);
1124
+ }
1125
+ } else {
1126
+ target.addSegment?.(toolSeg);
1127
+ addedTools.add(toolSeg.id);
1128
+ }
1129
+ break;
1130
+ }
1131
+ case "tool-awaiting-approval":
1132
+ target.updateSegment?.(`tool-${e.toolCallId}`, { status: "awaiting-approval" });
1133
+ dispatchLifecycleEvent(target, "aparte-tool-approval-request", { toolCallId: e.toolCallId, toolName: e.name, input: e.input }, targetId);
1134
+ break;
1135
+ case "tool-approved":
1136
+ settle(e.toolCallId, { status: "pending" });
1137
+ break;
1138
+ case "tool-rejected":
1139
+ settle(e.toolCallId, { status: "rejected", result: e.reason });
1140
+ break;
1141
+ case "tool-resolved":
1142
+ settle(e.toolCallId, {
1143
+ status: "resolved",
1144
+ result: e.result,
1145
+ ...e.structuredResult !== void 0 ? { structuredResult: e.structuredResult } : {}
1146
+ });
1147
+ break;
1148
+ case "tool-aborted":
1149
+ settle(e.toolCallId, { status: "aborted" });
1150
+ break;
1151
+ case "tool-failed":
1152
+ settle(e.toolCallId, { status: "failed", result: e.error });
1153
+ break;
1154
+ case "turn-limit-exceeded":
1155
+ if (e.scope === "global") {
1156
+ target.addSegment?.({
1157
+ id: `max-turns-${uuid()}`,
1158
+ type: "error",
1159
+ content: `Stopped after ${e.limit} tool calls to prevent an infinite loop.`,
1160
+ details: "MAX_TURNS_EXCEEDED"
1161
+ });
1162
+ } else if (e.toolCallId) {
1163
+ settle(e.toolCallId, { status: "aborted" });
1164
+ }
1165
+ break;
1166
+ case "run-aborted":
1167
+ dispatchLifecycleEvent(target, "aparte-message-aborted", { messageId }, targetId);
1168
+ break;
1169
+ case "run-suspended": {
1170
+ const ticket = e.ticket;
1171
+ for (const p of e.pending) {
1172
+ const id = `tool-${p.toolCallId}`;
1173
+ const deferred = { reason: p.reason, ...typeof ticket === "string" ? { ticket } : {} };
1174
+ if (addedTools.has(id) || !toolSegments.has(id)) {
1175
+ target.updateSegment?.(id, { deferred });
1176
+ } else {
1177
+ const seg = toolSegments.get(id);
1178
+ if (seg) {
1179
+ target.addSegment?.({ ...seg, deferred });
1180
+ addedTools.add(id);
1181
+ }
1182
+ }
1183
+ }
1184
+ target.updateMessage?.(messageId, { status: "completed" });
1185
+ if (e.usage) {
1186
+ try {
1187
+ target.setUsage?.(messageId, e.usage);
1188
+ } catch {
1189
+ }
1190
+ }
1191
+ dispatchLifecycleEvent(target, "aparte-message-paused", { messageId }, targetId);
1192
+ break;
1193
+ }
1194
+ case "run-done":
1195
+ target.updateMessage?.(messageId, { status: "completed" });
1196
+ if (e.usage) {
1197
+ try {
1198
+ target.setUsage?.(messageId, e.usage);
1199
+ } catch {
1200
+ }
1201
+ }
1202
+ break;
1203
+ }
1204
+ };
1205
+ }
1206
+ function readableToAsyncIterable(stream, signal) {
1207
+ return {
1208
+ [Symbol.asyncIterator]() {
1209
+ const reader = stream.getReader();
1210
+ const onAbort = () => {
1211
+ try {
1212
+ reader.cancel().catch(() => {
1213
+ });
1214
+ } catch {
1215
+ }
1216
+ };
1217
+ if (signal.aborted) onAbort();
1218
+ else signal.addEventListener("abort", onAbort, { once: true });
1219
+ return {
1220
+ async next() {
1221
+ const { done, value } = await reader.read();
1222
+ if (done) return { done: true, value: void 0 };
1223
+ return { done: false, value };
1224
+ },
1225
+ async return() {
1226
+ signal.removeEventListener("abort", onAbort);
1227
+ try {
1228
+ await reader.cancel();
1229
+ } catch {
1230
+ }
1231
+ try {
1232
+ reader.releaseLock();
1233
+ } catch {
1234
+ }
1235
+ return { done: true, value: void 0 };
1236
+ }
1237
+ };
1238
+ }
1239
+ };
1240
+ }
1241
+ const encoder = new TextEncoder();
1242
+ function canonicalJson(value) {
1243
+ if (Array.isArray(value)) return `[${value.map((v) => canonicalJson(v === void 0 ? null : v)).join(",")}]`;
1244
+ if (value && typeof value === "object") {
1245
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1246
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1247
+ }
1248
+ return JSON.stringify(value ?? null);
1249
+ }
1250
+ function toBase64Url(bytes) {
1251
+ let binary = "";
1252
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1253
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1254
+ }
1255
+ function fromBase64Url(text) {
1256
+ if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error("not base64url");
1257
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
1258
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
1259
+ }
1260
+ function hmacKey(secret, subtle) {
1261
+ return subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
1262
+ }
1263
+ async function signTicket(payload, secret, subtle) {
1264
+ const body = encoder.encode(canonicalJson(payload));
1265
+ const signature = new Uint8Array(await subtle.sign("HMAC", await hmacKey(secret, subtle), body));
1266
+ return `${toBase64Url(body)}.${toBase64Url(signature)}`;
1267
+ }
1268
+ async function verifyTicket(ticket, secrets, subtle, maxAgeMs, now = Date.now()) {
1269
+ const parts = ticket.split(".");
1270
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
1271
+ let body;
1272
+ let signature;
1273
+ try {
1274
+ body = fromBase64Url(parts[0]);
1275
+ signature = fromBase64Url(parts[1]);
1276
+ } catch {
1277
+ return null;
1278
+ }
1279
+ let valid = false;
1280
+ for (const secret of secrets) {
1281
+ if (await subtle.verify("HMAC", await hmacKey(secret, subtle), signature, body)) {
1282
+ valid = true;
1283
+ break;
1284
+ }
1285
+ }
1286
+ if (!valid) return null;
1287
+ let payload;
1288
+ try {
1289
+ payload = JSON.parse(new TextDecoder().decode(body));
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ if (payload?.v !== 1 || typeof payload.iat !== "number" || typeof payload.messageId !== "string" || !Array.isArray(payload.calls)) {
1294
+ return null;
1295
+ }
1296
+ if (now - payload.iat > maxAgeMs) return null;
1297
+ return payload;
1298
+ }
1299
+ function sameCalls(a, b) {
1300
+ return a.length === b.length && a.every((call, i) => {
1301
+ const other = b[i];
1302
+ return call.id === other.id && call.name === other.name && canonicalJson(call.input) === canonicalJson(other.input);
1303
+ });
1304
+ }
1305
+ const DEFAULT_TICKET_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
1306
+ function createAparteAgentHandler(options) {
1307
+ const secrets = typeof options.secret === "string" ? [options.secret] : [...options.secret];
1308
+ if (secrets.length === 0 || secrets.some((s) => typeof s !== "string" || s.length < 32)) {
1309
+ throw new Error("createAparteAgentHandler: `secret` must be at least 32 characters — it signs the pending tool calls of a suspended run.");
1310
+ }
1311
+ const tools = new Map((options.tools ?? []).map((t) => [t.tool.name, t]));
1312
+ const transport = options.transport ?? new AparteDirectTransport({ byok: true });
1313
+ const maxAge = options.ticketMaxAgeMs ?? DEFAULT_TICKET_MAX_AGE_MS;
1314
+ return async function handler(req) {
1315
+ const verdict = await options.authorize(req);
1316
+ if (verdict instanceof Response) return verdict;
1317
+ if (!verdict) return jsonError(401, "Unauthorized.");
1318
+ const subtle = options.crypto ?? globalThis.crypto?.subtle;
1319
+ if (!subtle) {
1320
+ return jsonError(500, "No Web Crypto here: pass `crypto` (on Node 18, the `webcrypto.subtle` of the built-in crypto module).");
1321
+ }
1322
+ let body;
1323
+ try {
1324
+ body = await req.json();
1325
+ } catch {
1326
+ return jsonError(400, "Invalid JSON body.");
1327
+ }
1328
+ if (body?.v !== 1) return jsonError(400, "Unsupported agent wire version: expected v 1.");
1329
+ if (typeof body.providerId !== "string" || typeof body.messageId !== "string" || !Array.isArray(body.request?.messages)) {
1330
+ return jsonError(400, "Body must be { v: 1, providerId, messageId, request: { messages } }.");
1331
+ }
1332
+ if (body.request.toolChoice && typeof body.request.toolChoice === "object") {
1333
+ return jsonError(400, "A forced tool call cannot come from the browser.");
1334
+ }
1335
+ const provider = Object.hasOwn(options.providers, body.providerId) ? options.providers[body.providerId] : void 0;
1336
+ if (!provider) {
1337
+ return jsonError(400, `Unknown providerId "${body.providerId}". Register it in the handler's providers map.`);
1338
+ }
1339
+ const clientTools = Array.isArray(body.clientTools) ? body.clientTools : [];
1340
+ for (const t of clientTools) {
1341
+ if (typeof t?.name !== "string") return jsonError(400, "A browser tool needs a name.");
1342
+ if (options.clientTools === false) return jsonError(400, "This endpoint accepts no browser tools.");
1343
+ if (tools.has(t.name)) return jsonError(400, `Browser tool "${t.name}" has the name of a server tool.`);
1344
+ if (Array.isArray(options.clientTools) && !options.clientTools.includes(t.name)) {
1345
+ return jsonError(400, `Browser tool "${t.name}" is not allowed here.`);
1346
+ }
1347
+ }
1348
+ let request = {
1349
+ ...body.request,
1350
+ tools: [...[...tools.values()].map((t) => t.tool), ...clientTools]
1351
+ };
1352
+ if (options.prepareRequest) request = await options.prepareRequest(request, req);
1353
+ const tail = unansweredTail(request.messages);
1354
+ if (!body.resume && tail.length > 0) {
1355
+ return jsonError(400, "The history ends on unanswered tool calls: a resume needs its ticket.");
1356
+ }
1357
+ let outcomes = {};
1358
+ if (body.resume) {
1359
+ const payload = typeof body.resume.ticket === "string" ? await verifyTicket(body.resume.ticket, secrets, subtle, maxAge) : null;
1360
+ if (!payload || payload.messageId !== body.messageId) {
1361
+ return jsonError(400, "Invalid or expired continuation ticket.");
1362
+ }
1363
+ if (!sameCalls(payload.calls, tail.map((c) => ({ id: c.id, name: c.name, input: c.input })))) {
1364
+ return jsonError(400, "The pending tool calls do not match the ticket.");
1365
+ }
1366
+ const ids = new Set(payload.calls.map((c) => c.id));
1367
+ const given = body.resume.outcomes && typeof body.resume.outcomes === "object" ? body.resume.outcomes : {};
1368
+ for (const id of Object.keys(given)) {
1369
+ if (!ids.has(id)) return jsonError(400, `No pending call "${id}" in the ticket.`);
1370
+ }
1371
+ if (options.onTicket && !await options.onTicket(body.resume.ticket.split(".")[1])) {
1372
+ return jsonError(409, "This continuation ticket was already used.");
1373
+ }
1374
+ outcomes = withoutUnacceptedPayloads(given, payload.calls, tools);
1375
+ }
1376
+ const key = await options.resolveKey?.(body.providerId);
1377
+ const external = new Set(clientTools.map((t) => t.name));
1378
+ const rule = (call) => options.approvalPolicy?.(call, tools.get(call.name)?.tool);
1379
+ const run = new AbortController();
1380
+ if (req.signal.aborted) run.abort();
1381
+ else req.signal.addEventListener("abort", () => run.abort(), { once: true });
1382
+ const encoder2 = new TextEncoder();
1383
+ let pendingWrite;
1384
+ const stream = new ReadableStream({
1385
+ start(controller) {
1386
+ let open = true;
1387
+ const write = (line) => {
1388
+ if (!open) return;
1389
+ try {
1390
+ controller.enqueue(encoder2.encode(JSON.stringify(line) + "\n"));
1391
+ } catch {
1392
+ open = false;
1393
+ }
1394
+ };
1395
+ const close = () => {
1396
+ if (!open) return;
1397
+ open = false;
1398
+ try {
1399
+ controller.close();
1400
+ } catch {
1401
+ }
1402
+ };
1403
+ const emitter = (event) => {
1404
+ if (event.type === "tool-failed") {
1405
+ console.error(`[aparte] tool call ${event.toolCallId} failed:`, event.error);
1406
+ write({ ...event, error: "The tool failed." });
1407
+ return;
1408
+ }
1409
+ if (event.type !== "run-suspended") {
1410
+ write(event);
1411
+ return;
1412
+ }
1413
+ const calls = event.pending.map((p) => ({ id: p.toolCallId, name: p.name, input: p.input }));
1414
+ pendingWrite = signTicket({ v: 1, iat: Date.now(), messageId: body.messageId, calls }, secrets[0], subtle).then((ticket) => write({ ...event, ticket }));
1415
+ };
1416
+ void runStreamAgent({
1417
+ messageId: body.messageId,
1418
+ providerId: body.providerId,
1419
+ baseRequest: request,
1420
+ transportCall: async (r) => {
1421
+ const response = await transport.chat(provider, r, key, { providerId: body.providerId, signal: run.signal });
1422
+ return typeof response === "string" ? response : readableToAsyncIterable(response, run.signal);
1423
+ },
1424
+ toolLookup: (name) => {
1425
+ const t = tools.get(name);
1426
+ return t ? (call, signal) => t.handler(call, signal, { request: req }) : void 0;
1427
+ },
1428
+ toolConfigLookup: (name) => {
1429
+ if (external.has(name)) return { external: true };
1430
+ const t = tools.get(name);
1431
+ if (!t) return void 0;
1432
+ return {
1433
+ ...t.tool.maxTurns !== void 0 ? { maxTurns: t.tool.maxTurns } : {},
1434
+ needsApproval: (call) => {
1435
+ const ruling = rule(call);
1436
+ if (ruling?.verdict === "allow") return false;
1437
+ if (ruling?.verdict) return ruling.verdict;
1438
+ return Boolean(t.tool.needsApproval);
1439
+ }
1440
+ };
1441
+ },
1442
+ // Reached only for a server `deny`: an `ask` is deferred to the page.
1443
+ approvalResolver: async (call) => ({
1444
+ approved: false,
1445
+ reason: rule(call)?.reason?.trim() || "Tool execution was refused by the server policy."
1446
+ }),
1447
+ deferral: { outcomes },
1448
+ emitter,
1449
+ signal: run.signal,
1450
+ ...options.maxTurns !== void 0 ? { maxTurns: options.maxTurns } : {},
1451
+ ...options.toolTimeoutMs !== void 0 ? { toolTimeoutMs: options.toolTimeoutMs } : {}
1452
+ }).then(async () => {
1453
+ await pendingWrite;
1454
+ close();
1455
+ }, async (error) => {
1456
+ console.error("[aparte] agent run failed", error);
1457
+ await pendingWrite?.catch(() => {
1458
+ });
1459
+ write({
1460
+ type: "run-error",
1461
+ message: "The agent run failed.",
1462
+ code: error instanceof AparteError ? error.code : "AGENT_RUN_FAILED"
1463
+ });
1464
+ close();
1465
+ });
1466
+ },
1467
+ cancel() {
1468
+ run.abort();
1469
+ }
1470
+ });
1471
+ return new Response(stream, { headers: { "Content-Type": "application/x-ndjson" } });
1472
+ };
1473
+ }
1474
+ function withoutUnacceptedPayloads(outcomes, calls, tools) {
1475
+ const out = {};
1476
+ for (const [id, outcome] of Object.entries(outcomes)) {
1477
+ const name = calls.find((c) => c.id === id)?.name;
1478
+ if (outcome?.kind === "decision" && outcome.payload !== void 0 && !(name && tools.get(name)?.acceptsApprovalPayload)) {
1479
+ const { payload: _dropped, ...rest } = outcome;
1480
+ out[id] = rest;
1481
+ } else {
1482
+ out[id] = outcome;
1483
+ }
1484
+ }
1485
+ return out;
1486
+ }
896
1487
  function jsonError(status, message) {
897
1488
  return new Response(JSON.stringify({ error: { message } }), {
898
1489
  status,
@@ -3135,118 +3726,6 @@ const DEFAULT_RENDERERS = [
3135
3726
  errorRenderer,
3136
3727
  toolCallRenderer
3137
3728
  ];
3138
- const OPEN_TOOL_STATUSES = /* @__PURE__ */ new Set(["pending", "awaiting-approval"]);
3139
- const ACTIVITY_KEYS = ["content", "output", "result"];
3140
- function isActivity(updates) {
3141
- return ACTIVITY_KEYS.some((key) => key in updates);
3142
- }
3143
- function stampSegmentOnInsert(segments, segment, messageId, defaults) {
3144
- const base = applyDefaults(segment, defaults);
3145
- return {
3146
- ...base,
3147
- messageId: segment.messageId ?? messageId,
3148
- index: segment.index ?? segments.length,
3149
- // A start already present is kept — a segment a consumer rebuilt from their own
3150
- // storage keeps the number it was persisted with. `adoptSegment` is the path
3151
- // that never writes one at all.
3152
- ...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
3153
- };
3154
- }
3155
- function adoptSegment(segments, segment, messageId) {
3156
- return {
3157
- ...segment,
3158
- messageId,
3159
- index: segments.length,
3160
- isStreaming: false,
3161
- ...segment.type === "tool_call" && segment.status === "awaiting-approval" ? { status: "aborted" } : {}
3162
- };
3163
- }
3164
- const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
3165
- function segmentTiming(segment) {
3166
- return segment.meta?.aparte;
3167
- }
3168
- function timingPatch(segment, timing) {
3169
- return { meta: { ...segment.meta, aparte: { ...segment.meta?.aparte, ...timing } } };
3170
- }
3171
- function applyDefaults(segment, defaults) {
3172
- if (!defaults) return segment;
3173
- const out = { ...segment };
3174
- for (const [key, value] of Object.entries(defaults)) {
3175
- if (RESERVED.has(key) || key in segment) continue;
3176
- if (key === "meta") {
3177
- const { aparte: _forged, ...rest } = value ?? {};
3178
- out.meta = rest;
3179
- continue;
3180
- }
3181
- out[key] = value;
3182
- }
3183
- return out;
3184
- }
3185
- function isSegmentSettled(segment) {
3186
- if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
3187
- return segment.isStreaming === false;
3188
- }
3189
- function stampSegmentOnUpdate(segment, updates) {
3190
- if (isSegmentSettled(segment)) return updates;
3191
- const merged = { ...segment, ...updates };
3192
- if (isSegmentSettled(merged)) {
3193
- const ended = segmentTiming(segment)?.endedAt;
3194
- return { ...updates, ...timingPatch(segment, { endedAt: ended ?? Date.now() }) };
3195
- }
3196
- return isActivity(updates) ? { ...updates, ...timingPatch(segment, { endedAt: Date.now() }) } : updates;
3197
- }
3198
- function mergeSegmentUpdate(segment, updates) {
3199
- const merged = { ...segment, ...updates };
3200
- if (!("meta" in updates)) return merged;
3201
- return {
3202
- ...merged,
3203
- meta: {
3204
- ...segment.meta,
3205
- ...updates.meta,
3206
- // Neither side's timing wins by accident: whichever one carries it, wins,
3207
- // and a consumer writing `meta` without an `aparte` key keeps ours.
3208
- ...segment.meta?.aparte || updates.meta?.aparte ? { aparte: { ...segment.meta?.aparte, ...updates.meta?.aparte } } : {}
3209
- }
3210
- };
3211
- }
3212
- function segmentDuration(segment) {
3213
- const { startedAt, endedAt } = segmentTiming(segment) ?? {};
3214
- if (startedAt == null || endedAt == null) return void 0;
3215
- return endedAt - startedAt;
3216
- }
3217
- function stampSegmentActivity(segment) {
3218
- return isSegmentSettled(segment) ? {} : timingPatch(segment, { endedAt: Date.now() });
3219
- }
3220
- function segmentContentUpdate(segment) {
3221
- const content = segment.content;
3222
- return {
3223
- content,
3224
- ...segment.isStreaming === false ? { isStreaming: false } : {}
3225
- };
3226
- }
3227
- function isTerminalStatus(status) {
3228
- return status != null && status !== "streaming" && status !== "pending";
3229
- }
3230
- function openSegmentIds(segments) {
3231
- return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
3232
- }
3233
- function adoptMessageSegments(message) {
3234
- if (!message.segments?.length) return { ...message };
3235
- return {
3236
- ...message,
3237
- segments: message.segments.reduce((acc, segment) => {
3238
- acc.push(adoptSegment(acc, segment, message.id));
3239
- return acc;
3240
- }, [])
3241
- };
3242
- }
3243
- function renumberSegments(segments) {
3244
- for (let i = 0; i < segments.length; i++) {
3245
- const segment = segments[i];
3246
- if (segment.index === i) continue;
3247
- segments[i] = { ...segment, index: i };
3248
- }
3249
- }
3250
3729
  function populateBubbleFromMessage(bubble, message, siblingInfo) {
3251
3730
  if (message.segments && message.segments.length > 0) {
3252
3731
  const existing = bubble.getSegments?.() ?? [];
@@ -3740,6 +4219,7 @@ class AparteConversationController {
3740
4219
  _onMessageDone = null;
3741
4220
  _onMessageError = null;
3742
4221
  _onMessageAborted = null;
4222
+ _onMessagePaused = null;
3743
4223
  _onSelectConversation = null;
3744
4224
  _unsubscribeManager = null;
3745
4225
  /** The manager the subscription above is on, so a replaced manager is re-subscribed. */
@@ -3864,10 +4344,14 @@ class AparteConversationController {
3864
4344
  this._isStreaming = false;
3865
4345
  queueMicrotask(() => this._persistActive());
3866
4346
  };
4347
+ this._onMessagePaused = () => {
4348
+ queueMicrotask(() => this._persistActive());
4349
+ };
3867
4350
  host.addEventListener("aparte-message-start", this._onMessageStart);
3868
4351
  host.addEventListener("aparte-message-done", this._onMessageDone);
3869
4352
  host.addEventListener("aparte-message-error", this._onMessageError);
3870
4353
  host.addEventListener("aparte-message-aborted", this._onMessageAborted);
4354
+ host.addEventListener("aparte-message-paused", this._onMessagePaused);
3871
4355
  this._onSelectConversation = (e) => {
3872
4356
  const evt = e;
3873
4357
  const targetId = evt.detail?.targetId;
@@ -3915,6 +4399,10 @@ class AparteConversationController {
3915
4399
  host.removeEventListener("aparte-message-aborted", this._onMessageAborted);
3916
4400
  this._onMessageAborted = null;
3917
4401
  }
4402
+ if (this._onMessagePaused) {
4403
+ host.removeEventListener("aparte-message-paused", this._onMessagePaused);
4404
+ this._onMessagePaused = null;
4405
+ }
3918
4406
  if (this._onSelectConversation && typeof window !== "undefined") {
3919
4407
  window.removeEventListener("aparte-conversation-select", this._onSelectConversation);
3920
4408
  this._onSelectConversation = null;
@@ -4028,6 +4516,11 @@ class AparteConversationController {
4028
4516
  this._isLoadingConversation = false;
4029
4517
  this._showWait(false);
4030
4518
  }
4519
+ this._binding.host.dispatchEvent(new CustomEvent("aparte-conversation-loaded", {
4520
+ bubbles: true,
4521
+ composed: true,
4522
+ detail: { targetId: this._binding.hostId, conversationId: id }
4523
+ }));
4031
4524
  }
4032
4525
  /**
4033
4526
  * The binding's wait, owned here: whoever runs `setConversationId` last clears it
@@ -4812,181 +5305,55 @@ class AparteChatHost {
4812
5305
  });
4813
5306
  }
4814
5307
  }
4815
- function dispatchLifecycleEvent(target, name, detail, targetId) {
4816
- const id = targetId ?? (target.id || void 0);
4817
- const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
4818
- target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
5308
+ const STOPPED_TOOL_TEXT = "The tool call was stopped before it ran.";
5309
+ const EMPTY_TOOL_TEXT = "The tool returned no output.";
5310
+ function answerOf(segment) {
5311
+ const result = segment.result?.trim();
5312
+ if (segment.status === "resolved") return result || EMPTY_TOOL_TEXT;
5313
+ if (segment.status === "failed") return result || "The tool failed.";
5314
+ if (segment.status === "rejected") return result || STOPPED_TOOL_TEXT;
5315
+ return STOPPED_TOOL_TEXT;
4819
5316
  }
4820
- function createStreamAdapter(opts) {
4821
- const { target, config, messageId, targetId } = opts;
4822
- let parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
4823
- let streaming = /* @__PURE__ */ new Set();
4824
- let thinkingId = null;
4825
- let thinkingContent = "";
4826
- let thinkingCollapsed = false;
4827
- return (e) => {
4828
- switch (e.type) {
4829
- case "run-start":
4830
- target.updateMessage?.(messageId, { status: "streaming" });
4831
- break;
4832
- case "turn-start":
4833
- parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
4834
- streaming = /* @__PURE__ */ new Set();
4835
- thinkingId = null;
4836
- thinkingContent = "";
4837
- thinkingCollapsed = false;
4838
- break;
4839
- case "thinking-delta": {
4840
- thinkingContent += e.delta;
4841
- if (!thinkingId) {
4842
- const seg = {
4843
- id: `think-${uuid()}`,
4844
- type: "thinking",
4845
- content: thinkingContent,
4846
- collapsed: true,
4847
- label: "Thinking"
4848
- };
4849
- thinkingId = seg.id;
4850
- streaming.add(seg.id);
4851
- target.addSegment?.(seg);
4852
- } else {
4853
- target.updateSegment?.(thinkingId, { content: thinkingContent });
4854
- }
4855
- break;
4856
- }
4857
- case "text-delta": {
4858
- if (thinkingId && !thinkingCollapsed) {
4859
- target.updateSegment?.(thinkingId, { collapsed: true, isStreaming: false });
4860
- thinkingCollapsed = true;
4861
- }
4862
- const result = parser.parse(e.delta);
4863
- for (const segment of result.segments) {
4864
- if (!streaming.has(segment.id)) {
4865
- target.addSegment?.(segment);
4866
- streaming.add(segment.id);
4867
- } else if ("content" in segment) {
4868
- target.updateSegment?.(segment.id, segmentContentUpdate(segment));
4869
- }
4870
- }
4871
- const active = parser.getState().activeSegment;
4872
- if (active) {
4873
- if (!streaming.has(active.id)) {
4874
- target.addSegment?.(active);
4875
- streaming.add(active.id);
4876
- } else {
4877
- target.updateSegment?.(active.id, segmentContentUpdate(active));
4878
- }
4879
- } else if (result.segments.length === 0) ;
4880
- break;
4881
- }
4882
- case "text-flush": {
4883
- const finals = parser.finalize();
4884
- for (const s of finals) {
4885
- if (!streaming.has(s.id)) target.addSegment?.(s);
4886
- else if ("content" in s) target.updateSegment?.(s.id, segmentContentUpdate(s));
4887
- }
4888
- break;
4889
- }
4890
- case "tool-start": {
4891
- const toolSeg = {
4892
- id: `tool-${e.toolCallId}`,
4893
- type: "tool_call",
4894
- toolCall: { id: e.toolCallId, name: e.name, input: e.input },
4895
- status: "pending"
4896
- };
4897
- const renderer = config.getToolRenderer(e.name);
4898
- if (renderer) {
4899
- injectToolRendererStyles(e.name, renderer);
4900
- const html = renderer.render(toolSeg);
4901
- if (html) target.addSegment?.(toolSeg);
4902
- } else {
4903
- target.addSegment?.(toolSeg);
4904
- }
4905
- break;
5317
+ const isOpenDeferred = (segment) => segment.type === "tool_call" && Boolean(segment.deferred) && (segment.status === "awaiting-approval" || segment.status === "pending");
5318
+ function wireTurns(message, textOf, options = {}) {
5319
+ const segments = message.segments ?? [];
5320
+ if (message.role !== "assistant" || !segments.some((s) => s.type === "tool_call")) {
5321
+ const content = textOf(segments) || (typeof message.content === "string" ? message.content : "");
5322
+ return content ? [{ role: message.role, content }] : [];
5323
+ }
5324
+ const out = [];
5325
+ let texts = [];
5326
+ let calls = [];
5327
+ const flush = () => {
5328
+ const content = textOf(texts);
5329
+ if (calls.length === 0) {
5330
+ if (content) out.push({ role: "assistant", content });
5331
+ } else {
5332
+ out.push({
5333
+ role: "assistant",
5334
+ content,
5335
+ toolCalls: calls.map((c) => ({ id: c.toolCall.id, name: c.toolCall.name, input: c.toolCall.input }))
5336
+ });
5337
+ for (const c of calls) {
5338
+ if (options.openDeferred && isOpenDeferred(c)) continue;
5339
+ out.push({ role: "tool", content: answerOf(c), toolCallId: c.toolCall.id, toolName: c.toolCall.name });
4906
5340
  }
4907
- case "tool-awaiting-approval":
4908
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "awaiting-approval" });
4909
- dispatchLifecycleEvent(target, "aparte-tool-approval-request", { toolCallId: e.toolCallId, toolName: e.name, input: e.input }, targetId);
4910
- break;
4911
- case "tool-approved":
4912
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "pending" });
4913
- break;
4914
- case "tool-rejected":
4915
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "rejected", result: e.reason });
4916
- break;
4917
- case "tool-resolved":
4918
- target.updateSegment?.(`tool-${e.toolCallId}`, {
4919
- status: "resolved",
4920
- result: e.result,
4921
- ...e.structuredResult !== void 0 ? { structuredResult: e.structuredResult } : {}
4922
- });
4923
- break;
4924
- case "tool-aborted":
4925
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "aborted" });
4926
- break;
4927
- case "tool-failed":
4928
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "failed", result: e.error });
4929
- break;
4930
- case "turn-limit-exceeded":
4931
- if (e.scope === "global") {
4932
- target.addSegment?.({
4933
- id: `max-turns-${uuid()}`,
4934
- type: "error",
4935
- content: `Stopped after ${e.limit} tool calls to prevent an infinite loop.`,
4936
- details: "MAX_TURNS_EXCEEDED"
4937
- });
4938
- } else if (e.toolCallId) {
4939
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "aborted" });
4940
- }
4941
- break;
4942
- case "run-aborted":
4943
- dispatchLifecycleEvent(target, "aparte-message-aborted", { messageId }, targetId);
4944
- break;
4945
- case "run-done":
4946
- target.updateMessage?.(messageId, { status: "completed" });
4947
- if (e.usage) {
4948
- try {
4949
- target.setUsage?.(messageId, e.usage);
4950
- } catch {
4951
- }
4952
- }
4953
- break;
4954
5341
  }
5342
+ texts = [];
5343
+ calls = [];
4955
5344
  };
4956
- }
4957
- function readableToAsyncIterable(stream, signal) {
4958
- return {
4959
- [Symbol.asyncIterator]() {
4960
- const reader = stream.getReader();
4961
- const onAbort = () => {
4962
- try {
4963
- void reader.cancel();
4964
- } catch {
4965
- }
4966
- };
4967
- if (signal.aborted) onAbort();
4968
- else signal.addEventListener("abort", onAbort, { once: true });
4969
- return {
4970
- async next() {
4971
- const { done, value } = await reader.read();
4972
- if (done) return { done: true, value: void 0 };
4973
- return { done: false, value };
4974
- },
4975
- async return() {
4976
- signal.removeEventListener("abort", onAbort);
4977
- try {
4978
- await reader.cancel();
4979
- } catch {
4980
- }
4981
- try {
4982
- reader.releaseLock();
4983
- } catch {
4984
- }
4985
- return { done: true, value: void 0 };
4986
- }
4987
- };
5345
+ let open = false;
5346
+ for (const segment of segments) {
5347
+ if (segment.type === "tool_call") {
5348
+ calls.push(segment);
5349
+ if (options.openDeferred && isOpenDeferred(segment)) open = true;
5350
+ } else {
5351
+ if (calls.length > 0 && !open) flush();
5352
+ texts.push(segment);
4988
5353
  }
4989
- };
5354
+ }
5355
+ flush();
5356
+ return out;
4990
5357
  }
4991
5358
  function eventOrigin(e) {
4992
5359
  return e.composedPath?.()[0] ?? e.target;
@@ -5523,6 +5890,7 @@ const CHAT_SELECTOR = "aparte-chat, aparte-chat-viewport, [data-aparte-chat]";
5523
5890
  class AparteClient {
5524
5891
  _boundHandler = null;
5525
5892
  _boundAbortHandler = null;
5893
+ _boundLoadedHandler = null;
5526
5894
  _boundRetryHandler = null;
5527
5895
  _boundEditHandler = null;
5528
5896
  _releaseKeyResolver = null;
@@ -5650,6 +6018,14 @@ class AparteClient {
5650
6018
  };
5651
6019
  }
5652
6020
  window.addEventListener("aparte-edit", this._boundEditHandler);
6021
+ if (!this._boundLoadedHandler) {
6022
+ this._boundLoadedHandler = (e) => {
6023
+ if (!this._isForThisInstance(e)) return;
6024
+ const host = e.target instanceof HTMLElement ? e.target : void 0;
6025
+ void this.resumeDeferred(host ?? e.detail?.targetId);
6026
+ };
6027
+ }
6028
+ window.addEventListener("aparte-conversation-loaded", this._boundLoadedHandler);
5653
6029
  }
5654
6030
  /**
5655
6031
  * Stop listening.
@@ -5665,6 +6041,10 @@ class AparteClient {
5665
6041
  window.removeEventListener("aparte-abort", this._boundAbortHandler);
5666
6042
  this._boundAbortHandler = null;
5667
6043
  }
6044
+ if (this._boundLoadedHandler) {
6045
+ window.removeEventListener("aparte-conversation-loaded", this._boundLoadedHandler);
6046
+ this._boundLoadedHandler = null;
6047
+ }
5668
6048
  if (this._boundRetryHandler) {
5669
6049
  window.removeEventListener("aparte-retry", this._boundRetryHandler);
5670
6050
  this._boundRetryHandler = null;
@@ -5800,14 +6180,54 @@ class AparteClient {
5800
6180
  }
5801
6181
  dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" }, chatId);
5802
6182
  const streamController = new AbortController();
6183
+ if (this._streamController !== streamController) this._streamController?.abort();
6184
+ this._streamController = streamController;
5803
6185
  try {
5804
- const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
6186
+ let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
6187
+ const maxResumes = this.options.maxTurns ?? 10;
6188
+ let resumes = 0;
6189
+ while (result.suspended) {
6190
+ const stopped = this._isAborted || streamController.signal.aborted;
6191
+ if (!stopped && ++resumes > maxResumes) {
6192
+ this._closeOpenCalls(targetElement, messageId);
6193
+ createStreamAdapter({ target: targetElement, config: this._config, messageId, ...chatId ? { targetId: chatId } : {} })(
6194
+ { type: "turn-limit-exceeded", scope: "global", limit: maxResumes }
6195
+ );
6196
+ break;
6197
+ }
6198
+ const outcomes = stopped ? void 0 : await this._answerDeferred(targetElement, result.suspended.pending, streamController.signal);
6199
+ if (!outcomes) {
6200
+ this._closeOpenCalls(targetElement, messageId);
6201
+ if (this._isAborted || streamController.signal.aborted) {
6202
+ dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
6203
+ }
6204
+ break;
6205
+ }
6206
+ const deferral = {
6207
+ outcomes,
6208
+ ...result.suspended.ticket ? { token: result.suspended.ticket } : {}
6209
+ };
6210
+ const messages2 = this._resumeMessages(targetElement, messageId, baseRequest.messages);
6211
+ this._spendPause(targetElement, messageId);
6212
+ result = await this._streamLoop(
6213
+ targetElement,
6214
+ messageId,
6215
+ provider,
6216
+ { ...baseRequest, messages: messages2 },
6217
+ authConfig,
6218
+ streamController,
6219
+ deferral
6220
+ );
6221
+ }
6222
+ const usage = result.usage;
5805
6223
  if (this._isAborted || streamController.signal.aborted) {
6224
+ this._closeOpenCalls(targetElement, messageId);
5806
6225
  this._updateMessage(targetElement, messageId, { status: "completed" });
5807
6226
  } else {
5808
6227
  dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage }, chatId);
5809
6228
  }
5810
6229
  } catch (error) {
6230
+ this._closeOpenCalls(targetElement, messageId);
5811
6231
  if (this._isAborted || streamController.signal.aborted) {
5812
6232
  dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
5813
6233
  this._updateMessage(targetElement, messageId, { status: "completed" });
@@ -6110,10 +6530,13 @@ class AparteClient {
6110
6530
  return messages.filter((m) => {
6111
6531
  if (m.role !== "user" && m.role !== "assistant") return false;
6112
6532
  return m.status !== "error" && !AparteClient._inFlight(m);
6113
- }).map((m) => ({
6114
- role: m.role,
6115
- content: this._wireText(m)
6116
- })).filter((m) => m.content.length > 0);
6533
+ }).flatMap((m) => {
6534
+ if (m.role === "assistant" && m.segments?.some((s) => s.type === "tool_call")) {
6535
+ return wireTurns(m, (segments) => this._segmentsToText(segments));
6536
+ }
6537
+ const content = this._wireText(m);
6538
+ return content ? [{ role: m.role, content }] : [];
6539
+ });
6117
6540
  }
6118
6541
  /** The "no model selected" warning is said once per client, not once per dropped send. */
6119
6542
  _warnedNoModel = false;
@@ -6157,6 +6580,7 @@ class AparteClient {
6157
6580
  console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
6158
6581
  return;
6159
6582
  }
6583
+ this._closeDeferred(targetElement);
6160
6584
  if (this.options.echoUserMessage !== false && event.detail?.echoed !== true) {
6161
6585
  const echoFiles = Array.isArray(event.detail?.files) ? event.detail.files : [];
6162
6586
  targetElement.appendMessage?.({
@@ -6319,8 +6743,8 @@ ${text}` : text;
6319
6743
  *
6320
6744
  * `thinking` and `tool_call` stay out on purpose — the first is the model's own
6321
6745
  * scratchpad, and most APIs neither want it back nor bill for it kindly; the
6322
- * second was already carried, within the turn, as the assistant's calls and the
6323
- * `tool` messages answering them.
6746
+ * second is not text: `_wireHistory` sends a reply's calls as the assistant's
6747
+ * `toolCalls` and the `tool` messages answering them (`wire-history.ts`).
6324
6748
  */
6325
6749
  _segmentsToText(segments) {
6326
6750
  if (!segments?.length) return "";
@@ -6424,11 +6848,12 @@ ${content}
6424
6848
  * The turn's agent loop — the engine's `runStreamAgent` rendered through the core
6425
6849
  * adapter, or the runner a host injected — after the fetch-level abort bookkeeping.
6426
6850
  */
6427
- async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController = new AbortController()) {
6428
- this._streamController?.abort();
6851
+ /** One run of the loop: its usage, and whether it paused (a server loop). */
6852
+ async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController = new AbortController(), deferral) {
6853
+ if (this._streamController !== streamController) this._streamController?.abort();
6429
6854
  this._streamController = streamController;
6430
6855
  const runner = this.options.streamRunner ?? runStreamAgent;
6431
- return this._runViaStreamRunner(runner, targetElement, messageId, provider, baseRequest, authConfig, streamController);
6856
+ return this._runViaStreamRunner(runner, targetElement, messageId, provider, baseRequest, authConfig, streamController, deferral);
6432
6857
  }
6433
6858
  /**
6434
6859
  * Run the loop through a {@link AparteStreamRunner} — the engine's by default —
@@ -6439,7 +6864,7 @@ ${content}
6439
6864
  * produces is pinned by the `stream-parity` suite's snapshots, recorded while
6440
6865
  * the inline loop it replaced still ran beside it.
6441
6866
  */
6442
- async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController) {
6867
+ async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController, deferral) {
6443
6868
  const signal = streamController.signal;
6444
6869
  const chatId = this._targetIdOf(targetElement);
6445
6870
  const adapter = createStreamAdapter({
@@ -6449,9 +6874,14 @@ ${content}
6449
6874
  ...chatId ? { targetId: chatId } : {}
6450
6875
  });
6451
6876
  const prefixSegments = baseRequest._meta?.["prefixSegments"];
6877
+ let suspended;
6452
6878
  const emitter = (event) => {
6453
6879
  adapter(event);
6454
- if (event.type === "run-start" && prefixSegments?.length) {
6880
+ if (event.type === "run-suspended") {
6881
+ const ticket = event.ticket;
6882
+ suspended = { pending: event.pending, ...typeof ticket === "string" ? { ticket } : {} };
6883
+ }
6884
+ if (event.type === "run-start" && prefixSegments?.length && !deferral) {
6455
6885
  for (const seg of prefixSegments) targetElement.addSegment?.(seg);
6456
6886
  }
6457
6887
  };
@@ -6477,16 +6907,11 @@ ${content}
6477
6907
  }
6478
6908
  };
6479
6909
  };
6480
- const approvalResolver = this.options.approvalResolver ?? (async (call, sig) => {
6481
- const ruling = this._config.ruleOnToolCall(call);
6482
- if (ruling.verdict === "allow") return { approved: true };
6483
- if (ruling.verdict === "deny") {
6484
- return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy." };
6485
- }
6486
- return this._askForApproval(call, sig, targetElement);
6487
- });
6910
+ const approvalResolver = this._approvalResolverFor(targetElement);
6488
6911
  const usage = await streamRunner({
6489
6912
  messageId,
6913
+ providerId: provider.id,
6914
+ ...deferral ? { deferral } : {},
6490
6915
  baseRequest,
6491
6916
  transportCall,
6492
6917
  toolLookup,
@@ -6504,8 +6929,196 @@ ${content}
6504
6929
  // BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
6505
6930
  idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
6506
6931
  });
6507
- return usage ?? void 0;
6932
+ return { usage: usage ?? void 0, ...suspended ? { suspended } : {} };
6933
+ }
6934
+ /**
6935
+ * The approval channel a run asks through — and the one a paused server run's calls
6936
+ * are answered through, so the page asks the same way whichever loop paused.
6937
+ *
6938
+ * `gatedElsewhere`: a server already decided this call needs a person. The page's
6939
+ * policy may still answer it when it has an opinion (`auto` mode allows, a plan
6940
+ * refuses), but with none the person is asked. `ruleOnToolCall`'s own fallback —
6941
+ * the tool's `needsApproval` — reads a tool the page does not have, and said
6942
+ * "allow": the page approved, silently, what the server wanted a person to decide.
6943
+ */
6944
+ _approvalResolverFor(targetElement, gatedElsewhere = false) {
6945
+ return this.options.approvalResolver ?? (async (call, sig) => {
6946
+ const ruling = gatedElsewhere ? this._config.getApprovalPolicy()?.(call, this._config.getTools().find((t) => t.name === call.name)) ?? { verdict: "ask" } : this._config.ruleOnToolCall(call);
6947
+ if (ruling.verdict === "allow") return { approved: true };
6948
+ if (ruling.verdict === "deny") {
6949
+ return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy." };
6950
+ }
6951
+ return this._askForApproval(call, sig, targetElement);
6952
+ });
6953
+ }
6954
+ /**
6955
+ * The answers a paused server run waits for, asked HERE because only the page can: an
6956
+ * approval through the same channel a run in the page uses (the panel, the policy, a
6957
+ * host's own resolver), a browser tool through the handler `registerTool` gave it.
6958
+ * A `queued` call needs nothing — the server runs it on resume. `undefined` when
6959
+ * nobody answered — the turn was stopped, or the channel gave up — and there is no
6960
+ * resume. A channel that CRASHES fails the turn, as it does for a run in the page.
6961
+ */
6962
+ async _answerDeferred(targetElement, pending, signal) {
6963
+ const outcomes = {};
6964
+ const resolver = this._approvalResolverFor(targetElement, true);
6965
+ for (const p of pending) {
6966
+ if (signal.aborted) break;
6967
+ const call = { id: p.toolCallId, name: p.name, input: p.input ?? {} };
6968
+ if (p.reason === "approval") {
6969
+ let decision;
6970
+ try {
6971
+ decision = await resolver(call, signal);
6972
+ } catch (error) {
6973
+ if (!signal.aborted && error?.name !== "AbortError") throw error;
6974
+ }
6975
+ if (!decision || signal.aborted) return void 0;
6976
+ outcomes[p.toolCallId] = { kind: "decision", ...decision };
6977
+ } else if (p.reason === "external") {
6978
+ const handler = this._config.getToolHandler(p.name);
6979
+ try {
6980
+ if (!handler) throw new Error(`No browser handler is registered for "${p.name}".`);
6981
+ const result = await handler(call, signal, { target: targetElement, config: this._config });
6982
+ outcomes[p.toolCallId] = {
6983
+ kind: "result",
6984
+ content: result.content,
6985
+ ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
6986
+ };
6987
+ } catch (error) {
6988
+ if (signal.aborted) break;
6989
+ outcomes[p.toolCallId] = { kind: "failed", error: error instanceof Error ? error.message : String(error) };
6990
+ }
6991
+ }
6992
+ }
6993
+ return signal.aborted ? void 0 : outcomes;
6994
+ }
6995
+ /**
6996
+ * The ticket is spent the moment the resume leaves: the server answers these calls
6997
+ * now, so a pause saved from here on — the conversation manager saves before it cuts a
6998
+ * switch — would be resumed a second time, against a server that has moved on. The
6999
+ * history that resumes them is built first; a new pause marks what still waits.
7000
+ */
7001
+ _spendPause(targetElement, messageId) {
7002
+ const segments = targetElement.getMessage?.(messageId)?.segments ?? targetElement.getMessages?.()?.find((m) => m.id === messageId)?.segments;
7003
+ const paused = (segment) => segment.type === "tool_call" && Boolean(segment.deferred);
7004
+ if (!segments?.some(paused)) return;
7005
+ targetElement.updateMessage?.(messageId, {
7006
+ segments: segments.map((segment) => {
7007
+ if (!paused(segment)) return segment;
7008
+ const { deferred: _spent, ...rest } = segment;
7009
+ return rest;
7010
+ })
7011
+ });
7012
+ }
7013
+ /**
7014
+ * Close the reply's calls that can no longer finish — paused, queued, or running on a
7015
+ * server the page stopped listening to: each says it stopped, and forgets the pause.
7016
+ * A cut connection carries none of their last events, so nothing else would: the rows
7017
+ * spun, and a queued call kept its ticket, which a reload then resumed against a
7018
+ * server that had moved on. By the reply's id, not the chat's head — a retry during
7019
+ * the wait moves the head. A run in the page ends every call itself: nothing to do.
7020
+ */
7021
+ _closeOpenCalls(targetElement, messageId) {
7022
+ const segments = targetElement.getMessage?.(messageId)?.segments ?? targetElement.getMessages?.()?.find((m) => m.id === messageId)?.segments;
7023
+ const unfinished = (segment) => segment.type === "tool_call" && (segment.status === "pending" || segment.status === "awaiting-approval");
7024
+ if (!segments?.some(unfinished)) return;
7025
+ targetElement.updateMessage?.(messageId, {
7026
+ segments: segments.map((segment) => {
7027
+ if (!unfinished(segment)) return segment;
7028
+ const { deferred: _spent, ...rest } = segment;
7029
+ return { ...rest, status: "aborted" };
7030
+ })
7031
+ });
7032
+ }
7033
+ /**
7034
+ * The history a resume sends: the request the turn was sent with — interceptor and
7035
+ * `history` option included, not rebuilt from the transcript — then the reply so
7036
+ * far, its paused calls left open for the server to answer.
7037
+ */
7038
+ _resumeMessages(targetElement, messageId, sent) {
7039
+ const reply = targetElement.getMessages?.().find((m) => m.id === messageId);
7040
+ return [...sent, ...reply ? wireTurns(reply, (segments) => this._segmentsToText(segments), { openDeferred: true }) : []];
7041
+ }
7042
+ /**
7043
+ * What the send of a reloaded turn put before its reply, rebuilt the way
7044
+ * `_buildMessages` builds it: the system prompts, the history `options.history`
7045
+ * asks for — given the transcript before the reply, where the send also saw the
7046
+ * reply's empty shell; handing over the paused reply would send it twice — and the
7047
+ * message that asked.
7048
+ * That message goes as its text: the files a send read are not in a transcript.
7049
+ */
7050
+ _resumeBase(all, replyIndex) {
7051
+ const before = all.slice(0, replyIndex);
7052
+ const askedAt = before.map((m) => m.role).lastIndexOf("user");
7053
+ const historyOption = this.options.history ?? "viewport";
7054
+ const history = historyOption === "viewport" ? this._toHistoryMessages(before) : typeof historyOption === "function" ? historyOption(before) : [];
7055
+ return [...this._systemMessages(), ...history, ...askedAt >= 0 ? this._wireHistory([before[askedAt]]) : []];
7056
+ }
7057
+ /**
7058
+ * A new message closes a wait nobody answered. The paused calls of the last reply are
7059
+ * marked stopped, so the history the new turn sends answers them ("stopped before it
7060
+ * ran") — a provider rejects a request that declares a call with no answer.
7061
+ */
7062
+ _closeDeferred(targetElement) {
7063
+ const messages = targetElement.getMessages?.() ?? [];
7064
+ const last = [...messages].reverse().find((m) => m.role === "assistant");
7065
+ if (!last?.segments?.some(isOpenDeferred)) return;
7066
+ const segments = last.segments.map((segment) => {
7067
+ if (!isOpenDeferred(segment)) return segment;
7068
+ const { deferred: _closed, ...rest } = segment;
7069
+ return { ...rest, status: "aborted" };
7070
+ });
7071
+ targetElement.updateMessage?.(last.id, { segments });
7072
+ }
7073
+ /**
7074
+ * Answer a turn a server loop paused, when the conversation comes back — a reload, a
7075
+ * conversation reopened days later. The last reply's paused calls (they carry the
7076
+ * server's ticket) are put to the person again, or run in the page, and the loop
7077
+ * resumes on the server, into the same bubble.
7078
+ *
7079
+ * Called on its own when a conversation manager loads a conversation
7080
+ * (`aparte-conversation-loaded`); call it yourself after `setMessages` if you load
7081
+ * conversations another way. Resolves `true` when a resume ran, `false` when there
7082
+ * was nothing to resume.
7083
+ *
7084
+ * @param target The chat — an element, or its id. Defaults to the one `targetResolver` names, else the first on the page.
7085
+ */
7086
+ async resumeDeferred(target) {
7087
+ const host = typeof target === "string" ? this._byId(target) : target ?? this.options.targetResolver?.() ?? null;
7088
+ const targetElement = host ? this._asRenderTarget(host) : this._scanForTarget();
7089
+ if (!targetElement) return false;
7090
+ const messages = targetElement.getMessages?.() ?? [];
7091
+ const last = [...messages].reverse().find((m) => m.role === "assistant");
7092
+ const open = (last?.segments ?? []).filter(isOpenDeferred);
7093
+ const ticket = open.find((s) => s.deferred?.ticket)?.deferred?.ticket;
7094
+ if (!last || open.length === 0 || !ticket || this._resuming.has(last.id)) return false;
7095
+ const config = this._config.getModelConfig();
7096
+ const provider = config.defaultProvider ? this._config.getAIProvider(config.defaultProvider) : void 0;
7097
+ if (!provider) {
7098
+ console.warn("[AparteClient] Cannot resume the paused turn: no provider is selected.");
7099
+ return false;
7100
+ }
7101
+ this._beginUserTurn();
7102
+ this._resuming.add(last.id);
7103
+ try {
7104
+ const authConfig = await this._resolveAuth(provider.id);
7105
+ const pending = open.map((s) => ({
7106
+ toolCallId: s.toolCall.id,
7107
+ name: s.toolCall.name,
7108
+ input: s.toolCall.input,
7109
+ reason: s.deferred.reason
7110
+ }));
7111
+ const base = this._resumeBase(messages, messages.indexOf(last));
7112
+ await this._streamTurn(targetElement, last.id, provider, base, config.defaultModel || "", authConfig, {
7113
+ resume: { pending, ticket }
7114
+ });
7115
+ } finally {
7116
+ this._resuming.delete(last.id);
7117
+ }
7118
+ return true;
6508
7119
  }
7120
+ /** Replies being resumed, so a second load of the same conversation does not ask twice. */
7121
+ _resuming = /* @__PURE__ */ new Set();
6509
7122
  /**
6510
7123
  * Updates an existing message or appends a new error if ID lookup fails
6511
7124
  */
@@ -6542,6 +7155,54 @@ ${content}
6542
7155
  dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error }, this._targetIdOf(target));
6543
7156
  }
6544
7157
  }
7158
+ function createAgentStreamRunner(options) {
7159
+ return async (opts) => {
7160
+ const doFetch = options.fetch ?? fetch;
7161
+ const headers = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
7162
+ const { tools, ...request } = opts.baseRequest;
7163
+ const body = {
7164
+ v: 1,
7165
+ providerId: opts.providerId,
7166
+ messageId: opts.messageId,
7167
+ request,
7168
+ clientTools: tools ?? [],
7169
+ ...opts.deferral?.token ? { resume: { ticket: opts.deferral.token, outcomes: opts.deferral.outcomes ?? {} } } : {}
7170
+ };
7171
+ const response = await doFetch(options.endpoint, {
7172
+ method: "POST",
7173
+ headers: { "Content-Type": "application/json", ...headers },
7174
+ body: JSON.stringify(body),
7175
+ signal: opts.signal,
7176
+ ...options.credentials ? { credentials: options.credentials } : {}
7177
+ });
7178
+ if (!response.ok || !response.body) {
7179
+ const message = await response.json().then((json) => json?.error?.message).catch(() => void 0);
7180
+ throw new AparteError(
7181
+ message || `The agent endpoint answered HTTP ${response.status}.`,
7182
+ AparteError.codeForStatus(response.status) ?? AparteErrorCode.PROVIDER_ERROR,
7183
+ void 0,
7184
+ void 0,
7185
+ response.status
7186
+ );
7187
+ }
7188
+ let usage;
7189
+ let ended = false;
7190
+ for await (const line of readNdjson(response.body, opts.signal)) {
7191
+ if (line.type === "run-error") {
7192
+ throw new AparteError(line.message || "The agent run failed.", AparteErrorCode.PROVIDER_ERROR, line.code ? { code: line.code } : void 0);
7193
+ }
7194
+ if (line.type === "run-done" || line.type === "run-suspended") {
7195
+ usage = line.usage;
7196
+ ended = true;
7197
+ }
7198
+ opts.emitter(line);
7199
+ }
7200
+ if (!ended && !opts.signal.aborted) {
7201
+ throw new AparteError("The agent stream ended unexpectedly.", AparteErrorCode.PROVIDER_ERROR);
7202
+ }
7203
+ return usage;
7204
+ };
7205
+ }
6545
7206
  function findHead(node) {
6546
7207
  if (node.next) return findHead(node.next);
6547
7208
  if ("current" in node) return node;
@@ -6848,6 +7509,8 @@ const APARTE_DEFAULT_UI_EVENTS = [
6848
7509
  "aparte-conversation-rename",
6849
7510
  "aparte-conversation-pin",
6850
7511
  "aparte-conversation-unpin",
7512
+ // The conversation controller, on the chat host: a conversation's messages are on screen.
7513
+ "aparte-conversation-loaded",
6851
7514
  // the select primitives
6852
7515
  "aparte-select-change",
6853
7516
  "aparte-select-open",
@@ -6864,6 +7527,7 @@ const APARTE_DEFAULT_UI_EVENTS = [
6864
7527
  "aparte-message-done",
6865
7528
  "aparte-message-error",
6866
7529
  "aparte-message-aborted",
7530
+ "aparte-message-paused",
6867
7531
  "aparte-tool-approval-request"
6868
7532
  ];
6869
7533
  function applyElementProps(el2, props, transformValue = (value) => value) {
@@ -6939,56 +7603,58 @@ function installDialogTriggersOnce(doc = document) {
6939
7603
  doc.addEventListener("click", onClick);
6940
7604
  }
6941
7605
  export {
6942
- resolveConfig as $,
7606
+ registerSegmentRenderer as $,
6943
7607
  APARTE_CONFIG_CHANGE as A,
6944
7608
  contextConfig as B,
6945
7609
  copyText as C,
6946
- createAparteChatHandler as D,
6947
- createStreamAdapter as E,
6948
- cssEscape as F,
6949
- declineDefaultRenderers as G,
6950
- defaultSanitizer as H,
6951
- detachConfig as I,
6952
- escapeAttr as J,
6953
- escapeHtml as K,
6954
- filesToAttachments as L,
6955
- getAllRenderers as M,
6956
- getSegmentRenderer as N,
6957
- installDefaultRenderersOnce as O,
6958
- installDialogTriggersOnce as P,
6959
- isAwaitingReply as Q,
6960
- isFormatAdapter as R,
6961
- isSafeUrl as S,
6962
- isSegmentSettled as T,
6963
- parseAparteEventStream as U,
6964
- parseMarkdownToSegments as V,
6965
- populateBubbleFromMessage as W,
6966
- readableToAsyncIterable as X,
6967
- registerDefaultRenderers as Y,
6968
- registerSegmentRenderer as Z,
6969
- requestUserInput as _,
7610
+ createAgentStreamRunner as D,
7611
+ createAparteAgentHandler as E,
7612
+ createAparteChatHandler as F,
7613
+ createStreamAdapter as G,
7614
+ cssEscape as H,
7615
+ declineDefaultRenderers as I,
7616
+ defaultSanitizer as J,
7617
+ detachConfig as K,
7618
+ escapeAttr as L,
7619
+ escapeHtml as M,
7620
+ filesToAttachments as N,
7621
+ getAllRenderers as O,
7622
+ getSegmentRenderer as P,
7623
+ installDefaultRenderersOnce as Q,
7624
+ installDialogTriggersOnce as R,
7625
+ isAwaitingReply as S,
7626
+ isFormatAdapter as T,
7627
+ isSafeUrl as U,
7628
+ isSegmentSettled as V,
7629
+ parseAparteEventStream as W,
7630
+ parseMarkdownToSegments as X,
7631
+ populateBubbleFromMessage as Y,
7632
+ readableToAsyncIterable as Z,
7633
+ registerDefaultRenderers as _,
6970
7634
  APARTE_CONVERSATION_SCHEMA_VERSION as a,
6971
- revokeAttachmentUrls as a0,
6972
- runWithConfig as a1,
6973
- segmentDuration as a2,
6974
- segmentTiming as a3,
6975
- subscribeConfigChange as a4,
6976
- unregisterSegmentRenderer as a5,
6977
- uuid as a6,
6978
- eventOrigin as a7,
6979
- activeElementIn as a8,
6980
- mergeSegmentUpdate as a9,
6981
- writeStreamedMarkdown as aa,
6982
- stampSegmentActivity as ab,
6983
- stampSegmentOnInsert as ac,
6984
- stampSegmentOnUpdate as ad,
6985
- renumberSegments as ae,
6986
- openSegmentIds as af,
6987
- isTerminalStatus as ag,
6988
- adoptMessageSegments as ah,
6989
- APARTE_DERIVED_SEGMENTS as ai,
6990
- chatBoundaryOf as aj,
6991
- rootOf as ak,
7635
+ requestUserInput as a0,
7636
+ resolveConfig as a1,
7637
+ revokeAttachmentUrls as a2,
7638
+ runWithConfig as a3,
7639
+ segmentDuration as a4,
7640
+ segmentTiming as a5,
7641
+ subscribeConfigChange as a6,
7642
+ unregisterSegmentRenderer as a7,
7643
+ uuid as a8,
7644
+ eventOrigin as a9,
7645
+ activeElementIn as aa,
7646
+ mergeSegmentUpdate as ab,
7647
+ writeStreamedMarkdown as ac,
7648
+ stampSegmentActivity as ad,
7649
+ stampSegmentOnInsert as ae,
7650
+ stampSegmentOnUpdate as af,
7651
+ renumberSegments as ag,
7652
+ openSegmentIds as ah,
7653
+ isTerminalStatus as ai,
7654
+ adoptMessageSegments as aj,
7655
+ APARTE_DERIVED_SEGMENTS as ak,
7656
+ chatBoundaryOf as al,
7657
+ rootOf as am,
6992
7658
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
6993
7659
  APARTE_DEFAULT_HOST_HANDLERS as c,
6994
7660
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -7015,4 +7681,4 @@ export {
7015
7681
  collectRendererStyles as y,
7016
7682
  contentToText as z
7017
7683
  };
7018
- //# sourceMappingURL=dialog-triggers-DwTGKWeU.js.map
7684
+ //# sourceMappingURL=dialog-triggers-Dad9V_ag.js.map