@cjhyy/code-shell-web 0.9.8 → 0.9.10
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/hooks/remoteAppSync.d.ts +10 -0
- package/dist/hooks/remoteAppSync.js +14 -0
- package/dist/hooks/useRemoteApp.d.ts +6 -1
- package/dist/hooks/useRemoteApp.js +198 -29
- package/dist/i18n/mobile.d.ts +4 -0
- package/dist/i18n/mobile.js +4 -0
- package/dist/i18n/translate.d.ts +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/lib/streamReducer.d.ts +4 -1
- package/dist/lib/streamReducer.js +61 -15
- package/dist/lib/transcriptReplay.d.ts +18 -0
- package/dist/lib/transcriptReplay.js +291 -0
- package/dist-app/assets/index-CPteW9-v.js +50 -0
- package/dist-app/assets/index-Dz4kCU3p.css +1 -0
- package/dist-app/icon.svg +1 -0
- package/dist-app/index.html +6 -3
- package/dist-app/manifest.webmanifest +15 -0
- package/dist-app/offline.html +51 -0
- package/dist-app/sw.js +48 -0
- package/package.json +5 -2
- package/dist-app/assets/index-BGyEiCap.js +0 -11
- package/dist-app/assets/index-xwzva9ZI.css +0 -1
|
@@ -96,16 +96,6 @@ export function reduceStream(state, raw) {
|
|
|
96
96
|
return [`${prefix}-${next}`, { ...s, seq: next }];
|
|
97
97
|
};
|
|
98
98
|
switch (type) {
|
|
99
|
-
case "session_user_message": {
|
|
100
|
-
const [id, s2] = freshId("u");
|
|
101
|
-
return {
|
|
102
|
-
...s2,
|
|
103
|
-
items: [
|
|
104
|
-
...s2.items,
|
|
105
|
-
{ kind: "user", id, text: typeof event.text === "string" ? event.text : "" },
|
|
106
|
-
],
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
99
|
case "stream_request_start": {
|
|
110
100
|
// Open a fresh assistant message (for THIS agent) to accumulate text into.
|
|
111
101
|
const key = agentKey(event);
|
|
@@ -117,6 +107,7 @@ export function reduceStream(state, raw) {
|
|
|
117
107
|
reasoning: "",
|
|
118
108
|
done: false,
|
|
119
109
|
agentId: event.agentId,
|
|
110
|
+
...(typeof event.messageId === "string" ? { messageId: event.messageId } : {}),
|
|
120
111
|
};
|
|
121
112
|
return {
|
|
122
113
|
...s2,
|
|
@@ -336,6 +327,21 @@ export function reduceStream(state, raw) {
|
|
|
336
327
|
items[idx] = { ...items[idx], summary };
|
|
337
328
|
return { ...s, items };
|
|
338
329
|
}
|
|
330
|
+
case "tombstone": {
|
|
331
|
+
if (typeof event.messageId !== "string")
|
|
332
|
+
return s;
|
|
333
|
+
const removed = new Set(s.items
|
|
334
|
+
.filter((item) => (item.kind === "assistant" && item.messageId === event.messageId) ||
|
|
335
|
+
(item.kind === "tool" && item.id === event.messageId))
|
|
336
|
+
.map((item) => item.id));
|
|
337
|
+
if (!removed.size)
|
|
338
|
+
return s;
|
|
339
|
+
return {
|
|
340
|
+
...s,
|
|
341
|
+
items: s.items.filter((item) => !removed.has(item.id)),
|
|
342
|
+
liveByAgent: Object.fromEntries(Object.entries(s.liveByAgent).filter(([, id]) => !removed.has(id))),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
339
345
|
case "assistant_message": {
|
|
340
346
|
// Final assistant message for the request — seal THIS agent's live bubble
|
|
341
347
|
// AND mark its open assistant items done. Without the latter, a finalized
|
|
@@ -464,7 +470,7 @@ export function reduceStream(state, raw) {
|
|
|
464
470
|
id: `sub-${agentId}`,
|
|
465
471
|
agentId,
|
|
466
472
|
label: event.name || event.description || "子代理",
|
|
467
|
-
status: "running",
|
|
473
|
+
status: event.status === "recorded" ? "recorded" : "running",
|
|
468
474
|
};
|
|
469
475
|
if (s.items.some((i) => i.kind === "subagent" && i.agentId === agentId))
|
|
470
476
|
return s;
|
|
@@ -494,16 +500,49 @@ export function reduceStream(state, raw) {
|
|
|
494
500
|
case "session_title": {
|
|
495
501
|
return { ...s, title: event.title };
|
|
496
502
|
}
|
|
503
|
+
case "session_user_message":
|
|
497
504
|
case "user_message": {
|
|
498
505
|
// History replay surfaces past user turns as a synthetic event so the
|
|
499
506
|
// same reducer rebuilds the full conversation (the live path uses
|
|
500
507
|
// appendUserMessage instead, since the phone echoes locally).
|
|
501
|
-
const
|
|
508
|
+
const clientMessageId = typeof event.clientMessageId === "string" ? event.clientMessageId : undefined;
|
|
502
509
|
const attachments = Array.isArray(event.attachments)
|
|
503
510
|
? event.attachments
|
|
504
511
|
.map((item) => item)
|
|
505
|
-
.filter((item) =>
|
|
512
|
+
.filter((item) => !!item &&
|
|
513
|
+
typeof item.name === "string" &&
|
|
514
|
+
typeof item.size === "number" &&
|
|
515
|
+
Number.isFinite(item.size) &&
|
|
516
|
+
item.size >= 0)
|
|
517
|
+
.map((item) => ({
|
|
518
|
+
name: item.name,
|
|
519
|
+
size: item.size,
|
|
520
|
+
...(typeof item.mime === "string" ? { mime: item.mime } : {}),
|
|
521
|
+
...(typeof item.path === "string" ? { path: item.path } : {}),
|
|
522
|
+
}))
|
|
506
523
|
: undefined;
|
|
524
|
+
if (clientMessageId &&
|
|
525
|
+
s.items.some((item) => item.kind === "user" && item.clientMessageId === clientMessageId)) {
|
|
526
|
+
// The local echo has original filenames but no authenticated workspace
|
|
527
|
+
// paths yet. Reconcile the host's staged attachment metadata in place.
|
|
528
|
+
if (!attachments?.length)
|
|
529
|
+
return s;
|
|
530
|
+
return {
|
|
531
|
+
...s,
|
|
532
|
+
items: s.items.map((item) => item.kind === "user" && item.clientMessageId === clientMessageId
|
|
533
|
+
? {
|
|
534
|
+
...item,
|
|
535
|
+
attachments: attachments.map((attachment, index) => {
|
|
536
|
+
const previous = item.attachments?.[index];
|
|
537
|
+
return previous?.name === attachment.name
|
|
538
|
+
? { ...previous, ...attachment }
|
|
539
|
+
: attachment;
|
|
540
|
+
}),
|
|
541
|
+
}
|
|
542
|
+
: item),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const [id, s2] = freshId("u");
|
|
507
546
|
return {
|
|
508
547
|
...s2,
|
|
509
548
|
items: [
|
|
@@ -513,6 +552,7 @@ export function reduceStream(state, raw) {
|
|
|
513
552
|
id,
|
|
514
553
|
text: event.text ?? "",
|
|
515
554
|
...(attachments?.length ? { attachments } : {}),
|
|
555
|
+
...(clientMessageId ? { clientMessageId } : {}),
|
|
516
556
|
},
|
|
517
557
|
],
|
|
518
558
|
};
|
|
@@ -551,14 +591,20 @@ export function reduceStream(state, raw) {
|
|
|
551
591
|
}
|
|
552
592
|
}
|
|
553
593
|
/** Append a local user echo (the phone shows its own message immediately). */
|
|
554
|
-
export function appendUserMessage(state, text, attachments) {
|
|
594
|
+
export function appendUserMessage(state, text, attachments, clientMessageId) {
|
|
555
595
|
const id = `u-${state.seq + 1}`;
|
|
556
596
|
return {
|
|
557
597
|
...state,
|
|
558
598
|
seq: state.seq + 1,
|
|
559
599
|
items: [
|
|
560
600
|
...state.items,
|
|
561
|
-
{
|
|
601
|
+
{
|
|
602
|
+
kind: "user",
|
|
603
|
+
id,
|
|
604
|
+
text,
|
|
605
|
+
...(attachments?.length ? { attachments } : {}),
|
|
606
|
+
...(clientMessageId ? { clientMessageId } : {}),
|
|
607
|
+
},
|
|
562
608
|
],
|
|
563
609
|
};
|
|
564
610
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe projection of Core's durable TranscriptEvent log into the same
|
|
3
|
+
* stream events used by the live reducer. Keep Node/Electron readers outside
|
|
4
|
+
* this module: the Hub, phone and other browser hosts can share this boundary.
|
|
5
|
+
*/
|
|
6
|
+
import { type ChatState, type UserAttachmentSummary } from "./streamReducer.js";
|
|
7
|
+
type ObjectValue = Record<string, unknown>;
|
|
8
|
+
/** Hide engine attachment plumbing while retaining attachment-only messages. */
|
|
9
|
+
export declare function transcriptUserDisplay(data: ObjectValue): {
|
|
10
|
+
text: string;
|
|
11
|
+
attachments: UserAttachmentSummary[];
|
|
12
|
+
};
|
|
13
|
+
/** Durable logs contain both assistant tool_use blocks and dedicated tool_use
|
|
14
|
+
* events. Their tool IDs are identical: emit idempotent starts, never two cards.
|
|
15
|
+
*/
|
|
16
|
+
export declare function transcriptToStreamEvents(records: readonly unknown[]): ObjectValue[];
|
|
17
|
+
export declare function replayTranscript(records: readonly unknown[]): ChatState;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe projection of Core's durable TranscriptEvent log into the same
|
|
3
|
+
* stream events used by the live reducer. Keep Node/Electron readers outside
|
|
4
|
+
* this module: the Hub, phone and other browser hosts can share this boundary.
|
|
5
|
+
*/
|
|
6
|
+
import { initialChatState, reduceStream, } from "./streamReducer.js";
|
|
7
|
+
function object(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
function text(value) {
|
|
13
|
+
return typeof value === "string" ? value : "";
|
|
14
|
+
}
|
|
15
|
+
function contentBlocks(value) {
|
|
16
|
+
return Array.isArray(value)
|
|
17
|
+
? value.map(object).filter((block) => block !== undefined)
|
|
18
|
+
: [];
|
|
19
|
+
}
|
|
20
|
+
function textContent(value) {
|
|
21
|
+
if (typeof value === "string")
|
|
22
|
+
return value;
|
|
23
|
+
return contentBlocks(value)
|
|
24
|
+
.filter((block) => block.type === "text")
|
|
25
|
+
.map((block) => text(block.text))
|
|
26
|
+
.join("");
|
|
27
|
+
}
|
|
28
|
+
function unescapeAttribute(value) {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/&/g, "&");
|
|
34
|
+
}
|
|
35
|
+
function attachmentName(path) {
|
|
36
|
+
const name = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "附件";
|
|
37
|
+
// The canonical staging service prefixes a content hash; it is not part of
|
|
38
|
+
// the filename the user selected. Other paths keep their original basename.
|
|
39
|
+
return path.includes(".code-shell/attachments/") ? name.replace(/^[a-f0-9]{16}-/, "") : name;
|
|
40
|
+
}
|
|
41
|
+
function byteSize(value) {
|
|
42
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
43
|
+
return Number.isSafeInteger(number) && number >= 0 ? number : 0;
|
|
44
|
+
}
|
|
45
|
+
/** Hide engine attachment plumbing while retaining attachment-only messages. */
|
|
46
|
+
export function transcriptUserDisplay(data) {
|
|
47
|
+
const attachments = [];
|
|
48
|
+
const imagePaths = [];
|
|
49
|
+
let raw = textContent(data.content);
|
|
50
|
+
raw = raw.replace(/<attached-file\s+path="([^"]*)">\s*([\s\S]*?)<\/attached-file>/g, (match, path, metadata) => {
|
|
51
|
+
if (!/^absolutePath:\s*.+$/m.test(metadata) || !/^origin:\s*.+$/m.test(metadata)) {
|
|
52
|
+
return match;
|
|
53
|
+
}
|
|
54
|
+
const mime = metadata.match(/^mime:\s*(.+)$/m)?.[1]?.trim();
|
|
55
|
+
attachments.push({
|
|
56
|
+
name: attachmentName(unescapeAttribute(path)),
|
|
57
|
+
path: unescapeAttribute(path),
|
|
58
|
+
size: byteSize(metadata.match(/^size:\s*(\d+)\s*$/m)?.[1]),
|
|
59
|
+
...(mime ? { mime } : {}),
|
|
60
|
+
});
|
|
61
|
+
return "";
|
|
62
|
+
});
|
|
63
|
+
raw = raw.replace(/<attached-directory\s+path="([^"]*)"[^>]*>[\s\S]*?<\/attached-directory>/g, (_match, path) => {
|
|
64
|
+
attachments.push({
|
|
65
|
+
name: `${attachmentName(unescapeAttribute(path))}/`,
|
|
66
|
+
path: unescapeAttribute(path),
|
|
67
|
+
size: 0,
|
|
68
|
+
});
|
|
69
|
+
return "";
|
|
70
|
+
});
|
|
71
|
+
raw = raw.replace(/<attached-image-paths>\s*([\s\S]*?)<\/attached-image-paths>\s*(?:\(上面附带的图片在工作区的真实路径[^\n]*\))?/g, (_match, paths) => {
|
|
72
|
+
imagePaths.push(...paths
|
|
73
|
+
.split("\n")
|
|
74
|
+
.map((path) => path.trim())
|
|
75
|
+
.filter(Boolean));
|
|
76
|
+
return "";
|
|
77
|
+
});
|
|
78
|
+
const imageBlocks = contentBlocks(data.content).filter((block) => block.type === "image");
|
|
79
|
+
for (let index = 0; index < Math.max(imageBlocks.length, imagePaths.length); index++) {
|
|
80
|
+
const source = object(imageBlocks[index]?.source);
|
|
81
|
+
const encoded = text(source?.data).replace(/\s/g, "");
|
|
82
|
+
attachments.push({
|
|
83
|
+
name: imagePaths[index] ? attachmentName(imagePaths[index]) : `图片 ${index + 1}`,
|
|
84
|
+
...(imagePaths[index] ? { path: imagePaths[index] } : {}),
|
|
85
|
+
size: encoded
|
|
86
|
+
? Math.max(0, Math.floor((encoded.length * 3) / 4) - (encoded.match(/=+$/)?.[0].length ?? 0))
|
|
87
|
+
: 0,
|
|
88
|
+
...(typeof source?.media_type === "string" ? { mime: source.media_type } : {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const explicit = Array.isArray(data.attachments)
|
|
92
|
+
? data.attachments.flatMap((item) => {
|
|
93
|
+
const attachment = object(item);
|
|
94
|
+
const name = text(attachment?.name) || text(attachment?.originalName);
|
|
95
|
+
return name
|
|
96
|
+
? [
|
|
97
|
+
{
|
|
98
|
+
name,
|
|
99
|
+
size: byteSize(attachment?.size),
|
|
100
|
+
...(typeof attachment?.mime === "string" ? { mime: attachment.mime } : {}),
|
|
101
|
+
...(typeof attachment?.path === "string" ? { path: attachment.path } : {}),
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
: [];
|
|
105
|
+
})
|
|
106
|
+
: [];
|
|
107
|
+
return {
|
|
108
|
+
text: typeof data.displayText === "string" ? data.displayText : raw.trim(),
|
|
109
|
+
attachments: explicit.length ? explicit : attachments,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function eventData(event) {
|
|
113
|
+
return object(event.data) ?? object(event.message) ?? event;
|
|
114
|
+
}
|
|
115
|
+
/** Durable logs contain both assistant tool_use blocks and dedicated tool_use
|
|
116
|
+
* events. Their tool IDs are identical: emit idempotent starts, never two cards.
|
|
117
|
+
*/
|
|
118
|
+
export function transcriptToStreamEvents(records) {
|
|
119
|
+
const result = [];
|
|
120
|
+
const seenIds = new Set();
|
|
121
|
+
const starts = new Set();
|
|
122
|
+
const knownTools = new Map();
|
|
123
|
+
for (const raw of records) {
|
|
124
|
+
const record = object(raw);
|
|
125
|
+
if (!record)
|
|
126
|
+
continue;
|
|
127
|
+
const data = eventData(record);
|
|
128
|
+
if (record.type === "tool_use" && text(data.toolCallId)) {
|
|
129
|
+
knownTools.set(text(data.toolCallId), {
|
|
130
|
+
name: text(data.toolName) || "工具",
|
|
131
|
+
args: object(data.args) ?? {},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
for (const block of contentBlocks(data.content)) {
|
|
135
|
+
if (block.type === "tool_use" && text(block.id)) {
|
|
136
|
+
knownTools.set(text(block.id), {
|
|
137
|
+
name: text(block.name) || "工具",
|
|
138
|
+
args: object(block.input) ?? {},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const startTool = (id, name, args, agentId) => {
|
|
144
|
+
if (!id)
|
|
145
|
+
return;
|
|
146
|
+
if (!starts.has(id)) {
|
|
147
|
+
starts.add(id);
|
|
148
|
+
result.push({
|
|
149
|
+
type: "tool_use_start",
|
|
150
|
+
toolCall: { id, toolName: name || "工具", args },
|
|
151
|
+
...(agentId ? { agentId } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
result.push({
|
|
156
|
+
type: "tool_use_args_delta",
|
|
157
|
+
toolCallId: id,
|
|
158
|
+
args,
|
|
159
|
+
...(agentId ? { agentId } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const finishTool = (id, data, agentId) => {
|
|
164
|
+
if (!id)
|
|
165
|
+
return;
|
|
166
|
+
const known = knownTools.get(id);
|
|
167
|
+
if (!starts.has(id))
|
|
168
|
+
startTool(id, known?.name || text(data.toolName) || "工具", known?.args ?? {}, agentId);
|
|
169
|
+
const output = typeof data.result === "string"
|
|
170
|
+
? data.result
|
|
171
|
+
: textContent(data.content ?? data.contentBlocks);
|
|
172
|
+
const error = text(data.error);
|
|
173
|
+
result.push({
|
|
174
|
+
type: "tool_result",
|
|
175
|
+
result: {
|
|
176
|
+
id,
|
|
177
|
+
result: output || error,
|
|
178
|
+
...(error ? { error } : {}),
|
|
179
|
+
isError: data.isError === true || data.is_error === true || !!error,
|
|
180
|
+
},
|
|
181
|
+
...(agentId ? { agentId } : {}),
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
for (const raw of records) {
|
|
185
|
+
const record = object(raw);
|
|
186
|
+
if (!record)
|
|
187
|
+
continue;
|
|
188
|
+
const id = text(record.id);
|
|
189
|
+
if (id && seenIds.has(id))
|
|
190
|
+
continue;
|
|
191
|
+
if (id)
|
|
192
|
+
seenIds.add(id);
|
|
193
|
+
const data = eventData(record);
|
|
194
|
+
const agentId = text(data.agentId) || undefined;
|
|
195
|
+
const type = typeof record.type === "string"
|
|
196
|
+
? record.type
|
|
197
|
+
: typeof data.role === "string"
|
|
198
|
+
? "message"
|
|
199
|
+
: "";
|
|
200
|
+
switch (type) {
|
|
201
|
+
case "message": {
|
|
202
|
+
const blocks = contentBlocks(data.content);
|
|
203
|
+
if (data.role === "user") {
|
|
204
|
+
if (data.injected === true ||
|
|
205
|
+
data.authority === "agent" ||
|
|
206
|
+
data.authority === "system" ||
|
|
207
|
+
data.authority === "policy")
|
|
208
|
+
break;
|
|
209
|
+
const display = transcriptUserDisplay(data);
|
|
210
|
+
if (display.text.startsWith("<system-reminder>"))
|
|
211
|
+
break;
|
|
212
|
+
if (display.text || display.attachments.length) {
|
|
213
|
+
result.push({
|
|
214
|
+
type: "user_message",
|
|
215
|
+
...display,
|
|
216
|
+
...(typeof data.clientMessageId === "string"
|
|
217
|
+
? { clientMessageId: data.clientMessageId }
|
|
218
|
+
: {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
for (const block of blocks) {
|
|
222
|
+
if (block.type === "tool_result")
|
|
223
|
+
finishTool(text(block.tool_use_id), block, agentId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (data.role === "assistant") {
|
|
227
|
+
const body = textContent(data.content);
|
|
228
|
+
const reasoning = blocks
|
|
229
|
+
.filter((block) => block.type === "reasoning")
|
|
230
|
+
.map((block) => text(block.reasoningContent) || text(block.text))
|
|
231
|
+
.join("");
|
|
232
|
+
if (body || reasoning) {
|
|
233
|
+
const scope = agentId ? { agentId } : {};
|
|
234
|
+
result.push({ type: "stream_request_start", ...scope });
|
|
235
|
+
if (reasoning)
|
|
236
|
+
result.push({ type: "thinking_delta", text: reasoning, ...scope });
|
|
237
|
+
if (body)
|
|
238
|
+
result.push({ type: "text_delta", text: body, ...scope });
|
|
239
|
+
result.push({ type: "assistant_message", ...scope });
|
|
240
|
+
}
|
|
241
|
+
for (const block of blocks) {
|
|
242
|
+
if (block.type === "tool_use")
|
|
243
|
+
startTool(text(block.id), text(block.name), object(block.input) ?? {}, agentId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (data.role === "tool") {
|
|
247
|
+
finishTool(text(data.tool_call_id), data, agentId);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "tool_use":
|
|
252
|
+
startTool(text(data.toolCallId), text(data.toolName), object(data.args) ?? {}, agentId);
|
|
253
|
+
break;
|
|
254
|
+
case "tool_result":
|
|
255
|
+
finishTool(text(data.toolCallId), data, agentId);
|
|
256
|
+
break;
|
|
257
|
+
case "subagent":
|
|
258
|
+
if (agentId)
|
|
259
|
+
result.push({
|
|
260
|
+
type: "agent_start",
|
|
261
|
+
agentId,
|
|
262
|
+
name: text(data.name),
|
|
263
|
+
description: text(data.description),
|
|
264
|
+
status: "recorded",
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
case "error":
|
|
268
|
+
if (text(data.error))
|
|
269
|
+
result.push({ type: "error", error: data.error });
|
|
270
|
+
break;
|
|
271
|
+
case "turn_stopped":
|
|
272
|
+
result.push({ type: "turn_complete", reason: "aborted_tools" });
|
|
273
|
+
break;
|
|
274
|
+
case "session_meta":
|
|
275
|
+
if (text(data.sessionId))
|
|
276
|
+
result.push({ type: "session_started", sessionId: data.sessionId });
|
|
277
|
+
break;
|
|
278
|
+
// turn_boundary marks the START of a loop, and checkpoint/summary logs
|
|
279
|
+
// are model context maintenance. They are not user-visible messages.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
export function replayTranscript(records) {
|
|
285
|
+
let state = initialChatState();
|
|
286
|
+
for (const event of transcriptToStreamEvents(records))
|
|
287
|
+
state = reduceStream(state, event);
|
|
288
|
+
// A durable snapshot cannot establish liveness. The authenticated host owns
|
|
289
|
+
// running state; leave incomplete tools visible but never spin from old logs.
|
|
290
|
+
return { ...state, run: state.run === "error" ? "error" : "idle", liveByAgent: {} };
|
|
291
|
+
}
|