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,314 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require("crypto");
4
+
5
+ function emptyExecutionState() {
6
+ return {
7
+ currentSegmentId: "",
8
+ mode: "single_action",
9
+ steps: {},
10
+ modifiedFiles: [],
11
+ lastExitCodes: [],
12
+ approvals: [],
13
+ retries: {},
14
+ segments: [],
15
+ };
16
+ }
17
+
18
+ function createSegmentId() {
19
+ return `seg_${Date.now().toString(36)}_${randomUUID().slice(0, 6)}`;
20
+ }
21
+
22
+ function normalizeExecutionSegment(segment = {}) {
23
+ const source = segment && typeof segment === "object" ? segment : {};
24
+ const steps = Array.isArray(source.steps) ? source.steps : [];
25
+ return {
26
+ type: String(source.type || "execution_segment").trim(),
27
+ objective: String(source.objective || "").trim(),
28
+ steps: steps.map((step, index) => ({
29
+ id: String(step.id || `s${index + 1}`).trim(),
30
+ tool: String(step.tool || "").trim(),
31
+ args: step.args && typeof step.args === "object" ? step.args : {},
32
+ dependsOn: Array.isArray(step.dependsOn) ? step.dependsOn.map(String) : [],
33
+ })),
34
+ checkpoint: source.checkpoint && typeof source.checkpoint === "object"
35
+ ? source.checkpoint
36
+ : { after: [] },
37
+ };
38
+ }
39
+
40
+ function startExecutionSegment(executionState = null, segment = {}) {
41
+ const state = executionState && typeof executionState === "object"
42
+ ? { ...executionState }
43
+ : emptyExecutionState();
44
+ const normalized = normalizeExecutionSegment(segment);
45
+ const segmentId = createSegmentId();
46
+ state.currentSegmentId = segmentId;
47
+ state.mode = normalized.type === "execution_segment" ? "execution_segment" : "single_action";
48
+ state.segments = Array.isArray(state.segments) ? state.segments : [];
49
+ state.segments.push({
50
+ segmentId,
51
+ objective: normalized.objective,
52
+ startedAt: new Date().toISOString(),
53
+ steps: normalized.steps,
54
+ checkpoint: normalized.checkpoint,
55
+ status: "running",
56
+ });
57
+ state.steps = {};
58
+ return { state, segmentId, segment: normalized };
59
+ }
60
+
61
+ function recordStepResult(executionState = null, {
62
+ stepId = "",
63
+ status = "success",
64
+ artifactId = "",
65
+ exitCode = null,
66
+ error = "",
67
+ } = {}) {
68
+ const state = executionState && typeof executionState === "object"
69
+ ? { ...executionState }
70
+ : emptyExecutionState();
71
+ state.steps = state.steps && typeof state.steps === "object" ? { ...state.steps } : {};
72
+ state.steps[stepId] = {
73
+ status: String(status || "success"),
74
+ artifactId: String(artifactId || ""),
75
+ exitCode,
76
+ error: String(error || ""),
77
+ at: new Date().toISOString(),
78
+ };
79
+ return state;
80
+ }
81
+
82
+ function shouldStopSegment(executionState = null, {
83
+ hadError = false,
84
+ schemaMismatch = false,
85
+ sideEffect = false,
86
+ reachedCheckpoint = false,
87
+ } = {}) {
88
+ if (hadError || schemaMismatch || sideEffect) return true;
89
+ if (reachedCheckpoint) return true;
90
+ const state = executionState && typeof executionState === "object" ? executionState : null;
91
+ if (!state || state.mode !== "execution_segment") return true;
92
+ return false;
93
+ }
94
+
95
+ function renderExecutionSegmentContext(executionState = null) {
96
+ if (!executionState || !executionState.currentSegmentId) return "";
97
+ const lines = [
98
+ "Current Execution Segment:",
99
+ `- Segment: ${executionState.currentSegmentId}`,
100
+ `- Mode: ${executionState.mode || "single_action"}`,
101
+ ];
102
+ const steps = executionState.steps && typeof executionState.steps === "object"
103
+ ? Object.entries(executionState.steps)
104
+ : [];
105
+ if (steps.length > 0) {
106
+ lines.push("Step status:");
107
+ for (const [id, info] of steps) {
108
+ lines.push(`- ${id}: ${info.status}${info.error ? ` (${info.error})` : ""}`);
109
+ }
110
+ }
111
+ return lines.join("\n");
112
+ }
113
+
114
+ function parseExecutionSegment(sideEffects = null) {
115
+ if (!sideEffects || typeof sideEffects !== "object") return null;
116
+ if (sideEffects.nextSegment) return normalizeExecutionSegment(sideEffects.nextSegment);
117
+ if (sideEffects.type === "execution_segment") return normalizeExecutionSegment(sideEffects);
118
+ return null;
119
+ }
120
+
121
+ const DEFAULT_MAX_SEGMENT_STEPS = 4;
122
+ const SIDE_EFFECT_TOOLS = new Set(["write", "edit"]);
123
+
124
+ function resolveStepArgs(args = {}, stepOutputs = new Map()) {
125
+ const next = args && typeof args === "object" ? { ...args } : {};
126
+ const argsJson = JSON.stringify(next);
127
+ for (const [depId, depValue] of stepOutputs.entries()) {
128
+ const token = `\${${depId}.matches}`;
129
+ if (argsJson.includes(token) && depValue && depValue.matches) {
130
+ next.matches = depValue.matches;
131
+ }
132
+ }
133
+ return next;
134
+ }
135
+
136
+ function isSideEffectTool(tool = "") {
137
+ return SIDE_EFFECT_TOOLS.has(String(tool || "").trim().toLowerCase());
138
+ }
139
+
140
+ function formatSegmentResultMessage(result = {}) {
141
+ return JSON.stringify({
142
+ type: "execution_segment_result",
143
+ segmentId: result.segmentId || "",
144
+ ok: result.ok !== false,
145
+ objective: result.objective || "",
146
+ stoppedAt: result.stoppedAt || "",
147
+ steps: Array.isArray(result.results) ? result.results : [],
148
+ error: result.error || "",
149
+ });
150
+ }
151
+
152
+ function executeExecutionSegment({
153
+ segment = {},
154
+ executionState = null,
155
+ runStep = () => ({ ok: false, error: "no runner" }),
156
+ onStepStart = null,
157
+ onStepComplete = null,
158
+ maxSteps = DEFAULT_MAX_SEGMENT_STEPS,
159
+ } = {}) {
160
+ const normalized = normalizeExecutionSegment(segment);
161
+ const cappedSteps = normalized.steps.slice(0, Math.max(1, Math.floor(maxSteps)));
162
+ const cappedSegment = { ...normalized, steps: cappedSteps };
163
+ const { state: startedState, segmentId } = startExecutionSegment(executionState, cappedSegment);
164
+ let state = startedState;
165
+ const stepOutputs = new Map();
166
+ const results = [];
167
+ const checkpointAfter = new Set(
168
+ Array.isArray(cappedSegment.checkpoint && cappedSegment.checkpoint.after)
169
+ ? cappedSegment.checkpoint.after.map(String)
170
+ : [],
171
+ );
172
+ let stoppedAt = "";
173
+ let fatalError = "";
174
+
175
+ for (const step of cappedSteps) {
176
+ const deps = Array.isArray(step.dependsOn) ? step.dependsOn : [];
177
+ for (const dep of deps) {
178
+ if (!stepOutputs.has(dep)) {
179
+ state = recordStepResult(state, {
180
+ stepId: step.id,
181
+ status: "failed",
182
+ error: `missing dependency ${dep}`,
183
+ });
184
+ fatalError = `segment dependency missing: ${dep}`;
185
+ state = completeExecutionSegment(state, { status: "failed", error: fatalError });
186
+ return {
187
+ ok: false,
188
+ segmentId,
189
+ objective: cappedSegment.objective,
190
+ executionState: state,
191
+ results,
192
+ error: fatalError,
193
+ stoppedAt: "dependency",
194
+ };
195
+ }
196
+ }
197
+
198
+ const args = resolveStepArgs(step.args, stepOutputs);
199
+ if (typeof onStepStart === "function") {
200
+ try {
201
+ onStepStart({ stepId: step.id, tool: step.tool, args });
202
+ } catch {
203
+ // ignore
204
+ }
205
+ }
206
+
207
+ const result = runStep({ stepId: step.id, tool: step.tool, args }) || { ok: false, error: "step failed" };
208
+ const stepRecord = {
209
+ stepId: step.id,
210
+ tool: step.tool,
211
+ ok: result.ok !== false,
212
+ artifactId: result.artifactId || "",
213
+ error: result.error || "",
214
+ };
215
+ results.push(stepRecord);
216
+
217
+ if (result.ok === false) {
218
+ state = recordStepResult(state, {
219
+ stepId: step.id,
220
+ status: "failed",
221
+ error: String(result.error || "step failed"),
222
+ });
223
+ fatalError = String(result.error || "segment step failed");
224
+ state = completeExecutionSegment(state, { status: "failed", error: fatalError });
225
+ return {
226
+ ok: false,
227
+ segmentId,
228
+ objective: cappedSegment.objective,
229
+ executionState: state,
230
+ results,
231
+ error: fatalError,
232
+ stoppedAt: "error",
233
+ };
234
+ }
235
+
236
+ stepOutputs.set(step.id, result);
237
+ state = recordStepResult(state, {
238
+ stepId: step.id,
239
+ status: "success",
240
+ artifactId: result.artifactId || "",
241
+ exitCode: Number.isFinite(result.code) ? result.code : null,
242
+ });
243
+
244
+ if (typeof onStepComplete === "function") {
245
+ try {
246
+ onStepComplete({ stepId: step.id, tool: step.tool, args, result });
247
+ } catch {
248
+ // ignore
249
+ }
250
+ }
251
+
252
+ if (checkpointAfter.has(step.id)) {
253
+ stoppedAt = "checkpoint";
254
+ break;
255
+ }
256
+ if (isSideEffectTool(step.tool)) {
257
+ stoppedAt = "side_effect";
258
+ break;
259
+ }
260
+ }
261
+
262
+ const finalStatus = stoppedAt === "checkpoint" ? "checkpoint" : "success";
263
+ state = completeExecutionSegment(state, { status: finalStatus });
264
+ return {
265
+ ok: true,
266
+ segmentId,
267
+ objective: cappedSegment.objective,
268
+ executionState: state,
269
+ results,
270
+ error: "",
271
+ stoppedAt,
272
+ };
273
+ }
274
+
275
+ function completeExecutionSegment(executionState = null, { status = "success", error = "" } = {}) {
276
+ const state = executionState && typeof executionState === "object"
277
+ ? { ...executionState }
278
+ : emptyExecutionState();
279
+ const segments = Array.isArray(state.segments) ? state.segments.slice() : [];
280
+ const currentId = String(state.currentSegmentId || "").trim();
281
+ if (currentId) {
282
+ for (let i = segments.length - 1; i >= 0; i -= 1) {
283
+ if (segments[i].segmentId === currentId) {
284
+ segments[i] = {
285
+ ...segments[i],
286
+ status,
287
+ error: String(error || ""),
288
+ endedAt: new Date().toISOString(),
289
+ };
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ state.segments = segments;
295
+ state.currentSegmentId = "";
296
+ state.mode = "single_action";
297
+ return state;
298
+ }
299
+
300
+ module.exports = {
301
+ DEFAULT_MAX_SEGMENT_STEPS,
302
+ emptyExecutionState,
303
+ createSegmentId,
304
+ normalizeExecutionSegment,
305
+ startExecutionSegment,
306
+ recordStepResult,
307
+ shouldStopSegment,
308
+ renderExecutionSegmentContext,
309
+ parseExecutionSegment,
310
+ completeExecutionSegment,
311
+ executeExecutionSegment,
312
+ formatSegmentResultMessage,
313
+ isSideEffectTool,
314
+ };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ function isContextV2Enabled(env = process.env) {
4
+ const raw = String(env.UFOO_UCODE_CONTEXT_V2 || "").trim().toLowerCase();
5
+ // Default ON. Explicit opt-out: 0 / false / off / no.
6
+ if (!raw) return true;
7
+ if (raw === "0" || raw === "false" || raw === "off" || raw === "no") return false;
8
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
9
+ }
10
+
11
+ module.exports = {
12
+ isContextV2Enabled,
13
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ ...require("./featureFlag"),
5
+ ...require("./transcript"),
6
+ ...require("./transcriptSync"),
7
+ ...require("./artifacts"),
8
+ ...require("./artifactIndex"),
9
+ ...require("./artifactGc"),
10
+ ...require("./stableJson"),
11
+ ...require("./reducers"),
12
+ ...require("./promptLayers"),
13
+ ...require("./projectSnapshot"),
14
+ ...require("./stateCommit"),
15
+ ...require("./workingSet"),
16
+ ...require("./executionSegment"),
17
+ ...require("./assembler"),
18
+ };
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { runToolCall } = require("../dispatch");
6
+ const { saveArtifact, createArtifactId, hashContent } = require("./artifacts");
7
+
8
+ const PREFLIGHT_FILES = [
9
+ "AGENTS.md",
10
+ "README.md",
11
+ "README.zh-CN.md",
12
+ "package.json",
13
+ ];
14
+
15
+ function readFileIfExists(workspaceRoot = process.cwd(), relPath = "") {
16
+ const full = path.resolve(workspaceRoot, relPath);
17
+ try {
18
+ if (!fs.existsSync(full) || !fs.statSync(full).isFile()) return null;
19
+ const content = fs.readFileSync(full, "utf8");
20
+ return { path: relPath, content, hash: hashContent(content) };
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function summarizePackageJson(content = "") {
27
+ try {
28
+ const parsed = JSON.parse(content);
29
+ return {
30
+ name: parsed.name || "",
31
+ packageManager: parsed.packageManager || "",
32
+ scripts: Object.keys(parsed.scripts || {}).slice(0, 8),
33
+ };
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function summarizeAgentsRules(content = "") {
40
+ const lines = String(content || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
41
+ return lines.slice(0, 12);
42
+ }
43
+
44
+ function summarizeReadme(content = "") {
45
+ const lines = String(content || "").split(/\r?\n/);
46
+ const headings = lines
47
+ .filter((l) => /^#{1,3}\s+/.test(l))
48
+ .slice(0, 10);
49
+ const intro = lines.filter((l) => l.trim() && !l.startsWith("#")).slice(0, 3).join(" ");
50
+ return { headings, intro: intro.slice(0, 240) };
51
+ }
52
+
53
+ function collectCurrentFileHashes(workspaceRoot = process.cwd()) {
54
+ const root = path.resolve(workspaceRoot || process.cwd());
55
+ return PREFLIGHT_FILES.map((relPath) => {
56
+ const file = readFileIfExists(root, relPath);
57
+ return file ? { path: relPath, hash: file.hash } : null;
58
+ }).filter(Boolean);
59
+ }
60
+
61
+ function isProjectSnapshotStale(snapshot = null, workspaceRoot = process.cwd()) {
62
+ if (!snapshot || !snapshot.projectSnapshotId || !Array.isArray(snapshot.files)) return true;
63
+ const current = collectCurrentFileHashes(workspaceRoot);
64
+ if (current.length !== snapshot.files.length) return true;
65
+ const byPath = new Map(snapshot.files.map((entry) => [entry.path, entry.hash]));
66
+ for (const entry of current) {
67
+ if (byPath.get(entry.path) !== entry.hash) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function invalidateProjectSnapshotIfPathTouched(session = {}, filePath = "") {
73
+ if (!session || typeof session !== "object") return false;
74
+ const rel = String(filePath || "").trim().replace(/\\/g, "/");
75
+ if (!rel) return false;
76
+ const touched = PREFLIGHT_FILES.some((name) => (
77
+ rel === name || rel.endsWith(`/${name}`)
78
+ ));
79
+ if (!touched) return false;
80
+ session.projectSnapshot = null;
81
+ return true;
82
+ }
83
+
84
+ function buildProjectSnapshot({
85
+ workspaceRoot = process.cwd(),
86
+ sessionId = "",
87
+ existing = null,
88
+ } = {}) {
89
+ const root = path.resolve(workspaceRoot || process.cwd());
90
+ if (existing && !isProjectSnapshotStale(existing, root)) {
91
+ return existing;
92
+ }
93
+ const files = [];
94
+ const summary = {
95
+ language: "",
96
+ packageManager: "",
97
+ entryPoints: [],
98
+ rules: [],
99
+ readmeHeadings: [],
100
+ readmeIntro: "",
101
+ };
102
+
103
+ for (const relPath of PREFLIGHT_FILES) {
104
+ const file = readFileIfExists(root, relPath);
105
+ if (!file) continue;
106
+ const artifactId = createArtifactId(`artifact_${relPath.replace(/[^a-zA-Z0-9]+/g, "_")}`);
107
+ saveArtifact(root, sessionId, {
108
+ artifactId,
109
+ type: "source_file",
110
+ source: relPath,
111
+ tool: "read",
112
+ raw: { ok: true, path: relPath, content: file.content },
113
+ summary: relPath,
114
+ createdBy: "project_snapshot",
115
+ });
116
+ files.push({
117
+ path: relPath,
118
+ artifactId,
119
+ hash: file.hash,
120
+ });
121
+ if (relPath === "package.json") {
122
+ const pkg = summarizePackageJson(file.content);
123
+ summary.packageManager = pkg.packageManager || "";
124
+ summary.language = "Node.js";
125
+ summary.entryPoints = pkg.scripts || [];
126
+ }
127
+ if (relPath === "AGENTS.md") {
128
+ summary.rules = summarizeAgentsRules(file.content);
129
+ }
130
+ if (relPath.startsWith("README")) {
131
+ const readme = summarizeReadme(file.content);
132
+ summary.readmeHeadings = readme.headings;
133
+ summary.readmeIntro = readme.intro;
134
+ }
135
+ }
136
+
137
+ const snapshotId = `project_snapshot_${hashContent(JSON.stringify(files))}`;
138
+
139
+ return {
140
+ projectSnapshotId: snapshotId,
141
+ files,
142
+ summary,
143
+ createdAt: new Date().toISOString(),
144
+ };
145
+ }
146
+
147
+ function renderProjectSnapshotContext(snapshot = null) {
148
+ if (!snapshot || !snapshot.projectSnapshotId) return "";
149
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : {};
150
+ const fileRefs = (Array.isArray(snapshot.files) ? snapshot.files : [])
151
+ .map((f) => `- ${f.path}: artifact://${f.artifactId} (hash ${f.hash})`)
152
+ .join("\n");
153
+ const lines = [
154
+ "Project Snapshot:",
155
+ summary.language ? `- Language: ${summary.language}` : "",
156
+ summary.packageManager ? `- Package manager: ${summary.packageManager}` : "",
157
+ summary.rules && summary.rules.length > 0
158
+ ? `- Repository rules: ${summary.rules.slice(0, 5).join("; ")}`
159
+ : "",
160
+ summary.readmeIntro ? `- README intro: ${summary.readmeIntro}` : "",
161
+ fileRefs ? `Files:\n${fileRefs}` : "",
162
+ ].filter(Boolean);
163
+ return lines.join("\n");
164
+ }
165
+
166
+ function createProjectPreflightContextV2({
167
+ workspaceRoot = process.cwd(),
168
+ sessionId = "",
169
+ pushToolLog = () => null,
170
+ existingSnapshot = null,
171
+ } = {}) {
172
+ const root = String(workspaceRoot || process.cwd());
173
+ for (const relPath of PREFLIGHT_FILES) {
174
+ pushToolLog({ tool: "read", phase: "start", args: { path: relPath }, error: "" });
175
+ const readRes = runToolCall(
176
+ { tool: "read", args: { path: relPath, maxBytes: 12000 } },
177
+ { workspaceRoot: root, cwd: root },
178
+ );
179
+ pushToolLog({
180
+ tool: "read",
181
+ phase: readRes && readRes.ok === false ? "error" : "",
182
+ args: { path: relPath },
183
+ error: readRes && readRes.ok === false ? String(readRes.error || "") : "",
184
+ });
185
+ }
186
+ return buildProjectSnapshot({
187
+ workspaceRoot: root,
188
+ sessionId,
189
+ existing: existingSnapshot,
190
+ });
191
+ }
192
+
193
+ module.exports = {
194
+ PREFLIGHT_FILES,
195
+ buildProjectSnapshot,
196
+ renderProjectSnapshotContext,
197
+ createProjectPreflightContextV2,
198
+ isProjectSnapshotStale,
199
+ invalidateProjectSnapshotIfPathTouched,
200
+ collectCurrentFileHashes,
201
+ };
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ const {
4
+ getIdentitySection,
5
+ } = require("../../agents/prompts/native/identity");
6
+ const {
7
+ getSystemSection,
8
+ } = require("../../agents/prompts/native/system");
9
+ const {
10
+ getDoingTasksSection,
11
+ } = require("../../agents/prompts/native/tasks");
12
+ const {
13
+ getActionsSection,
14
+ } = require("../../agents/prompts/native/actions");
15
+ const {
16
+ getSafetySection,
17
+ } = require("../../agents/prompts/native/safety");
18
+ const {
19
+ getOutputEfficiencySection,
20
+ } = require("../../agents/prompts/native/efficiency");
21
+ const {
22
+ getUfooIntegrationSection,
23
+ } = require("../../agents/prompts/native/ufoo");
24
+ const {
25
+ getSessionStableEnvironmentSection,
26
+ getTurnDynamicEnvironmentSection,
27
+ } = require("../../agents/prompts/native/environment");
28
+ const {
29
+ listUcodeSkills,
30
+ renderSkillsSection,
31
+ } = require("../skills");
32
+ const { hashContent } = require("./artifacts");
33
+
34
+ const PROMPT_VERSION = "native-v4";
35
+
36
+ function buildImmutablePrefix() {
37
+ return [
38
+ `promptVersion: ${PROMPT_VERSION}`,
39
+ getIdentitySection(),
40
+ getSystemSection(),
41
+ getDoingTasksSection(),
42
+ getActionsSection(),
43
+ getSafetySection(),
44
+ getOutputEfficiencySection(),
45
+ [
46
+ "Tool calling grammar:",
47
+ "- Use read, write, edit, bash, artifact_read tools.",
48
+ "- Tool results may reference artifactId; use artifact_read to hydrate raw content.",
49
+ "State commit schema (optional at segment end):",
50
+ '{"stateCommit":{"factsAdd":[],"hypothesesUpdate":[],"decisionsAdd":[],"questionsClose":[],"nextObjective":""},"contextPlan":{"retainRaw":[],"retainRegions":[],"summarize":[],"evict":[],"rehydrateNext":[]}}',
51
+ "Context action schema:",
52
+ '{"type":"execution_segment","objective":"","steps":[],"checkpoint":{"after":[]}}',
53
+ ].join("\n"),
54
+ ].join("\n\n");
55
+ }
56
+
57
+ function buildSkillCatalogVersion(skills = []) {
58
+ const names = (Array.isArray(skills) ? skills : [])
59
+ .map((s) => `${s.name}:${s.path}`)
60
+ .sort()
61
+ .join("|");
62
+ return hashContent(names || "empty");
63
+ }
64
+
65
+ function buildSessionStablePrefix({
66
+ workspaceRoot = "",
67
+ provider = "",
68
+ model = "",
69
+ sessionStableExtras = "",
70
+ } = {}) {
71
+ const root = workspaceRoot || process.cwd();
72
+ const outcome = listUcodeSkills({ workspaceRoot: root });
73
+ const catalogVersion = buildSkillCatalogVersion(outcome.skills);
74
+ const parts = [
75
+ getUfooIntegrationSection(),
76
+ getSessionStableEnvironmentSection({ workspaceRoot: root, provider, model }),
77
+ renderSkillsSection(outcome.skills),
78
+ `skillCatalogVersion: ${catalogVersion}`,
79
+ ];
80
+ if (sessionStableExtras) parts.push(String(sessionStableExtras).trim());
81
+ return parts.filter(Boolean).join("\n\n");
82
+ }
83
+
84
+ function buildLayeredSystemPrompt({
85
+ workspaceRoot = "",
86
+ model = "",
87
+ provider = "",
88
+ appendSystemPrompt = "",
89
+ overrideSystemPrompt = "",
90
+ epochDynamic = "",
91
+ turnDynamic = "",
92
+ sessionStableExtras = "",
93
+ } = {}) {
94
+ if (overrideSystemPrompt) {
95
+ return {
96
+ blocks: [{ layer: "override", text: overrideSystemPrompt, cacheable: false }],
97
+ flatText: overrideSystemPrompt,
98
+ };
99
+ }
100
+
101
+ const immutable = buildImmutablePrefix();
102
+ const sessionStable = buildSessionStablePrefix({
103
+ workspaceRoot,
104
+ provider,
105
+ model,
106
+ sessionStableExtras,
107
+ });
108
+ const epochText = String(epochDynamic || "").trim();
109
+ const turnParts = [
110
+ getTurnDynamicEnvironmentSection({ workspaceRoot: workspaceRoot || process.cwd() }),
111
+ String(turnDynamic || "").trim(),
112
+ String(appendSystemPrompt || "").trim(),
113
+ ].filter(Boolean);
114
+
115
+ const blocks = [
116
+ { layer: "immutable", text: immutable, cacheable: true },
117
+ { layer: "sessionStable", text: sessionStable, cacheable: true },
118
+ ];
119
+ if (epochText) {
120
+ blocks.push({
121
+ layer: "epoch",
122
+ text: epochText,
123
+ cacheable: true,
124
+ });
125
+ }
126
+ if (turnParts.length > 0) {
127
+ blocks.push({
128
+ layer: "turnDynamic",
129
+ text: turnParts.join("\n\n"),
130
+ cacheable: false,
131
+ });
132
+ }
133
+
134
+ return {
135
+ blocks,
136
+ flatText: blocks.map((b) => b.text).filter(Boolean).join("\n\n"),
137
+ };
138
+ }
139
+
140
+ function systemBlocksToAnthropicPayload(blocks = []) {
141
+ const list = (Array.isArray(blocks) ? blocks : []).filter((b) => b && b.text);
142
+ const ANTHROPIC_CACHE_CONTROL = { type: "ephemeral" };
143
+ // Place cache breakpoints on every cacheable layer so Anthropic can reuse
144
+ // Immutable → SessionStable → Epoch prefixes independently. Turn-dynamic
145
+ // never gets cache_control.
146
+ return list.map((block) => {
147
+ const entry = { type: "text", text: block.text };
148
+ if (block.cacheable) entry.cache_control = { ...ANTHROPIC_CACHE_CONTROL };
149
+ return entry;
150
+ });
151
+ }
152
+
153
+ module.exports = {
154
+ PROMPT_VERSION,
155
+ buildImmutablePrefix,
156
+ buildSessionStablePrefix,
157
+ buildLayeredSystemPrompt,
158
+ systemBlocksToAnthropicPayload,
159
+ };