@aparte/core 0.17.3 → 0.19.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 (54) 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/components/conversation-list/aparte-conversation-list.d.ts +15 -0
  9. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  10. package/dist/conversations/conversation-controller.d.ts +1 -0
  11. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  12. package/dist/conversations/index.d.ts +1 -1
  13. package/dist/conversations/index.d.ts.map +1 -1
  14. package/dist/conversations/types.d.ts +14 -0
  15. package/dist/conversations/types.d.ts.map +1 -1
  16. package/dist/custom-elements.json +10883 -10064
  17. package/dist/{dialog-triggers-DwTGKWeU.js → dialog-triggers-xA3T--rt.js} +1132 -454
  18. package/dist/dialog-triggers-xA3T--rt.js.map +1 -0
  19. package/dist/index.css +1 -1
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +86 -45
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.node.d.ts +5 -4
  25. package/dist/index.node.d.ts.map +1 -1
  26. package/dist/index.node.js +35 -33
  27. package/dist/index.node.js.map +1 -1
  28. package/dist/interop/element-props.d.ts +1 -1
  29. package/dist/interop/element-props.d.ts.map +1 -1
  30. package/dist/parsers/aparte-event-stream.d.ts.map +1 -1
  31. package/dist/transport/agent-handler.d.ts +104 -0
  32. package/dist/transport/agent-handler.d.ts.map +1 -0
  33. package/dist/transport/agent-ticket.d.ts +48 -0
  34. package/dist/transport/agent-ticket.d.ts.map +1 -0
  35. package/dist/transport/index.d.ts +1 -0
  36. package/dist/transport/index.d.ts.map +1 -1
  37. package/dist/types/event-map.d.ts +4 -1
  38. package/dist/types/event-map.d.ts.map +1 -1
  39. package/dist/types/events.d.ts +16 -0
  40. package/dist/types/events.d.ts.map +1 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/models.d.ts +10 -0
  44. package/dist/types/models.d.ts.map +1 -1
  45. package/dist/types/segments.d.ts +11 -0
  46. package/dist/types/segments.d.ts.map +1 -1
  47. package/dist/types/tools.d.ts +6 -0
  48. package/dist/types/tools.d.ts.map +1 -1
  49. package/dist/utils/ndjson.d.ts +13 -0
  50. package/dist/utils/ndjson.d.ts.map +1 -0
  51. package/dist/utils/segments.d.ts +11 -3
  52. package/dist/utils/segments.d.ts.map +1 -1
  53. package/package.json +2 -2
  54. 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,18 +894,596 @@ 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
  }
888
901
  },
889
- cancel(reason) {
890
- void events.cancel(reason);
902
+ cancel(reason) {
903
+ void events.cancel(reason);
904
+ }
905
+ });
906
+ return new Response(ndjson, { headers: { "Content-Type": "application/x-ndjson" } });
907
+ };
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();
891
1469
  }
892
1470
  });
893
- return new Response(ndjson, { headers: { "Content-Type": "application/x-ndjson" } });
1471
+ return new Response(stream, { headers: { "Content-Type": "application/x-ndjson" } });
894
1472
  };
895
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
@@ -4745,248 +5238,122 @@ class AparteChatHost {
4745
5238
  host["clearAll"] = (options) => this.clearMessages(options);
4746
5239
  }
