@olares/lares-core 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +23 -0
  2. package/brand/identity.d.ts +9 -0
  3. package/brand/identity.js +82 -0
  4. package/brand/manifest.js +15 -0
  5. package/drive/download.js +40 -0
  6. package/drive/execute.js +143 -0
  7. package/drive/files-path.js +55 -0
  8. package/drive/ls.js +126 -0
  9. package/drive/paths.js +320 -0
  10. package/drive/tools.js +206 -0
  11. package/drive/url-download.js +314 -0
  12. package/files/deliverables.js +32 -0
  13. package/files/disposition.js +20 -0
  14. package/files/filename.js +22 -0
  15. package/files/intake.js +217 -0
  16. package/files/limits.js +1 -0
  17. package/files/markdown.js +88 -0
  18. package/files/mention.js +64 -0
  19. package/files/preview-cache.js +100 -0
  20. package/files/preview-groups.js +21 -0
  21. package/files/preview-workspace.js +342 -0
  22. package/files/preview.js +305 -0
  23. package/files/upload-client.js +40 -0
  24. package/files/upload-http.js +22 -0
  25. package/files/upload.js +104 -0
  26. package/i18n/chat-model-switch.js +41 -0
  27. package/i18n/chat-model.js +40 -0
  28. package/i18n/files.js +39 -0
  29. package/i18n/mobile.js +115 -0
  30. package/i18n/preview.js +57 -0
  31. package/i18n/search.js +33 -0
  32. package/i18n/t.js +22 -0
  33. package/i18n/voice.js +70 -0
  34. package/icons/mark.js +22 -0
  35. package/larepass/chat.js +134 -0
  36. package/larepass/context-provenance.js +65 -0
  37. package/larepass/host.js +80 -0
  38. package/larepass/mux.js +194 -0
  39. package/larepass/questions.js +16 -0
  40. package/larepass/rpc.js +60 -0
  41. package/larepass/runtime.js +395 -0
  42. package/larepass/session-cache.js +104 -0
  43. package/larepass/settings.js +100 -0
  44. package/larepass/stage-copy.js +16 -0
  45. package/larepass/tool-row.js +132 -0
  46. package/larepass/transcript.js +332 -0
  47. package/media/ffmpeg-run.js +117 -0
  48. package/media/router-images.js +63 -0
  49. package/olares/entrance.js +45 -0
  50. package/olares/identity.js +190 -0
  51. package/olares/session-identity.js +48 -0
  52. package/olares/trusted-host.js +37 -0
  53. package/package.json +111 -0
  54. package/router/catalog-sync.js +87 -0
  55. package/router/catalog.d.ts +15 -0
  56. package/router/catalog.js +73 -0
  57. package/router/gateway.d.ts +4 -0
  58. package/router/gateway.js +21 -0
  59. package/router/models-client.js +39 -0
  60. package/router/models.d.ts +16 -0
  61. package/router/models.js +133 -0
  62. package/router/search.js +142 -0
  63. package/router/session-model.js +143 -0
  64. package/router/shim.js +134 -0
  65. package/router/stt.js +200 -0
  66. package/search/client.js +38 -0
  67. package/search/config.js +98 -0
  68. package/search/menu.js +22 -0
  69. package/tools/async.js +34 -0
  70. package/tools/crypto-uuid.js +26 -0
  71. package/tools/http.js +123 -0
  72. package/tools/json-file.js +18 -0
  73. package/voice/client.js +90 -0
  74. package/voice/config.js +59 -0
  75. package/voice/languages.js +29 -0
  76. package/voice/recorder.js +43 -0
  77. package/voice/service.js +83 -0
  78. package/workspace/env.js +13 -0
  79. package/workspace/home.js +9 -0
  80. package/workspace/path.js +80 -0
  81. package/workspace/seed.js +13 -0
  82. package/workspace/session.js +42 -0
