u-foo 2.5.13 → 2.5.15

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +187 -31
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +408 -55
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+
3
+ function emptyTaskContract() {
4
+ return {
5
+ objective: "",
6
+ successCriteria: [],
7
+ constraints: [],
8
+ preferences: [],
9
+ };
10
+ }
11
+
12
+ function emptyStateEpoch() {
13
+ return {
14
+ epochId: 1,
15
+ snapshot: {
16
+ phase: "active",
17
+ currentObjective: "",
18
+ facts: [],
19
+ hypotheses: [],
20
+ decisions: [],
21
+ openQuestions: [],
22
+ },
23
+ commits: [],
24
+ };
25
+ }
26
+
27
+ function extractConstraintsFromText(text = "") {
28
+ const raw = String(text || "");
29
+ const constraints = [];
30
+ const patterns = [
31
+ /(?:don'?t|do not|never|without)\s+[^!?\n]{8,120}/gi,
32
+ /(?:must|should|need to)\s+[^!?\n]{8,120}/gi,
33
+ /(?:不要|不能|禁止|必须)[^.!?\n]{4,80}/g,
34
+ ];
35
+ for (const re of patterns) {
36
+ let match;
37
+ while ((match = re.exec(raw))) {
38
+ const item = String(match[0] || "").trim();
39
+ if (item && !constraints.includes(item)) constraints.push(item);
40
+ }
41
+ }
42
+ return constraints.slice(0, 8);
43
+ }
44
+
45
+ function buildInitialTaskContract(userTask = "") {
46
+ const task = String(userTask || "").trim();
47
+ if (!task) return emptyTaskContract();
48
+ const firstLine = task.split(/\r?\n/).map((l) => l.trim()).find(Boolean) || task;
49
+ return {
50
+ objective: firstLine.slice(0, 500),
51
+ successCriteria: [
52
+ "Complete the requested work with verifiable outcomes",
53
+ "Keep changes aligned with repository conventions",
54
+ ],
55
+ constraints: extractConstraintsFromText(task),
56
+ preferences: [],
57
+ };
58
+ }
59
+
60
+ function patchTaskContract(contract = {}, patch = {}) {
61
+ const base = contract && typeof contract === "object" ? { ...contract } : emptyTaskContract();
62
+ const source = patch && typeof patch === "object" ? patch : {};
63
+ if (typeof source.objective === "string" && source.objective.trim()) {
64
+ // Runtime does not allow silent objective overwrite unless explicitly patched
65
+ if (source.allowObjectiveReplace === true) {
66
+ base.objective = source.objective.trim();
67
+ }
68
+ }
69
+ const mergeList = (key) => {
70
+ const incoming = Array.isArray(source[key]) ? source[key].map(String).filter(Boolean) : [];
71
+ if (incoming.length === 0) return;
72
+ const set = new Set([...(Array.isArray(base[key]) ? base[key] : []), ...incoming]);
73
+ base[key] = Array.from(set);
74
+ };
75
+ mergeList("successCriteria");
76
+ mergeList("constraints");
77
+ mergeList("preferences");
78
+ return base;
79
+ }
80
+
81
+ function renderTaskContract(contract = null) {
82
+ if (!contract || !contract.objective) return "";
83
+ const lines = [
84
+ "Task Contract:",
85
+ `- Objective: ${contract.objective}`,
86
+ ];
87
+ if (Array.isArray(contract.successCriteria) && contract.successCriteria.length > 0) {
88
+ lines.push(`- Success criteria: ${contract.successCriteria.join("; ")}`);
89
+ }
90
+ if (Array.isArray(contract.constraints) && contract.constraints.length > 0) {
91
+ lines.push(`- Constraints: ${contract.constraints.join("; ")}`);
92
+ }
93
+ if (Array.isArray(contract.preferences) && contract.preferences.length > 0) {
94
+ lines.push(`- Preferences: ${contract.preferences.join("; ")}`);
95
+ }
96
+ return lines.join("\n");
97
+ }
98
+
99
+ function normalizeStateCommit(commit = {}) {
100
+ const source = commit && typeof commit === "object" ? commit : {};
101
+ return {
102
+ factsAdd: Array.isArray(source.factsAdd) ? source.factsAdd.map(String).filter(Boolean) : [],
103
+ factsUpdate: Array.isArray(source.factsUpdate) ? source.factsUpdate : [],
104
+ hypothesesAdd: Array.isArray(source.hypothesesAdd) ? source.hypothesesAdd.map(String).filter(Boolean) : [],
105
+ hypothesesUpdate: Array.isArray(source.hypothesesUpdate) ? source.hypothesesUpdate : [],
106
+ decisionsAdd: Array.isArray(source.decisionsAdd) ? source.decisionsAdd.map(String).filter(Boolean) : [],
107
+ questionsAdd: Array.isArray(source.questionsAdd) ? source.questionsAdd.map(String).filter(Boolean) : [],
108
+ questionsClose: Array.isArray(source.questionsClose) ? source.questionsClose.map(String).filter(Boolean) : [],
109
+ nextObjective: typeof source.nextObjective === "string" ? source.nextObjective.trim() : "",
110
+ };
111
+ }
112
+
113
+ function applyListPatch(list = [], { add = [], update = [], close = [] } = {}) {
114
+ let next = Array.isArray(list) ? list.slice() : [];
115
+ for (const item of add) {
116
+ if (!next.includes(item)) next.push(item);
117
+ }
118
+ for (const entry of update) {
119
+ if (!entry || typeof entry !== "object") continue;
120
+ const from = String(entry.from || "").trim();
121
+ const to = String(entry.to || "").trim();
122
+ if (!from || !to) continue;
123
+ next = next.map((v) => (v === from ? to : v));
124
+ }
125
+ for (const item of close) {
126
+ next = next.filter((v) => v !== item);
127
+ }
128
+ return next;
129
+ }
130
+
131
+ function applyStateCommit(stateEpoch = null, commit = {}) {
132
+ const epoch = stateEpoch && typeof stateEpoch === "object"
133
+ ? JSON.parse(JSON.stringify(stateEpoch))
134
+ : emptyStateEpoch();
135
+ const normalized = normalizeStateCommit(commit);
136
+ const snapshot = epoch.snapshot && typeof epoch.snapshot === "object"
137
+ ? epoch.snapshot
138
+ : emptyStateEpoch().snapshot;
139
+
140
+ snapshot.facts = applyListPatch(snapshot.facts, {
141
+ add: normalized.factsAdd,
142
+ update: normalized.factsUpdate,
143
+ });
144
+ snapshot.hypotheses = applyListPatch(snapshot.hypotheses, {
145
+ add: normalized.hypothesesAdd,
146
+ update: normalized.hypothesesUpdate,
147
+ });
148
+ snapshot.decisions = applyListPatch(snapshot.decisions, {
149
+ add: normalized.decisionsAdd,
150
+ });
151
+ snapshot.openQuestions = applyListPatch(snapshot.openQuestions, {
152
+ add: normalized.questionsAdd,
153
+ close: normalized.questionsClose,
154
+ });
155
+ if (normalized.nextObjective) snapshot.currentObjective = normalized.nextObjective;
156
+
157
+ epoch.snapshot = snapshot;
158
+ epoch.commits = Array.isArray(epoch.commits) ? epoch.commits : [];
159
+ epoch.commits.push({
160
+ at: new Date().toISOString(),
161
+ commit: normalized,
162
+ });
163
+ return epoch;
164
+ }
165
+
166
+ function renderStateEpoch(stateEpoch = null) {
167
+ if (!stateEpoch || !stateEpoch.snapshot) return "";
168
+ const snap = stateEpoch.snapshot;
169
+ const lines = [
170
+ "State Snapshot:",
171
+ `- Phase: ${snap.phase || "active"}`,
172
+ snap.currentObjective ? `- Current objective: ${snap.currentObjective}` : "",
173
+ snap.facts && snap.facts.length > 0 ? `- Facts: ${snap.facts.join("; ")}` : "",
174
+ snap.hypotheses && snap.hypotheses.length > 0 ? `- Hypotheses: ${snap.hypotheses.join("; ")}` : "",
175
+ snap.decisions && snap.decisions.length > 0 ? `- Decisions: ${snap.decisions.join("; ")}` : "",
176
+ snap.openQuestions && snap.openQuestions.length > 0 ? `- Open questions: ${snap.openQuestions.join("; ")}` : "",
177
+ ].filter(Boolean);
178
+ const commits = Array.isArray(stateEpoch.commits) ? stateEpoch.commits.slice(-3) : [];
179
+ if (commits.length > 0) {
180
+ lines.push("Recent state commits:");
181
+ for (const entry of commits) {
182
+ const c = entry.commit || {};
183
+ const bits = [];
184
+ if (c.factsAdd && c.factsAdd.length) bits.push(`facts+${c.factsAdd.length}`);
185
+ if (c.decisionsAdd && c.decisionsAdd.length) bits.push(`decisions+${c.decisionsAdd.length}`);
186
+ if (c.nextObjective) bits.push(`next=${c.nextObjective}`);
187
+ lines.push(`- ${entry.at}: ${bits.join(", ") || "noop"}`);
188
+ }
189
+ }
190
+ return lines.join("\n");
191
+ }
192
+
193
+ function extractBalancedJsonObjects(text = "") {
194
+ const source = String(text || "");
195
+ const objects = [];
196
+ for (let i = 0; i < source.length; i += 1) {
197
+ if (source[i] !== "{") continue;
198
+ let depth = 0;
199
+ let inString = false;
200
+ let escaped = false;
201
+ for (let j = i; j < source.length; j += 1) {
202
+ const ch = source[j];
203
+ if (inString) {
204
+ if (escaped) {
205
+ escaped = false;
206
+ } else if (ch === "\\") {
207
+ escaped = true;
208
+ } else if (ch === "\"") {
209
+ inString = false;
210
+ }
211
+ continue;
212
+ }
213
+ if (ch === "\"") {
214
+ inString = true;
215
+ continue;
216
+ }
217
+ if (ch === "{") depth += 1;
218
+ if (ch === "}") {
219
+ depth -= 1;
220
+ if (depth === 0) {
221
+ objects.push(source.slice(i, j + 1));
222
+ i = j;
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ return objects;
229
+ }
230
+
231
+ function isStructuredSideEffectPayload(parsed = null) {
232
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
233
+ return Boolean(
234
+ parsed.stateCommit
235
+ || parsed.contextPlan
236
+ || parsed.nextSegment
237
+ || parsed.type === "execution_segment",
238
+ );
239
+ }
240
+
241
+ function parseStructuredSideEffects(text = "") {
242
+ const raw = String(text || "").trim();
243
+ if (!raw) return null;
244
+ const candidates = [];
245
+ const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/g);
246
+ if (fence) {
247
+ for (const block of fence) {
248
+ candidates.push(block.replace(/```(?:json)?/g, "").replace(/```/g, "").trim());
249
+ }
250
+ }
251
+ candidates.push(raw);
252
+ for (const objectText of extractBalancedJsonObjects(raw)) {
253
+ candidates.push(objectText);
254
+ }
255
+
256
+ for (const item of candidates) {
257
+ try {
258
+ const parsed = JSON.parse(item);
259
+ if (isStructuredSideEffectPayload(parsed)) return parsed;
260
+ } catch {
261
+ // keep scanning
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+
267
+ function resolveCommitInterval(env = process.env) {
268
+ const parsed = Number.parseInt(String(env.UFOO_UCODE_COMMIT_INTERVAL || ""), 10);
269
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
270
+ return 4;
271
+ }
272
+
273
+ function validateStateCommit(commit = {}) {
274
+ const normalized = normalizeStateCommit(commit);
275
+ const errors = [];
276
+ for (const listName of ["factsAdd", "hypothesesAdd", "decisionsAdd", "questionsAdd", "questionsClose"]) {
277
+ const list = normalized[listName];
278
+ if (!Array.isArray(list)) errors.push(`${listName} must be an array`);
279
+ else if (list.some((item) => String(item).length > 500)) errors.push(`${listName} item too long`);
280
+ }
281
+ for (const listName of ["factsUpdate", "hypothesesUpdate"]) {
282
+ const list = normalized[listName];
283
+ if (!Array.isArray(list)) errors.push(`${listName} must be an array`);
284
+ else {
285
+ for (const entry of list) {
286
+ if (!entry || typeof entry !== "object" || !entry.from || !entry.to) {
287
+ errors.push(`${listName} entries require from/to`);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ if (normalized.nextObjective.length > 500) errors.push("nextObjective too long");
293
+ return { ok: errors.length === 0, errors, commit: normalized };
294
+ }
295
+
296
+ function buildDeterministicToolCommit(toolEvent = {}) {
297
+ const source = toolEvent && typeof toolEvent === "object" ? toolEvent : {};
298
+ const tool = String(source.tool || "").trim().toLowerCase();
299
+ const preview = String(source.preview || source.summary || "").trim();
300
+ const artifactId = String(source.artifactId || "").trim();
301
+ if (!tool && !preview) return null;
302
+ const fact = [
303
+ tool ? `tool:${tool}` : "",
304
+ artifactId ? `artifact:${artifactId}` : "",
305
+ preview ? preview.slice(0, 180) : "",
306
+ ].filter(Boolean).join(" ");
307
+ if (!fact) return null;
308
+ return {
309
+ factsAdd: [fact],
310
+ nextObjective: "",
311
+ };
312
+ }
313
+
314
+ function shouldCommitAfterToolCall(session = {}, interval = resolveCommitInterval()) {
315
+ const count = Number(session.toolCallsSinceCommit) || 0;
316
+ return count >= interval;
317
+ }
318
+
319
+ function appendCommitLog(workspaceRoot = process.cwd(), sessionId = "", commitEntry = {}) {
320
+ if (!sessionId) return { ok: false, error: "invalid session id" };
321
+ const fs = require("fs");
322
+ const path = require("path");
323
+ const filePath = path.join(
324
+ path.resolve(workspaceRoot || process.cwd()),
325
+ ".ufoo",
326
+ "agent",
327
+ "ucode",
328
+ "commits",
329
+ `${sessionId}.jsonl`,
330
+ );
331
+ try {
332
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
333
+ fs.appendFileSync(filePath, `${JSON.stringify(commitEntry)}\n`, "utf8");
334
+ return { ok: true, filePath };
335
+ } catch (err) {
336
+ return { ok: false, error: err && err.message ? err.message : "failed to append commit log" };
337
+ }
338
+ }
339
+
340
+ function resolveCommitFoldThreshold(env = process.env) {
341
+ const parsed = Number.parseInt(String(env.UFOO_UCODE_COMMIT_FOLD_THRESHOLD || ""), 10);
342
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
343
+ return 8;
344
+ }
345
+
346
+ function foldCommitsIfNeeded(stateEpoch = null, threshold = resolveCommitFoldThreshold()) {
347
+ const epoch = stateEpoch && typeof stateEpoch === "object"
348
+ ? JSON.parse(JSON.stringify(stateEpoch))
349
+ : emptyStateEpoch();
350
+ const commits = Array.isArray(epoch.commits) ? epoch.commits : [];
351
+ if (commits.length < threshold) return epoch;
352
+ epoch.commits = commits.slice(-3);
353
+ epoch.epochId = (Number(epoch.epochId) || 1) + 1;
354
+ return epoch;
355
+ }
356
+
357
+ function patchTaskContractFromUserMessage(contract = {}, userMessage = "") {
358
+ const constraints = extractConstraintsFromText(userMessage);
359
+ if (constraints.length === 0) return contract;
360
+ return patchTaskContract(contract, { constraints });
361
+ }
362
+
363
+ function applyValidatedStateCommit(session = {}, commit = {}, workspaceRoot = process.cwd()) {
364
+ const validated = validateStateCommit(commit);
365
+ if (!validated.ok) return { ok: false, errors: validated.errors, stateEpoch: session.stateEpoch };
366
+ session.stateEpoch = applyStateCommit(session.stateEpoch, validated.commit);
367
+ session.stateEpoch = foldCommitsIfNeeded(session.stateEpoch);
368
+ session.toolCallsSinceCommit = 0;
369
+ appendCommitLog(workspaceRoot, session.sessionId, {
370
+ at: new Date().toISOString(),
371
+ commit: validated.commit,
372
+ source: "runtime",
373
+ });
374
+ return { ok: true, errors: [], stateEpoch: session.stateEpoch };
375
+ }
376
+
377
+ function ensureTaskContract(session = {}, userTask = "") {
378
+ if (session.taskContract && session.taskContract.objective) return session.taskContract;
379
+ session.taskContract = buildInitialTaskContract(userTask);
380
+ return session.taskContract;
381
+ }
382
+
383
+ function ensureStateEpoch(session = {}) {
384
+ if (session.stateEpoch && session.stateEpoch.snapshot) return session.stateEpoch;
385
+ session.stateEpoch = emptyStateEpoch();
386
+ return session.stateEpoch;
387
+ }
388
+
389
+ module.exports = {
390
+ emptyTaskContract,
391
+ emptyStateEpoch,
392
+ buildInitialTaskContract,
393
+ patchTaskContract,
394
+ patchTaskContractFromUserMessage,
395
+ renderTaskContract,
396
+ normalizeStateCommit,
397
+ validateStateCommit,
398
+ buildDeterministicToolCommit,
399
+ resolveCommitInterval,
400
+ resolveCommitFoldThreshold,
401
+ foldCommitsIfNeeded,
402
+ shouldCommitAfterToolCall,
403
+ appendCommitLog,
404
+ applyValidatedStateCommit,
405
+ applyStateCommit,
406
+ renderStateEpoch,
407
+ parseStructuredSideEffects,
408
+ extractBalancedJsonObjects,
409
+ isStructuredSideEffectPayload,
410
+ ensureTaskContract,
411
+ ensureStateEpoch,
412
+ };
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { randomUUID } = require("crypto");
6
+
7
+ function getTranscriptsDir(workspaceRoot = process.cwd()) {
8
+ const root = path.resolve(workspaceRoot || process.cwd());
9
+ return path.join(root, ".ufoo", "agent", "ucode", "transcripts");
10
+ }
11
+
12
+ function getTranscriptFilePath(workspaceRoot = process.cwd(), sessionId = "") {
13
+ const id = String(sessionId || "").trim();
14
+ if (!id) return "";
15
+ return path.join(getTranscriptsDir(workspaceRoot), `${id}.jsonl`);
16
+ }
17
+
18
+ function createTranscriptEventId() {
19
+ return `msg_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
20
+ }
21
+
22
+ function normalizeTranscriptEvent(input = {}) {
23
+ const source = input && typeof input === "object" ? input : {};
24
+ return {
25
+ id: String(source.id || createTranscriptEventId()).trim(),
26
+ role: String(source.role || "").trim(),
27
+ content: source.content,
28
+ toolCalls: Array.isArray(source.toolCalls) ? source.toolCalls : undefined,
29
+ toolCallId: source.toolCallId ? String(source.toolCallId) : undefined,
30
+ artifactId: source.artifactId ? String(source.artifactId) : undefined,
31
+ preview: source.preview ? String(source.preview) : undefined,
32
+ segmentId: source.segmentId ? String(source.segmentId) : undefined,
33
+ createdAt: String(source.createdAt || new Date().toISOString()),
34
+ rawMessage: source.rawMessage && typeof source.rawMessage === "object"
35
+ ? source.rawMessage
36
+ : undefined,
37
+ };
38
+ }
39
+
40
+ function messageToTranscriptEvent(message = {}, extra = {}) {
41
+ if (!message || typeof message !== "object") return null;
42
+ const role = String(message.role || "").trim();
43
+ if (!role) return null;
44
+ const event = {
45
+ id: createTranscriptEventId(),
46
+ role,
47
+ content: message.content,
48
+ createdAt: new Date().toISOString(),
49
+ rawMessage: message,
50
+ ...extra,
51
+ };
52
+ if (message.tool_calls) event.toolCalls = message.tool_calls;
53
+ if (message.tool_call_id) event.toolCallId = message.tool_call_id;
54
+ return normalizeTranscriptEvent(event);
55
+ }
56
+
57
+ function readTranscriptFile(filePath = "") {
58
+ try {
59
+ if (!filePath || !fs.existsSync(filePath)) return [];
60
+ const raw = fs.readFileSync(filePath, "utf8");
61
+ if (!raw.trim()) return [];
62
+ const events = [];
63
+ for (const line of raw.split(/\r?\n/).map((item) => item.trim()).filter(Boolean)) {
64
+ try {
65
+ events.push(normalizeTranscriptEvent(JSON.parse(line)));
66
+ } catch {
67
+ // ignore malformed line
68
+ }
69
+ }
70
+ return events;
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ function loadTranscript(workspaceRoot = process.cwd(), sessionId = "") {
77
+ const filePath = getTranscriptFilePath(workspaceRoot, sessionId);
78
+ return {
79
+ filePath,
80
+ events: readTranscriptFile(filePath),
81
+ };
82
+ }
83
+
84
+ function appendTranscriptEvent(workspaceRoot = process.cwd(), sessionId = "", event = {}) {
85
+ const filePath = getTranscriptFilePath(workspaceRoot, sessionId);
86
+ if (!filePath) {
87
+ return { ok: false, error: "invalid session id", event: null };
88
+ }
89
+ const normalized = normalizeTranscriptEvent(event);
90
+ try {
91
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
92
+ fs.appendFileSync(filePath, `${JSON.stringify(normalized)}\n`, "utf8");
93
+ return { ok: true, error: "", event: normalized, filePath };
94
+ } catch (err) {
95
+ return {
96
+ ok: false,
97
+ error: err && err.message ? err.message : "failed to append transcript",
98
+ event: normalized,
99
+ filePath,
100
+ };
101
+ }
102
+ }
103
+
104
+ function appendTranscriptMessages(workspaceRoot = process.cwd(), sessionId = "", messages = [], extra = {}) {
105
+ const list = Array.isArray(messages) ? messages : [];
106
+ const appended = [];
107
+ for (const message of list) {
108
+ const event = messageToTranscriptEvent(message, extra);
109
+ if (!event) continue;
110
+ const result = appendTranscriptEvent(workspaceRoot, sessionId, event);
111
+ if (result.ok) appended.push(result.event);
112
+ }
113
+ return appended;
114
+ }
115
+
116
+ function transcriptEventsToMessages(events = [], options = {}) {
117
+ const preferArtifact = options.preferArtifact !== false;
118
+ const list = Array.isArray(events) ? events : [];
119
+ const messages = [];
120
+ for (const event of list) {
121
+ if (preferArtifact && event.artifactId) {
122
+ messages.push({
123
+ role: event.role || "tool",
124
+ content: JSON.stringify({
125
+ artifactId: event.artifactId,
126
+ preview: event.preview || "",
127
+ }),
128
+ tool_call_id: event.toolCallId,
129
+ });
130
+ continue;
131
+ }
132
+ if (!preferArtifact && event.rawMessage && typeof event.rawMessage === "object") {
133
+ messages.push(event.rawMessage);
134
+ continue;
135
+ }
136
+ const message = { role: event.role };
137
+ if (event.content !== undefined) message.content = event.content;
138
+ if (event.toolCalls) message.tool_calls = event.toolCalls;
139
+ if (event.toolCallId) message.tool_call_id = event.toolCallId;
140
+ messages.push(message);
141
+ }
142
+ return messages;
143
+ }
144
+
145
+ function migrateNlMessagesToTranscript(workspaceRoot = process.cwd(), sessionId = "", nlMessages = []) {
146
+ const filePath = getTranscriptFilePath(workspaceRoot, sessionId);
147
+ if (filePath && fs.existsSync(filePath)) {
148
+ return loadTranscript(workspaceRoot, sessionId).events;
149
+ }
150
+ const messages = Array.isArray(nlMessages) ? nlMessages : [];
151
+ if (messages.length === 0) return [];
152
+ appendTranscriptMessages(workspaceRoot, sessionId, messages, { migrated: true });
153
+ return loadTranscript(workspaceRoot, sessionId).events;
154
+ }
155
+
156
+ function deleteTranscript(workspaceRoot = process.cwd(), sessionId = "") {
157
+ const filePath = getTranscriptFilePath(workspaceRoot, sessionId);
158
+ if (!filePath || !fs.existsSync(filePath)) return { ok: true, error: "" };
159
+ try {
160
+ fs.unlinkSync(filePath);
161
+ return { ok: true, error: "" };
162
+ } catch (err) {
163
+ return {
164
+ ok: false,
165
+ error: err && err.message ? err.message : "failed to delete transcript",
166
+ };
167
+ }
168
+ }
169
+
170
+ module.exports = {
171
+ getTranscriptsDir,
172
+ getTranscriptFilePath,
173
+ createTranscriptEventId,
174
+ normalizeTranscriptEvent,
175
+ messageToTranscriptEvent,
176
+ loadTranscript,
177
+ appendTranscriptEvent,
178
+ appendTranscriptMessages,
179
+ transcriptEventsToMessages,
180
+ migrateNlMessagesToTranscript,
181
+ deleteTranscript,
182
+ };
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ const {
4
+ normalizeTranscriptEvent,
5
+ createTranscriptEventId,
6
+ appendTranscriptEvent,
7
+ } = require("./transcript");
8
+
9
+ function messageRole(message = {}) {
10
+ return String(message && message.role || "").trim().toLowerCase();
11
+ }
12
+
13
+ function isToolRoleMessage(message = {}) {
14
+ const role = messageRole(message);
15
+ if (role === "tool") return true;
16
+ if (Array.isArray(message.content)) {
17
+ return message.content.some((block) => block && block.type === "tool_result");
18
+ }
19
+ return false;
20
+ }
21
+
22
+ function parseToolArtifactContent(content = "") {
23
+ if (typeof content !== "string" || !content.trim()) return null;
24
+ try {
25
+ const parsed = JSON.parse(content);
26
+ if (!parsed || typeof parsed !== "object") return null;
27
+ const artifactId = String(parsed.artifactId || "").trim();
28
+ if (!artifactId) return null;
29
+ return {
30
+ artifactId,
31
+ preview: String(parsed.preview || "").trim(),
32
+ };
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function messageToTranscriptEventForStorage(message = {}, extra = {}) {
39
+ if (!message || typeof message !== "object") return null;
40
+ const role = messageRole(message);
41
+ if (!role) return null;
42
+
43
+ const base = {
44
+ id: createTranscriptEventId(),
45
+ role,
46
+ createdAt: new Date().toISOString(),
47
+ ...extra,
48
+ };
49
+
50
+ if (isToolRoleMessage(message)) {
51
+ const artifact = parseToolArtifactContent(
52
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content || ""),
53
+ );
54
+ if (artifact) {
55
+ return normalizeTranscriptEvent({
56
+ ...base,
57
+ role: "tool",
58
+ artifactId: artifact.artifactId,
59
+ preview: artifact.preview,
60
+ toolCallId: message.tool_call_id ? String(message.tool_call_id) : undefined,
61
+ });
62
+ }
63
+ const preview = typeof message.content === "string"
64
+ ? message.content.slice(0, 600)
65
+ : JSON.stringify(message.content).slice(0, 600);
66
+ return normalizeTranscriptEvent({
67
+ ...base,
68
+ role: "tool",
69
+ preview,
70
+ toolCallId: message.tool_call_id ? String(message.tool_call_id) : undefined,
71
+ content: preview,
72
+ });
73
+ }
74
+
75
+ if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
76
+ return normalizeTranscriptEvent({
77
+ ...base,
78
+ content: message.content,
79
+ toolCalls: message.tool_calls,
80
+ });
81
+ }
82
+
83
+ return normalizeTranscriptEvent({
84
+ ...base,
85
+ content: message.content,
86
+ });
87
+ }
88
+
89
+ function appendTranscriptMessagesForStorage(workspaceRoot = process.cwd(), sessionId = "", messages = [], extra = {}) {
90
+ const list = Array.isArray(messages) ? messages : [];
91
+ const appended = [];
92
+ for (const message of list) {
93
+ const event = messageToTranscriptEventForStorage(message, extra);
94
+ if (!event) continue;
95
+ const result = appendTranscriptEvent(workspaceRoot, sessionId, event);
96
+ if (result.ok) appended.push(result.event);
97
+ }
98
+ return appended;
99
+ }
100
+
101
+ module.exports = {
102
+ parseToolArtifactContent,
103
+ messageToTranscriptEventForStorage,
104
+ appendTranscriptMessagesForStorage,
105
+ isToolRoleMessage,
106
+ };