4747
5240
  _installLifecycleListeners(host) {
4748
- const onStart = (e) => {
4749
- const d = e?.detail;
4750
- if (d?.messageId) this._setStreamingId(d.messageId);
4751
- };
4752
- const endsThisTurn = (e) => {
4753
- const id = e?.detail?.messageId;
4754
- return !id || this._streamingId === null || id === this._streamingId;
4755
- };
4756
- const onDone = (e) => {
4757
- if (endsThisTurn(e)) {
4758
- this.binding.onTypingChange?.(false);
4759
- this._setStreamingId(null);
4760
- }
4761
- const d = e?.detail;
4762
- if (!d?.messageId || !d.usage) return;
4763
- const next = this.binding.getMessages().map(
4764
- (m) => m.id === d.messageId ? { ...m, usage: d.usage } : m
4765
- );
4766
- this.binding.setMessages(next);
4767
- this.binding.onMessagesChange?.(next);
4768
- this._bubbleById(d.messageId)?.setUsage?.(d.usage);
4769
- };
4770
- const onEnd = (e) => {
4771
- if (!endsThisTurn(e)) return;
4772
- this.binding.onTypingChange?.(false);
4773
- this._setStreamingId(null);
4774
- };
4775
- host.addEventListener("aparte-message-start", onStart);
4776
- host.addEventListener("aparte-message-done", onDone);
4777
- host.addEventListener("aparte-message-error", onEnd);
4778
- host.addEventListener("aparte-message-aborted", onEnd);
4779
- this._teardown.push(() => {
4780
- host.removeEventListener("aparte-message-start", onStart);
4781
- host.removeEventListener("aparte-message-done", onDone);
4782
- host.removeEventListener("aparte-message-error", onEnd);
4783
- host.removeEventListener("aparte-message-aborted", onEnd);
4784
- });
4785
- }
4786
- _installViewportListeners() {
4787
- const vp = this.binding.viewport;
4788
- if (!vp) return;
4789
- const onPathChanged = (e) => {
4790
- const evt = e;
4791
- const incoming = evt.detail.messages;
4792
- this._renderedIds.clear();
4793
- const prev = this.binding.getMessages();
4794
- const merged = incoming.map((m) => {
4795
- if (m.usage !== void 0) return m;
4796
- const old = prev.find((p) => p.id === m.id);
4797
- return old?.usage !== void 0 ? { ...m, usage: old.usage } : m;
4798
- });
4799
- this.binding.setMessages(merged);
4800
- this.binding.onMessagesChange?.(merged);
4801
- if (evt.detail.siblings.some((s) => s.count > 1)) {
4802
- this._pendingSiblings = evt.detail.siblings;
4803
- this.binding.afterRender(() => this._applyPendingSiblings());
4804
- }
4805
- };
4806
- const onBranchNavigate = () => this.syncRepoFromMessages();
4807
- vp.addEventListener("aparte-path-changed", onPathChanged);
4808
- vp.addEventListener("aparte-branch-navigate", onBranchNavigate, { capture: true });
4809
- this._teardown.push(() => {
4810
- vp.removeEventListener("aparte-path-changed", onPathChanged);
4811
- vp.removeEventListener("aparte-branch-navigate", onBranchNavigate, { capture: true });
4812
- });
4813
- }
4814
- }
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 }));
4819
- }
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;
5241
+ const onStart = (e) => {
5242
+ const d = e?.detail;
5243
+ if (d?.messageId) this._setStreamingId(d.messageId);
5244
+ };
5245
+ const endsThisTurn = (e) => {
5246
+ const id = e?.detail?.messageId;
5247
+ return !id || this._streamingId === null || id === this._streamingId;
5248
+ };
5249
+ const onDone = (e) => {
5250
+ if (endsThisTurn(e)) {
5251
+ this.binding.onTypingChange?.(false);
5252
+ this._setStreamingId(null);
4881
5253
  }
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;
5254
+ const d = e?.detail;
5255
+ if (!d?.messageId || !d.usage) return;
5256
+ const next = this.binding.getMessages().map(
5257
+ (m) => m.id === d.messageId ? { ...m, usage: d.usage } : m
5258
+ );
5259
+ this.binding.setMessages(next);
5260
+ this.binding.onMessagesChange?.(next);
5261
+ this._bubbleById(d.messageId)?.setUsage?.(d.usage);
5262
+ };
5263
+ const onEnd = (e) => {
5264
+ if (!endsThisTurn(e)) return;
5265
+ this.binding.onTypingChange?.(false);
5266
+ this._setStreamingId(null);
5267
+ };
5268
+ host.addEventListener("aparte-message-start", onStart);
5269
+ host.addEventListener("aparte-message-done", onDone);
5270
+ host.addEventListener("aparte-message-error", onEnd);
5271
+ host.addEventListener("aparte-message-aborted", onEnd);
5272
+ this._teardown.push(() => {
5273
+ host.removeEventListener("aparte-message-start", onStart);
5274
+ host.removeEventListener("aparte-message-done", onDone);
5275
+ host.removeEventListener("aparte-message-error", onEnd);
5276
+ host.removeEventListener("aparte-message-aborted", onEnd);
5277
+ });
5278
+ }
5279
+ _installViewportListeners() {
5280
+ const vp = this.binding.viewport;
5281
+ if (!vp) return;
5282
+ const onPathChanged = (e) => {
5283
+ const evt = e;
5284
+ const incoming = evt.detail.messages;
5285
+ this._renderedIds.clear();
5286
+ const prev = this.binding.getMessages();
5287
+ const merged = incoming.map((m) => {
5288
+ if (m.usage !== void 0) return m;
5289
+ const old = prev.find((p) => p.id === m.id);
5290
+ return old?.usage !== void 0 ? { ...m, usage: old.usage } : m;
5291
+ });
5292
+ this.binding.setMessages(merged);
5293
+ this.binding.onMessagesChange?.(merged);
5294
+ if (evt.detail.siblings.some((s) => s.count > 1)) {
5295
+ this._pendingSiblings = evt.detail.siblings;
5296
+ this.binding.afterRender(() => this._applyPendingSiblings());
4889
5297
  }
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;
5298
+ };
5299
+ const onBranchNavigate = () => this.syncRepoFromMessages();
5300
+ vp.addEventListener("aparte-path-changed", onPathChanged);
5301
+ vp.addEventListener("aparte-branch-navigate", onBranchNavigate, { capture: true });
5302
+ this._teardown.push(() => {
5303
+ vp.removeEventListener("aparte-path-changed", onPathChanged);
5304
+ vp.removeEventListener("aparte-branch-navigate", onBranchNavigate, { capture: true });
5305
+ });
5306
+ }
5307
+ }
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;
5316
+ }
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;
@@ -5799,15 +6179,64 @@ class AparteClient {
5799
6179
  return;
5800
6180
  }
