@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.
- package/dist/client/agent-runner.d.ts +27 -0
- package/dist/client/agent-runner.d.ts.map +1 -0
- package/dist/client/aparte-client.d.ts +77 -2
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/wire-history.d.ts +33 -0
- package/dist/client/wire-history.d.ts.map +1 -0
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +15 -0
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts +1 -0
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/index.d.ts +1 -1
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/types.d.ts +14 -0
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/custom-elements.json +10883 -10064
- package/dist/{dialog-triggers-DwTGKWeU.js → dialog-triggers-xA3T--rt.js} +1132 -454
- package/dist/dialog-triggers-xA3T--rt.js.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -45
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +5 -4
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +35 -33
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +1 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/parsers/aparte-event-stream.d.ts.map +1 -1
- package/dist/transport/agent-handler.d.ts +104 -0
- package/dist/transport/agent-handler.d.ts.map +1 -0
- package/dist/transport/agent-ticket.d.ts +48 -0
- package/dist/transport/agent-ticket.d.ts.map +1 -0
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/types/event-map.d.ts +4 -1
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +16 -0
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/models.d.ts +10 -0
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +11 -0
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +6 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/ndjson.d.ts +13 -0
- package/dist/utils/ndjson.d.ts.map +1 -0
- package/dist/utils/segments.d.ts +11 -3
- package/dist/utils/segments.d.ts.map +1 -1
- package/package.json +2 -2
- 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
|
|
728
|
+
async function* readNdjson(stream, signal) {
|
|
729
|
+
const reader = stream.getReader();
|
|
729
730
|
const decoder = new TextDecoder();
|
|
730
731
|
let buffer = "";
|
|
731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
if (done)
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
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
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
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
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
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
|
-
|
|
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
|
-
}).
|
|
6114
|
-
role
|
|
6115
|
-
|
|
6116
|
-
|
|
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
|
|
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
|
-
|
|
6428
|
-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
7618
|
+
registerSegmentRenderer as $,
|
|
6943
7619
|
APARTE_CONFIG_CHANGE as A,
|
|
6944
7620
|
contextConfig as B,
|
|
6945
7621
|
copyText as C,
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
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
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
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-
|
|
7696
|
+
//# sourceMappingURL=dialog-triggers-xA3T--rt.js.map
|