@@ -0,0 +1,104 @@
1
+ import { mergeEvents } from "./transcript.js";
2
+
3
+ function emptyPage() {
4
+ return {
5
+ events: [],
6
+ scroll: { top: 0, stick: true },
7
+ ready: false,
8
+ };
9
+ }
10
+
11
+ function eventStamp(event) {
12
+ return `${event.seq}\0${event.type}\0${JSON.stringify(event.data)}\0${JSON.stringify(event.view ?? null)}`;
13
+ }
14
+
15
+ export function eventsUpdated(prev, next) {
16
+ if (prev === next) return false;
17
+ if (!Array.isArray(prev) || !Array.isArray(next) || prev.length !== next.length) return true;
18
+ for (let i = 0; i < next.length; i += 1) {
19
+ if (prev[i] === next[i]) continue;
20
+ if (eventStamp(prev[i]) !== eventStamp(next[i])) return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ export function createSessionCache() {
26
+ const pages = new Map();
27
+ const loads = new Map();
28
+
29
+ function peek(sessionId) {
30
+ return sessionId ? pages.get(sessionId) ?? null : null;
31
+ }
32
+
33
+ function ensure(sessionId) {
34
+ let page = peek(sessionId);
35
+ if (page) return page;
36
+ page = emptyPage();
37
+ pages.set(sessionId, page);
38
+ return page;
39
+ }
40
+
41
+ function commit(sessionId, extra, ready) {
42
+ if (!sessionId) return false;
43
+ const page = ensure(sessionId);
44
+ const next = extra?.length ? mergeEvents(page.events, extra) : page.events;
45
+ const changed = eventsUpdated(page.events, next);
46
+ if (changed) page.events = next;
47
+ let flipped = false;
48
+ if (ready && !page.ready) {
49
+ page.ready = true;
50
+ flipped = true;
51
+ }
52
+ return changed || flipped;
53
+ }
54
+
55
+ return {
56
+ peek,
57
+ events(sessionId) {
58
+ return peek(sessionId)?.events ?? [];
59
+ },
60
+ scroll(sessionId) {
61
+ return peek(sessionId)?.scroll ?? { top: 0, stick: true };
62
+ },
63
+ ready(sessionId) {
64
+ return Boolean(peek(sessionId)?.ready);
65
+ },
66
+ loading(sessionId) {
67
+ return Boolean(sessionId) && loads.has(sessionId);
68
+ },
69
+ merge(sessionId, extra) {
70
+ return commit(sessionId, extra, false);
71
+ },
72
+ rememberHistory(sessionId, extra) {
73
+ return commit(sessionId, extra, true);
74
+ },
75
+ readyEmpty(sessionId) {
76
+ if (!sessionId) return;
77
+ const page = ensure(sessionId);
78
+ if (page.events.length === 0) page.ready = true;
79
+ },
80
+ drop(sessionId) {
81
+ if (!sessionId) return;
82
+ pages.delete(sessionId);
83
+ loads.delete(sessionId);
84
+ },
85
+ setScroll(sessionId, scroll) {
86
+ if (!sessionId || !scroll) return;
87
+ ensure(sessionId).scroll = scroll;
88
+ },
89
+ load(sessionId, start) {
90
+ if (!sessionId) return Promise.resolve();
91
+ const pending = loads.get(sessionId);
92
+ if (pending) return pending;
93
+ const job = Promise.resolve().then(start).finally(() => {
94
+ if (loads.get(sessionId) === job) loads.delete(sessionId);
95
+ });
96
+ loads.set(sessionId, job);
97
+ return job;
98
+ },
99
+ clear() {
100
+ pages.clear();
101
+ loads.clear();
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,100 @@
1
+ import { createSnapshotStore } from "../tools/async.js";
2
+
3
+ const modelsStore = createSnapshotStore();
4
+ const voiceStore = createSnapshotStore();
5
+ const searchStore = createSnapshotStore();
6
+
7
+ function fail(res, path) {
8
+ const err = res?.body?.error;
9
+ throw new Error(err?.message || err?.code || `${path} ${res?.status ?? ""}`.trim());
10
+ }
11
+
12
+ function payload(res, path) {
13
+ if (!res?.ok) fail(res, path);
14
+ return res.body;
15
+ }
16
+
17
+ function sttModels(body) {
18
+ const list = Array.isArray(body?.stt) ? body.stt : [];
19
+ return list.map((item) => (typeof item === "string" ? item : item?.id)).filter(Boolean);
20
+ }
21
+
22
+ export function rememberedSettings() {
23
+ return {
24
+ models: modelsStore.peek(),
25
+ voice: voiceStore.peek(),
26
+ search: searchStore.peek(),
27
+ };
28
+ }
29
+
30
+ export function resetHostSettingsCache() {
31
+ modelsStore.remember(null);
32
+ voiceStore.remember(null);
33
+ searchStore.remember(null);
34
+ }
35
+
36
+ export function createHostSettings(request) {
37
+ return {
38
+ async models(options = {}) {
39
+ return modelsStore.load(
40
+ async () => payload(await request("/api/lares/models"), "/api/lares/models"),
41
+ options,
42
+ );
43
+ },
44
+ async refreshModels() {
45
+ return modelsStore.remember(
46
+ payload(
47
+ await request("/api/lares/models/refresh", { method: "POST" }),
48
+ "/api/lares/models/refresh",
49
+ ),
50
+ );
51
+ },
52
+ async setDefaultModel(selection) {
53
+ return modelsStore.remember(
54
+ payload(
55
+ await request("/api/lares/models/default", { method: "POST", body: selection }),
56
+ "/api/lares/models/default",
57
+ ),
58
+ );
59
+ },
60
+ async voice(force = false) {
61
+ return voiceStore.load(async () => {
62
+ const query = force ? "?refresh=1" : "";
63
+ const [config, status, models] = await Promise.all([
64
+ request("/api/lares/voice/config"),
65
+ request(`/api/lares/voice/status${query}`),
66
+ request(`/api/lares/voice/models${query}`),
67
+ ]);
68
+ return {
69
+ config: payload(config, "/api/lares/voice/config"),
70
+ status: payload(status, "/api/lares/voice/status"),
71
+ sttModels: sttModels(payload(models, "/api/lares/voice/models")),
72
+ };
73
+ }, { force: Boolean(force) });
74
+ },
75
+ async saveVoice(patch) {
76
+ payload(
77
+ await request("/api/lares/voice/config", { method: "POST", body: patch }),
78
+ "/api/lares/voice/config",
79
+ );
80
+ return this.voice(true);
81
+ },
82
+ async search(options = {}) {
83
+ return searchStore.load(
84
+ async () => payload(await request("/api/lares/web-search/config"), "/api/lares/web-search/config"),
85
+ options,
86
+ );
87
+ },
88
+ async setSearchDefault(id) {
89
+ return searchStore.remember(
90
+ payload(
91
+ await request("/api/lares/web-search/config/default", {
92
+ method: "POST",
93
+ body: { defaultSearchModel: id },
94
+ }),
95
+ "/api/lares/web-search/config/default",
96
+ ),
97
+ );
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,16 @@
1
+ /** Stage chrome copy matches dsh conversation EN / design literals, not locale. */
2
+
3
+ export const STAGE_COPY = {
4
+ think: "Think",
5
+ contextInjection: "Context injection",
6
+ contextRecall: "Session recall",
7
+ retry: {
8
+ active: "Retrying model request",
9
+ cancelled: "Model request retry cancelled",
10
+ started: "Retried model request",
11
+ scheduled: "Waiting to retry model request",
12
+ status: "{label} ({retry}/{maximum}) · {seconds}s",
13
+ delay: "Retry delay: ",
14
+ failure: "Failure reason: ",
15
+ },
16
+ };
@@ -0,0 +1,132 @@
1
+ /** Same row model as dsh-client-ui-tool `toolRowModel` (titles are design literals). */
2
+
3
+ const VARIANT_TITLES = {
4
+ search: "Search",
5
+ read: "Read",
6
+ bash: "Bash",
7
+ write: "Write",
8
+ edit: "Edit",
9
+ code: "Code",
10
+ others: "Tool call",
11
+ };
12
+
13
+ const TOOL_VARIANTS = {
14
+ bash: "bash",
15
+ pwsh: "bash",
16
+ read: "read",
17
+ web_fetch: "read",
18
+ web_search: "search",
19
+ grep: "search",
20
+ glob: "search",
21
+ write: "write",
22
+ edit: "edit",
23
+ run_code: "code",
24
+ cordis_package_inspect: "read",
25
+ cordis_runtime_inspect: "read",
26
+ cordis_run: "others",
27
+ cordis_stop: "others",
28
+ cordis_undefine: "others",
29
+ };
30
+
31
+ const TOOL_TITLES = {
32
+ cordis_package_inspect: "Inspect",
33
+ cordis_runtime_inspect: "Inspect",
34
+ cordis_run: "Run Cordis Plugin",
35
+ cordis_stop: "Stop Cordis Plugin",
36
+ cordis_undefine: "Remove Cordis Plugin",
37
+ pwsh: "Pwsh",
38
+ };
39
+
40
+ const SUMMARY_KEYS = {
41
+ bash: ["description", "command"],
42
+ read: ["path", "file_path", "url"],
43
+ search: ["query", "pattern", "url"],
44
+ write: ["path", "file_path"],
45
+ edit: ["path", "file_path"],
46
+ code: ["description"],
47
+ others: [],
48
+ };
49
+
50
+ function firstLine(text) {
51
+ const newline = String(text ?? "").indexOf("\n");
52
+ return newline === -1 ? String(text ?? "") : String(text).slice(0, newline);
53
+ }
54
+
55
+ function parseArgs(argsRaw) {
56
+ try {
57
+ return JSON.parse(argsRaw);
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ function pickString(args, keys) {
64
+ for (const key of keys) {
65
+ const value = args[key];
66
+ if (typeof value === "string" && value !== "") return value;
67
+ }
68
+ }
69
+
70
+ function deriveSummary(variant, argsRaw) {
71
+ const parsed = parseArgs(argsRaw);
72
+ if (typeof parsed !== "object" || parsed === null) return firstLine(argsRaw);
73
+ if (variant === "search" && Array.isArray(parsed.queries)) {
74
+ const queries = parsed.queries.filter((query) => typeof query === "string" && query !== "");
75
+ if (queries.length > 0) return queries.map(firstLine).join(", ");
76
+ }
77
+ const picked = pickString(parsed, SUMMARY_KEYS[variant]);
78
+ if (picked !== undefined) return firstLine(picked);
79
+ for (const value of Object.values(parsed)) {
80
+ if (typeof value === "string" && value !== "") return firstLine(value);
81
+ }
82
+ return firstLine(argsRaw);
83
+ }
84
+
85
+ const BODY_KEYS = {
86
+ write: ["contents", "content", "code"],
87
+ edit: ["new_string", "newString", "contents", "content"],
88
+ code: ["code"],
89
+ };
90
+
91
+ function deriveBody(variant, argsRaw) {
92
+ if (!argsRaw) return "";
93
+ const parsed = parseArgs(argsRaw);
94
+ if (parsed === undefined) return argsRaw;
95
+ const text = pickString(parsed, BODY_KEYS[variant] ?? []);
96
+ if (text !== undefined) return text;
97
+ return JSON.stringify(parsed, null, 2);
98
+ }
99
+
100
+ export function classifyTool(toolName) {
101
+ return TOOL_VARIANTS[toolName] ?? "others";
102
+ }
103
+
104
+ const VARIANT_ICONS = {
105
+ search: "search",
106
+ read: "browse",
107
+ bash: "api",
108
+ write: "edit",
109
+ edit: "edit",
110
+ code: "code",
111
+ others: "sparkle",
112
+ };
113
+
114
+ export function toolVariantIcon(variant) {
115
+ return VARIANT_ICONS[variant] ?? "sparkle";
116
+ }
117
+
118
+ export function toolRowModel(toolName, argsRaw = "") {
119
+ const name = String(toolName || "");
120
+ const variant = classifyTool(name);
121
+ const base = argsRaw === "" ? "" : deriveSummary(variant, argsRaw);
122
+ const toolTitle = TOOL_TITLES[name];
123
+ const summary = variant === "others" && name && toolTitle === undefined && base
124
+ ? `${name} · ${base}`
125
+ : base;
126
+ return {
127
+ variant,
128
+ title: toolTitle ?? VARIANT_TITLES[variant],
129
+ summary,
130
+ body: deriveBody(variant, argsRaw),
131
+ };
132
+ }
@@ -0,0 +1,332 @@
1
+ import { producedPathsFromView } from "../files/deliverables.js";
2
+ import { contextForm, contextProvenance, isHumanUserSource } from "./context-provenance.js";
3
+ import { toolRowModel } from "./tool-row.js";
4
+
5
+ export function textFromBlocks(blocks) {
6
+ if (!Array.isArray(blocks)) return "";
7
+ return blocks
8
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
9
+ .map((block) => block.text)
10
+ .join("");
11
+ }
12
+
13
+ function eventOf(entry) {
14
+ if (!entry || typeof entry !== "object") return null;
15
+ const event = "event" in entry && entry.event ? entry.event : entry;
16
+ return event && typeof event === "object" ? event : null;
17
+ }
18
+
19
+ function entryView(entry, event) {
20
+ return entry?.view ?? event?.view ?? null;
21
+ }
22
+
23
+ export function mergeEvents(base, extra) {
24
+ const bySeq = new Map();
25
+ for (const item of [...(Array.isArray(base) ? base : []), ...(Array.isArray(extra) ? extra : [])]) {
26
+ const event = eventOf(item);
27
+ if (!event || typeof event.seq !== "number") continue;
28
+ const view = entryView(item, event);
29
+ bySeq.set(event.seq, view ? { ...event, view } : event);
30
+ }
31
+ return [...bySeq.entries()]
32
+ .sort((left, right) => left[0] - right[0])
33
+ .map(([, event]) => event);
34
+ }
35
+
36
+ function callViewOf(view) {
37
+ return view?.for === "call" ? view.view : null;
38
+ }
39
+
40
+ function resultViewOf(view) {
41
+ return view?.for === "result" ? view.view : null;
42
+ }
43
+
44
+ function resultCallId(data) {
45
+ return data?.message?.source?.callId || data?.callId || "";
46
+ }
47
+
48
+ function resultIsError(data) {
49
+ if (data?.error) return true;
50
+ const blocks = data?.message?.content;
51
+ return Array.isArray(blocks) && blocks.some((block) => block?.isError);
52
+ }
53
+
54
+ function retrySeconds(milliseconds) {
55
+ return Math.max(1, Math.ceil(Number(milliseconds) / 1000) || 1);
56
+ }
57
+
58
+ function blockKey(turn, step, index) {
59
+ return `${turn ?? 0}:${step ?? 0}:${index ?? 0}`;
60
+ }
61
+
62
+ function messagesFromItems(items) {
63
+ const messages = [];
64
+ for (const item of items) {
65
+ if (item.type === "user") messages.push({ role: "user", text: item.text });
66
+ if (item.type === "assistant") {
67
+ messages.push({
68
+ role: "assistant",
69
+ text: item.text,
70
+ ...(item.pending ? { pending: true } : {}),
71
+ });
72
+ }
73
+ }
74
+ return messages;
75
+ }
76
+
77
+ function upsert(items, indexOf, key, row) {
78
+ if (indexOf.has(key)) {
79
+ Object.assign(items[indexOf.get(key)], row);
80
+ return items[indexOf.get(key)];
81
+ }
82
+ items.push(row);
83
+ indexOf.set(key, items.length - 1);
84
+ return row;
85
+ }
86
+
87
+ function settleOpen(items, running) {
88
+ for (const row of items) {
89
+ if (row.type === "reasoning") row.running = Boolean(running && row.running);
90
+ if (row.type === "assistant" && !running) delete row.pending;
91
+ if (row.type === "retry" && !running && row.retryState === "scheduled") {
92
+ row.retryState = "cancelled";
93
+ }
94
+ }
95
+ }
96
+
97
+ export function foldTranscript(entries) {
98
+ const items = [];
99
+ const blocks = new Map();
100
+ const tools = new Map();
101
+ const retries = new Map();
102
+ const fileSeen = new Set();
103
+ let files = [];
104
+ let filesAt = -1;
105
+ let running = false;
106
+ let error = "";
107
+ let streamCursor = { loc: "", kind: "", index: 0 };
108
+
109
+ const rememberFile = (path) => {
110
+ if (!path || fileSeen.has(path)) return;
111
+ fileSeen.add(path);
112
+ files.push(path);
113
+ };
114
+
115
+ const syncFiles = () => {
116
+ if (!files.length) return;
117
+ if (filesAt >= 0) {
118
+ items[filesAt].paths = [...files];
119
+ return;
120
+ }
121
+ items.push({ type: "files", paths: [...files] });
122
+ filesAt = items.length - 1;
123
+ };
124
+
125
+ const closeTurn = () => {
126
+ running = false;
127
+ for (const row of items) {
128
+ if (row.type === "reasoning") row.running = false;
129
+ if (row.type === "assistant") delete row.pending;
130
+ }
131
+ syncFiles();
132
+ };
133
+
134
+ const putBlock = (turn, step, index, row) => {
135
+ upsert(items, blocks, blockKey(turn, step, index), row);
136
+ };
137
+
138
+ const chunkIndex = (data, chunk) => {
139
+ if (typeof chunk.index === "number") return chunk.index;
140
+ const loc = `${data.turn ?? 0}:${data.step ?? 0}`;
141
+ const kind = chunk.type === "reasoning-delta" || chunk.block?.type === "reasoning"
142
+ ? "reasoning"
143
+ : chunk.type === "text-delta" || chunk.block?.type === "text"
144
+ ? "text"
145
+ : chunk.type;
146
+ if (streamCursor.loc !== loc || streamCursor.kind !== kind) {
147
+ streamCursor.index = streamCursor.loc === loc ? streamCursor.index + 1 : 0;
148
+ streamCursor.loc = loc;
149
+ streamCursor.kind = kind;
150
+ }
151
+ return streamCursor.index;
152
+ };
153
+
154
+ const applyChunk = (data) => {
155
+ const chunk = data?.chunk;
156
+ if (!chunk || typeof chunk !== "object") return;
157
+ const turn = data.turn;
158
+ const step = data.step;
159
+ const index = chunkIndex(data, chunk);
160
+ if (chunk.type === "text-delta" && typeof chunk.text === "string") {
161
+ const prev = blocks.has(blockKey(turn, step, index))
162
+ ? items[blocks.get(blockKey(turn, step, index))]
163
+ : null;
164
+ const text = `${prev?.type === "assistant" ? prev.text : ""}${chunk.text}`;
165
+ putBlock(turn, step, index, {
166
+ type: "assistant",
167
+ text,
168
+ ...(running ? { pending: true } : {}),
169
+ });
170
+ return;
171
+ }
172
+ if (chunk.type === "reasoning-delta" && typeof chunk.text === "string") {
173
+ const prev = blocks.has(blockKey(turn, step, index))
174
+ ? items[blocks.get(blockKey(turn, step, index))]
175
+ : null;
176
+ const text = `${prev?.type === "reasoning" ? prev.text : ""}${chunk.text}`;
177
+ putBlock(turn, step, index, {
178
+ type: "reasoning",
179
+ text,
180
+ running: Boolean(running),
181
+ });
182
+ return;
183
+ }
184
+ if (chunk.type === "block-end" && chunk.block?.type === "text" && typeof chunk.block.text === "string") {
185
+ putBlock(turn, step, index, {
186
+ type: "assistant",
187
+ text: chunk.block.text,
188
+ ...(running ? { pending: true } : {}),
189
+ });
190
+ return;
191
+ }
192
+ if (chunk.type === "block-end" && chunk.block?.type === "reasoning" && typeof chunk.block.text === "string") {
193
+ putBlock(turn, step, index, {
194
+ type: "reasoning",
195
+ text: chunk.block.text,
196
+ running: Boolean(running),
197
+ });
198
+ }
199
+ };
200
+
201
+ const applyAssistantMessage = (data) => {
202
+ const content = data?.message?.content;
203
+ const turn = data?.turn ?? data?.message?.turn;
204
+ const step = data?.step ?? data?.message?.step;
205
+ if (!Array.isArray(content)) return;
206
+ content.forEach((block, index) => {
207
+ if (block?.type === "reasoning" && typeof block.text === "string") {
208
+ putBlock(turn, step, index, { type: "reasoning", text: block.text, running: false });
209
+ return;
210
+ }
211
+ if (block?.type === "text" && typeof block.text === "string") {
212
+ putBlock(turn, step, index, { type: "assistant", text: block.text });
213
+ }
214
+ });
215
+ };
216
+
217
+ for (const entry of Array.isArray(entries) ? entries : []) {
218
+ const event = eventOf(entry);
219
+ if (!event) continue;
220
+ const data = event.data ?? {};
221
+ const view = entryView(entry, event);
222
+
223
+ if (event.type === "turn/start") {
224
+ closeTurn();
225
+ files = [];
226
+ fileSeen.clear();
227
+ filesAt = -1;
228
+ running = true;
229
+ error = "";
230
+ continue;
231
+ }
232
+
233
+ if (event.type === "turn/end") {
234
+ if (data.reason?.kind === "error") {
235
+ error = data.reason.error?.message || "turn failed";
236
+ }
237
+ closeTurn();
238
+ continue;
239
+ }
240
+
241
+ if (event.type === "user/message") {
242
+ const text = textFromBlocks(data.content);
243
+ if (isHumanUserSource(data.source)) {
244
+ if (text) items.push({ type: "user", text });
245
+ continue;
246
+ }
247
+ const provenance = contextProvenance(data.source);
248
+ items.push({
249
+ type: "context",
250
+ role: provenance.role,
251
+ label: provenance.label,
252
+ form: contextForm(data.source),
253
+ text,
254
+ });
255
+ continue;
256
+ }
257
+
258
+ if (event.type === "assistant/message") {
259
+ applyAssistantMessage(data);
260
+ continue;
261
+ }
262
+
263
+ if (event.type === "assistant/chunk") {
264
+ applyChunk(data);
265
+ continue;
266
+ }
267
+
268
+ if (event.type === "tool/call") {
269
+ const callView = callViewOf(view);
270
+ const argsRaw = typeof data.arguments === "string" ? data.arguments : "";
271
+ const model = toolRowModel(data.name, argsRaw);
272
+ const row = {
273
+ type: "tool",
274
+ callId: data.callId,
275
+ name: data.name,
276
+ status: "running",
277
+ argsRaw,
278
+ paths: producedPathsFromView(callView),
279
+ ...model,
280
+ };
281
+ items.push(row);
282
+ if (data.callId) tools.set(data.callId, items.length - 1);
283
+ continue;
284
+ }
285
+
286
+ if (event.type === "tool/result") {
287
+ const id = resultCallId(data);
288
+ const index = tools.get(id);
289
+ const tool = index === undefined ? null : items[index];
290
+ if (tool) {
291
+ tool.status = resultIsError(data) ? "error" : "done";
292
+ const resultView = resultViewOf(view);
293
+ if (resultView?.title) tool.title = resultView.title;
294
+ if (tool.status === "done") {
295
+ for (const path of tool.paths) rememberFile(path);
296
+ }
297
+ }
298
+ continue;
299
+ }
300
+
301
+ if (event.type === "llm/retry") {
302
+ const retryId = data.retryId || String(event.seq ?? items.length);
303
+ upsert(items, retries, retryId, {
304
+ type: "retry",
305
+ retryId,
306
+ retry: data.retry,
307
+ maxRetries: data.maxRetries,
308
+ mode: data.mode,
309
+ delayMs: data.delayMs,
310
+ seconds: retrySeconds(data.delayMs),
311
+ failure: data.failure,
312
+ retryState: "scheduled",
313
+ });
314
+ continue;
315
+ }
316
+
317
+ if (event.type === "llm/retry-started") {
318
+ const retryId = data.retryId;
319
+ const index = retries.get(retryId);
320
+ if (index !== undefined) items[index].retryState = "started";
321
+ }
322
+ }
323
+
324
+ settleOpen(items, running);
325
+ if (running) syncFiles();
326
+ return {
327
+ items,
328
+ messages: messagesFromItems(items),
329
+ running,
330
+ error,
331
+ };
332
+ }