5801
6181
  dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" }, chatId);
6182
+ if (!opts?.resume) {
6183
+ this._updateMessage(targetElement, messageId, {
6184
+ model: {
6185
+ providerId: provider.id,
6186
+ modelId: baseRequest.modelId,
6187
+ ...baseRequest.temperature !== void 0 ? { temperature: baseRequest.temperature } : {}
6188
+ }
6189
+ });
6190
+ }
5802
6191
  const streamController = new AbortController();
6192
+ if (this._streamController !== streamController) this._streamController?.abort();
6193
+ this._streamController = streamController;
5803
6194
  try {
5804
- const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
6195
+ let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
6196
+ const maxResumes = this.options.maxTurns ?? 10;
6197
+ let resumes = 0;
6198
+ while (result.suspended) {
6199
+ const stopped = this._isAborted || streamController.signal.aborted;
6200
+ if (!stopped && ++resumes > maxResumes) {
6201
+ this._closeOpenCalls(targetElement, messageId);
6202
+ createStreamAdapter({ target: targetElement, config: this._config, messageId, ...chatId ? { targetId: chatId } : {} })(
6203
+ { type: "turn-limit-exceeded", scope: "global", limit: maxResumes }
6204
+ );
6205
+ break;
6206
+ }
6207
+ const outcomes = stopped ? void 0 : await this._answerDeferred(targetElement, result.suspended.pending, streamController.signal);
6208
+ if (!outcomes) {
6209
+ this._closeOpenCalls(targetElement, messageId);
6210
+ if (this._isAborted || streamController.signal.aborted) {
6211
+ dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
6212
+ }
6213
+ break;
6214
+ }
6215
+ const deferral = {
6216
+ outcomes,
6217
+ ...result.suspended.ticket ? { token: result.suspended.ticket } : {}
6218
+ };
6219
+ const messages2 = this._resumeMessages(targetElement, messageId, baseRequest.messages);
6220
+ this._spendPause(targetElement, messageId);
6221
+ result = await this._streamLoop(
6222
+ targetElement,
6223
+ messageId,
6224
+ provider,
6225
+ { ...baseRequest, messages: messages2 },
6226
+ authConfig,
6227
+ streamController,
6228
+ deferral
6229
+ );
6230
+ }
6231
+ const usage = result.usage;
5805
6232
  if (this._isAborted || streamController.signal.aborted) {
6233
+ this._closeOpenCalls(targetElement, messageId);
5806
6234
  this._updateMessage(targetElement, messageId, { status: "completed" });
5807
6235
  } else {
5808
6236
  dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage }, chatId);
5809
6237
  }
5810
6238
  } catch (error) {
6239
+ this._closeOpenCalls(targetElement, messageId);
5811
6240
  if (this._isAborted || streamController.signal.aborted) {
5812
6241
  dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
5813
6242
  this._updateMessage(targetElement, messageId, { status: "completed" });
@@ -6110,10 +6539,13 @@ class AparteClient {
6110
6539
  return messages.filter((m) => {
6111
6540
  if (m.role !== "user" && m.role !== "assistant") return false;
6112
6541
  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);
6542
+ }).flatMap((m) => {
6543
+ if (m.role === "assistant" && m.segments?.some((s) => s.type === "tool_call")) {
6544
+ return wireTurns(m, (segments) => this._segmentsToText(segments));
6545
+ }
6546
+ const content = this._wireText(m);
6547
+ return content ? [{ role: m.role, content }] : [];
6548
+ });
6117
6549
  }
6118
6550
  /** The "no model selected" warning is said once per client, not once per dropped send. */
6119
6551
  _warnedNoModel = false;
@@ -6157,6 +6589,7 @@ class AparteClient {
6157
6589
  console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
6158
6590
  return;
6159
6591
  }
6592
+ this._closeDeferred(targetElement);
6160
6593
  if (this.options.echoUserMessage !== false && event.detail?.echoed !== true) {
6161
6594
  const echoFiles = Array.isArray(event.detail?.files) ? event.detail.files : [];
6162
6595
  targetElement.appendMessage?.({
@@ -6319,8 +6752,8 @@ ${text}` : text;
6319
6752
  *
6320
6753
  * `thinking` and `tool_call` stay out on purpose — the first is the model's own
6321
6754
  * 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.
6755
+ * second is not text: `_wireHistory` sends a reply's calls as the assistant's
6756
+ * `toolCalls` and the `tool` messages answering them (`wire-history.ts`).
6324
6757
  */
6325
6758
  _segmentsToText(segments) {
6326
6759
  if (!segments?.length) return "";
@@ -6424,11 +6857,12 @@ ${content}
6424
6857
  * The turn's agent loop — the engine's `runStreamAgent` rendered through the core
6425
6858
  * adapter, or the runner a host injected — after the fetch-level abort bookkeeping.
6426
6859
  */
6427
- async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController = new AbortController()) {
6428
- this._streamController?.abort();
6860
+ /** One run of the loop: its usage, and whether it paused (a server loop). */
6861
+ async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController = new AbortController(), deferral) {
6862
+ if (this._streamController !== streamController) this._streamController?.abort();
6429
6863
  this._streamController = streamController;
6430
6864
  const runner = this.options.streamRunner ?? runStreamAgent;
6431
- return this._runViaStreamRunner(runner, targetElement, messageId, provider, baseRequest, authConfig, streamController);
6865
+ return this._runViaStreamRunner(runner, targetElement, messageId, provider, baseRequest, authConfig, streamController, deferral);
6432
6866
  }
6433
6867
  /**
6434
6868
  * Run the loop through a {@link AparteStreamRunner} — the engine's by default —
@@ -6439,7 +6873,7 @@ ${content}
6439
6873
  * produces is pinned by the `stream-parity` suite's snapshots, recorded while
6440
6874
  * the inline loop it replaced still ran beside it.
6441
6875
  */
6442
- async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController) {
6876
+ async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController, deferral) {
6443
6877
  const signal = streamController.signal;
6444
6878
  const chatId = this._targetIdOf(targetElement);
6445
6879
  const adapter = createStreamAdapter({
@@ -6449,9 +6883,14 @@ ${content}
6449
6883
  ...chatId ? { targetId: chatId } : {}
6450
6884
  });
6451
6885
  const prefixSegments = baseRequest._meta?.["prefixSegments"];
6886
+ let suspended;
6452
6887
  const emitter = (event) => {
6453
6888
  adapter(event);
6454
- if (event.type === "run-start" && prefixSegments?.length) {
6889
+ if (event.type === "run-suspended") {
6890
+ const ticket = event.ticket;
6891
+ suspended = { pending: event.pending, ...typeof ticket === "string" ? { ticket } : {} };
6892
+ }
6893
+ if (event.type === "run-start" && prefixSegments?.length && !deferral) {
6455
6894
  for (const seg of prefixSegments) targetElement.addSegment?.(seg);
6456
6895
  }
6457
6896
  };
@@ -6477,16 +6916,11 @@ ${content}
6477
6916
  }
6478
6917
  };
6479
6918
  };
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
- });
6919
+ const approvalResolver = this._approvalResolverFor(targetElement);
6488
6920
  const usage = await streamRunner({
6489
6921
  messageId,
6922
+ providerId: provider.id,
6923
+ ...deferral ? { deferral } : {},
6490
6924
  baseRequest,
6491
6925
  transportCall,
6492
6926
  toolLookup,
@@ -6504,8 +6938,199 @@ ${content}
6504
6938
  // BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
6505
6939
  idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
6506
6940
  });
6507
- return usage ?? void 0;
6941
+ return { usage: usage ?? void 0, ...suspended ? { suspended } : {} };
6942
+ }
6943
+ /**
6944
+ * The approval channel a run asks through — and the one a paused server run's calls
6945
+ * are answered through, so the page asks the same way whichever loop paused.
6946
+ *
6947
+ * `gatedElsewhere`: a server already decided this call needs a person. The page's
6948
+ * policy may still answer it when it has an opinion (`auto` mode allows, a plan
6949
+ * refuses), but with none the person is asked. `ruleOnToolCall`'s own fallback —
6950
+ * the tool's `needsApproval` — reads a tool the page does not have, and said
6951
+ * "allow": the page approved, silently, what the server wanted a person to decide.
6952
+ */
6953
+ _approvalResolverFor(targetElement, gatedElsewhere = false) {
6954
+ return this.options.approvalResolver ?? (async (call, sig) => {
6955
+ const ruling = gatedElsewhere ? this._config.getApprovalPolicy()?.(call, this._config.getTools().find((t) => t.name === call.name)) ?? { verdict: "ask" } : this._config.ruleOnToolCall(call);
6956
+ if (ruling.verdict === "allow") return { approved: true };
6957
+ if (ruling.verdict === "deny") {
6958
+ return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy." };
6959
+ }
6960
+ return this._askForApproval(call, sig, targetElement);
6961
+ });
6962
+ }
6963
+ /**
6964
+ * The answers a paused server run waits for, asked HERE because only the page can: an
6965
+ * approval through the same channel a run in the page uses (the panel, the policy, a
6966
+ * host's own resolver), a browser tool through the handler `registerTool` gave it.
6967
+ * A `queued` call needs nothing — the server runs it on resume. `undefined` when
6968
+ * nobody answered — the turn was stopped, or the channel gave up — and there is no
6969
+ * resume. A channel that CRASHES fails the turn, as it does for a run in the page.
6970
+ */
6971
+ async _answerDeferred(targetElement, pending, signal) {
6972
+ const outcomes = {};
6973
+ const resolver = this._approvalResolverFor(targetElement, true);
6974
+ for (const p of pending) {
6975
+ if (signal.aborted) break;
6976
+ const call = { id: p.toolCallId, name: p.name, input: p.input ?? {} };
6977
+ if (p.reason === "approval") {
6978
+ let decision;
6979
+ try {
6980
+ decision = await resolver(call, signal);
6981
+ } catch (error) {
6982
+ if (!signal.aborted && error?.name !== "AbortError") throw error;
6983
+ }
6984
+ if (!decision || signal.aborted) return void 0;
6985
+ outcomes[p.toolCallId] = { kind: "decision", ...decision };
6986
+ } else if (p.reason === "external") {
6987
+ const handler = this._config.getToolHandler(p.name);
6988
+ try {
6989
+ if (!handler) throw new Error(`No browser handler is registered for "${p.name}".`);
6990
+ const result = await handler(call, signal, { target: targetElement, config: this._config });
6991
+ outcomes[p.toolCallId] = {
6992
+ kind: "result",
6993
+ content: result.content,
6994
+ ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
6995
+ };
6996
+ } catch (error) {
6997
+ if (signal.aborted) break;
6998
+ outcomes[p.toolCallId] = { kind: "failed", error: error instanceof Error ? error.message : String(error) };
6999
+ }
7000
+ }
7001
+ }
7002
+ return signal.aborted ? void 0 : outcomes;
7003
+ }
7004
+ /**
7005
+ * The ticket is spent the moment the resume leaves: the server answers these calls
7006
+ * now, so a pause saved from here on — the conversation manager saves before it cuts a
7007
+ * switch — would be resumed a second time, against a server that has moved on. The
7008
+ * history that resumes them is built first; a new pause marks what still waits.
7009
+ */
7010
+ _spendPause(targetElement, messageId) {
7011
+ const segments = targetElement.getMessage?.(messageId)?.segments ?? targetElement.getMessages?.()?.find((m) => m.id === messageId)?.segments;
7012
+ const paused = (segment) => segment.type === "tool_call" && Boolean(segment.deferred);
7013
+ if (!segments?.some(paused)) return;
7014
+ targetElement.updateMessage?.(messageId, {
7015
+ segments: segments.map((segment) => {
7016
+ if (!paused(segment)) return segment;
7017
+ const { deferred: _spent, ...rest } = segment;
7018
+ return rest;
7019
+ })
7020
+ });
7021
+ }
7022
+ /**
7023
+ * Close the reply's calls that can no longer finish — paused, queued, or running on a
7024
+ * server the page stopped listening to: each says it stopped, and forgets the pause.
7025
+ * A cut connection carries none of their last events, so nothing else would: the rows
7026
+ * spun, and a queued call kept its ticket, which a reload then resumed against a
7027
+ * server that had moved on. By the reply's id, not the chat's head — a retry during
7028
+ * the wait moves the head. A run in the page ends every call itself: nothing to do.
7029
+ */
7030
+ _closeOpenCalls(targetElement, messageId) {
7031
+ const segments = targetElement.getMessage?.(messageId)?.segments ?? targetElement.getMessages?.()?.find((m) => m.id === messageId)?.segments;
7032
+ const unfinished = (segment) => segment.type === "tool_call" && (segment.status === "pending" || segment.status === "awaiting-approval");
7033
+ if (!segments?.some(unfinished)) return;
7034
+ targetElement.updateMessage?.(messageId, {
7035
+ segments: segments.map((segment) => {
7036
+ if (!unfinished(segment)) return segment;
7037
+ const { deferred: _spent, ...rest } = segment;
7038
+ return { ...rest, status: "aborted" };
7039
+ })
7040
+ });
7041
+ }
7042
+ /**
7043
+ * The history a resume sends: the request the turn was sent with — interceptor and
7044
+ * `history` option included, not rebuilt from the transcript — then the reply so
7045
+ * far, its paused calls left open for the server to answer.
7046
+ */
7047
+ _resumeMessages(targetElement, messageId, sent) {
7048
+ const reply = targetElement.getMessages?.().find((m) => m.id === messageId);
7049
+ return [...sent, ...reply ? wireTurns(reply, (segments) => this._segmentsToText(segments), { openDeferred: true }) : []];
7050
+ }
7051
+ /**
7052
+ * What the send of a reloaded turn put before its reply, rebuilt the way
7053
+ * `_buildMessages` builds it: the system prompts, the history `options.history`
7054
+ * asks for — given the transcript before the reply, where the send also saw the
7055
+ * reply's empty shell; handing over the paused reply would send it twice — and the
7056
+ * message that asked.
7057
+ * That message goes as its text: the files a send read are not in a transcript.
7058
+ */
7059
+ _resumeBase(all, replyIndex) {
7060
+ const before = all.slice(0, replyIndex);
7061
+ const askedAt = before.map((m) => m.role).lastIndexOf("user");
7062
+ const historyOption = this.options.history ?? "viewport";
7063
+ const history = historyOption === "viewport" ? this._toHistoryMessages(before) : typeof historyOption === "function" ? historyOption(before) : [];
7064
+ return [...this._systemMessages(), ...history, ...askedAt >= 0 ? this._wireHistory([before[askedAt]]) : []];
7065
+ }
7066
+ /**
7067
+ * A new message closes a wait nobody answered. The paused calls of the last reply are
7068
+ * marked stopped, so the history the new turn sends answers them ("stopped before it
7069
+ * ran") — a provider rejects a request that declares a call with no answer.
7070
+ */
7071
+ _closeDeferred(targetElement) {
7072
+ const messages = targetElement.getMessages?.() ?? [];
7073
+ const last = [...messages].reverse().find((m) => m.role === "assistant");
7074
+ if (!last?.segments?.some(isOpenDeferred)) return;
7075
+ const segments = last.segments.map((segment) => {
7076
+ if (!isOpenDeferred(segment)) return segment;
7077
+ const { deferred: _closed, ...rest } = segment;
7078
+ return { ...rest, status: "aborted" };
7079
+ });
7080
+ targetElement.updateMessage?.(last.id, { segments });
7081
+ }
7082
+ /**
7083
+ * Answer a turn a server loop paused, when the conversation comes back — a reload, a
7084
+ * conversation reopened days later. The last reply's paused calls (they carry the
7085
+ * server's ticket) are put to the person again, or run in the page, and the loop
7086
+ * resumes on the server, into the same bubble.
7087
+ *
7088
+ * Called on its own when a conversation manager loads a conversation
7089
+ * (`aparte-conversation-loaded`); call it yourself after `setMessages` if you load
7090
+ * conversations another way. Resolves `true` when a resume ran, `false` when there
7091
+ * was nothing to resume.
7092
+ *
7093
+ * @param target The chat — an element, or its id. Defaults to the one `targetResolver` names, else the first on the page.
7094
+ */
7095
+ async resumeDeferred(target) {
7096
+ const host = typeof target === "string" ? this._byId(target) : target ?? this.options.targetResolver?.() ?? null;
7097
+ const targetElement = host ? this._asRenderTarget(host) : this._scanForTarget();
7098
+ if (!targetElement) return false;
7099
+ const messages = targetElement.getMessages?.() ?? [];
7100
+ const last = [...messages].reverse().find((m) => m.role === "assistant");
7101
+ const open = (last?.segments ?? []).filter(isOpenDeferred);
7102
+ const ticket = open.find((s) => s.deferred?.ticket)?.deferred?.ticket;
7103
+ if (!last || open.length === 0 || !ticket || this._resuming.has(last.id)) return false;
7104
+ const config = this._config.getModelConfig();
7105
+ const recorded = last.model && this._config.getAIProvider(last.model.providerId) ? last.model : void 0;
7106
+ const providerId = recorded?.providerId ?? config.defaultProvider;
7107
+ const provider = providerId ? this._config.getAIProvider(providerId) : void 0;
7108
+ if (!provider) {
7109
+ console.warn("[AparteClient] Cannot resume the paused turn: no provider is selected.");
7110
+ return false;
7111
+ }
7112
+ this._beginUserTurn();
7113
+ this._resuming.add(last.id);
7114
+ try {
7115
+ const authConfig = await this._resolveAuth(provider.id);
7116
+ const pending = open.map((s) => ({
7117
+ toolCallId: s.toolCall.id,
7118
+ name: s.toolCall.name,
7119
+ input: s.toolCall.input,
7120
+ reason: s.deferred.reason
7121
+ }));
7122
+ const base = this._resumeBase(messages, messages.indexOf(last));
7123
+ await this._streamTurn(targetElement, last.id, provider, base, recorded?.modelId ?? config.defaultModel ?? "", authConfig, {
7124
+ resume: { pending, ticket },
7125
+ ...recorded?.temperature !== void 0 ? { temperature: recorded.temperature } : {}
7126
+ });
7127
+ } finally {
7128
+ this._resuming.delete(last.id);
7129
+ }
7130
+ return true;
6508
7131
  }
7132
+ /** Replies being resumed, so a second load of the same conversation does not ask twice. */
7133
+ _resuming = /* @__PURE__ */ new Set();
6509
7134
  /**
6510
7135
  * Updates an existing message or appends a new error if ID lookup fails
6511
7136
  */
@@ -6542,6 +7167,54 @@ ${content}
6542
7167
  dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error }, this._targetIdOf(target));
6543
7168
  }
6544
7169
  }
7170
+ function createAgentStreamRunner(options) {
7171
+ return async (opts) => {
7172
+ const doFetch = options.fetch ?? fetch;
7173
+ const headers = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
7174
+ const { tools, ...request } = opts.baseRequest;
7175
+ const body = {
7176
+ v: 1,
7177
+ providerId: opts.providerId,
7178
+ messageId: opts.messageId,
7179
+ request,
7180
+ clientTools: tools ?? [],
7181
+ ...opts.deferral?.token ? { resume: { ticket: opts.deferral.token, outcomes: opts.deferral.outcomes ?? {} } } : {}
7182
+ };
7183
+ const response = await doFetch(options.endpoint, {
7184
+ method: "POST",
7185
+ headers: { "Content-Type": "application/json", ...headers },
7186
+ body: JSON.stringify(body),
7187
+ signal: opts.signal,
7188
+ ...options.credentials ? { credentials: options.credentials } : {}
7189
+ });
7190
+ if (!response.ok || !response.body) {
7191
+ const message = await response.json().then((json) => json?.error?.message).catch(() => void 0);
7192
+ throw new AparteError(
7193
+ message || `The agent endpoint answered HTTP ${response.status}.`,
7194
+ AparteError.codeForStatus(response.status) ?? AparteErrorCode.PROVIDER_ERROR,
7195
+ void 0,
7196
+ void 0,
7197
+ response.status
7198
+ );
7199
+ }
7200
+ let usage;
7201
+ let ended = false;
7202
+ for await (const line of readNdjson(response.body, opts.signal)) {
7203
+ if (line.type === "run-error") {
7204
+ throw new AparteError(line.message || "The agent run failed.", AparteErrorCode.PROVIDER_ERROR, line.code ? { code: line.code } : void 0);
7205
+ }
7206
+ if (line.type === "run-done" || line.type === "run-suspended") {
7207
+ usage = line.usage;
7208
+ ended = true;
7209
+ }
7210
+ opts.emitter(line);
7211
+ }
7212
+ if (!ended && !opts.signal.aborted) {
7213
+ throw new AparteError("The agent stream ended unexpectedly.", AparteErrorCode.PROVIDER_ERROR);
7214
+ }
7215
+ return usage;
7216
+ };
7217
+ }
6545
7218
  function findHead(node) {
6546
7219
  if (node.next) return findHead(node.next);
6547
7220
  if ("current" in node) return node;
@@ -6848,6 +7521,8 @@ const APARTE_DEFAULT_UI_EVENTS = [
6848
7521
  "aparte-conversation-rename",
6849
7522
  "aparte-conversation-pin",
6850
7523
  "aparte-conversation-unpin",
7524
+ // The conversation controller, on the chat host: a conversation's messages are on screen.
7525
+ "aparte-conversation-loaded",
6851
7526
  // the select primitives
6852
7527
  "aparte-select-change",
6853
7528
  "aparte-select-open",
@@ -6864,6 +7539,7 @@ const APARTE_DEFAULT_UI_EVENTS = [
6864
7539
  "aparte-message-done",
6865
7540
  "aparte-message-error",
6866
7541
  "aparte-message-aborted",
7542
+ "aparte-message-paused",
6867
7543
  "aparte-tool-approval-request"
6868
7544
  ];
6869
7545
  function applyElementProps(el2, props, transformValue = (value) => value) {
@@ -6939,56 +7615,58 @@ function installDialogTriggersOnce(doc = document) {
6939
7615
  doc.addEventListener("click", onClick);
6940
7616
  }
6941
7617
  export {
6942
- resolveConfig as $,
7618
+ registerSegmentRenderer as $,
6943
7619
  APARTE_CONFIG_CHANGE as A,
6944
7620
  contextConfig as B,
6945
7621
  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 _,
7622
+ createAgentStreamRunner as D,
7623
+ createAparteAgentHandler as E,
7624
+ createAparteChatHandler as F,
7625
+ createStreamAdapter as G,
7626
+ cssEscape as H,
7627
+ declineDefaultRenderers as I,
7628
+ defaultSanitizer as J,
7629
+ detachConfig as K,
7630
+ escapeAttr as L,
7631
+ escapeHtml as M,
7632
+ filesToAttachments as N,
7633
+ getAllRenderers as O,
7634
+ getSegmentRenderer as P,
7635
+ installDefaultRenderersOnce as Q,
7636
+ installDialogTriggersOnce as R,
7637
+ isAwaitingReply as S,
7638
+ isFormatAdapter as T,
7639
+ isSafeUrl as U,
7640
+ isSegmentSettled as V,
7641
+ parseAparteEventStream as W,
7642
+ parseMarkdownToSegments as X,
7643
+ populateBubbleFromMessage as Y,
7644
+ readableToAsyncIterable as Z,
7645
+ registerDefaultRenderers as _,
6970
7646
  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,
7647
+ requestUserInput as a0,
7648
+ resolveConfig as a1,
7649
+ revokeAttachmentUrls as a2,
7650
+ runWithConfig as a3,
7651
+ segmentDuration as a4,
7652
+ segmentTiming as a5,
7653
+ subscribeConfigChange as a6,
7654
+ unregisterSegmentRenderer as a7,
7655
+ uuid as a8,
7656
+ eventOrigin as a9,
7657
+ activeElementIn as aa,
7658
+ mergeSegmentUpdate as ab,
7659
+ writeStreamedMarkdown as ac,
7660
+ stampSegmentActivity as ad,
7661
+ stampSegmentOnInsert as ae,
7662
+ stampSegmentOnUpdate as af,
7663
+ renumberSegments as ag,
7664
+ openSegmentIds as ah,
7665
+ isTerminalStatus as ai,
7666
+ adoptMessageSegments as aj,
7667
+ APARTE_DERIVED_SEGMENTS as ak,
7668
+ chatBoundaryOf as al,
7669
+ rootOf as am,
6992
7670
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
6993
7671
  APARTE_DEFAULT_HOST_HANDLERS as c,
6994
7672
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -7015,4 +7693,4 @@ export {
7015
7693
  collectRendererStyles as y,
7016
7694
  contentToText as z
7017
7695
  };
7018
- //# sourceMappingURL=dialog-triggers-DwTGKWeU.js.map
7696
+ //# sourceMappingURL=dialog-triggers-xA3T--rt.js.map