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.
- package/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +339 -24
- package/src/code/commands.js +61 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +698 -0
- package/src/code/context/executionSegment.js +314 -0
- package/src/code/context/featureFlag.js +13 -0
- package/src/code/context/index.js +18 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +159 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +412 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +4 -1
- package/src/code/index.js +6 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +187 -31
- package/src/code/repl.js +36 -32
- package/src/code/sessionStore.js +227 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +65 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +32 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +260 -44
- package/src/ui/format/markdownRenderer.js +215 -72
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +408 -55
- package/src/ui/ink/chatLogModel.js +102 -21
package/src/code/sessionStore.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
|
+
const { isContextV2Enabled } = require("./context/featureFlag");
|
|
5
|
+
const {
|
|
6
|
+
getTranscriptsDir,
|
|
7
|
+
getTranscriptFilePath,
|
|
8
|
+
loadTranscript,
|
|
9
|
+
migrateNlMessagesToTranscript,
|
|
10
|
+
appendTranscriptMessages,
|
|
11
|
+
transcriptEventsToMessages,
|
|
12
|
+
deleteTranscript,
|
|
13
|
+
} = require("./context/transcript");
|
|
14
|
+
const { deleteSessionArtifacts } = require("./context/artifacts");
|
|
15
|
+
const { deleteSessionCommitLog, maybeGcSessionArtifacts } = require("./context/artifactGc");
|
|
16
|
+
const { defaultContextPolicy } = require("./context/assembler");
|
|
17
|
+
const { emptyTaskContract } = require("./context/stateCommit");
|
|
18
|
+
const { emptyWorkingSet } = require("./context/workingSet");
|
|
19
|
+
const { emptyExecutionState } = require("./context/executionSegment");
|
|
4
20
|
|
|
5
21
|
function getSessionsDir(workspaceRoot = process.cwd()) {
|
|
6
22
|
const root = path.resolve(workspaceRoot || process.cwd());
|
|
@@ -40,21 +56,174 @@ function cloneMessages(value = []) {
|
|
|
40
56
|
}
|
|
41
57
|
}
|
|
42
58
|
|
|
59
|
+
function normalizeContextPolicy(value = {}) {
|
|
60
|
+
const defaults = defaultContextPolicy();
|
|
61
|
+
const source = value && typeof value === "object" ? value : {};
|
|
62
|
+
return {
|
|
63
|
+
...defaults,
|
|
64
|
+
...source,
|
|
65
|
+
transcriptWindow: Number.isFinite(source.transcriptWindow)
|
|
66
|
+
? Math.max(1, Math.floor(source.transcriptWindow))
|
|
67
|
+
: defaults.transcriptWindow,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
function buildSessionSnapshot(input = {}) {
|
|
44
72
|
const source = input && typeof input === "object" ? input : {};
|
|
45
73
|
const sessionId = resolveSessionId(source.sessionId);
|
|
46
74
|
const createdAt = String(source.createdAt || "").trim() || toIsoNow();
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
const useV2 = isContextV2Enabled() || Number(source.version) >= 2;
|
|
76
|
+
|
|
77
|
+
const base = {
|
|
49
78
|
sessionId,
|
|
50
79
|
workspaceRoot: String(source.workspaceRoot || process.cwd()).trim() || process.cwd(),
|
|
51
80
|
provider: String(source.provider || "").trim(),
|
|
52
81
|
model: String(source.model || "").trim(),
|
|
53
82
|
context: String(source.context || ""),
|
|
54
|
-
nlMessages: cloneMessages(source.nlMessages),
|
|
55
83
|
createdAt,
|
|
56
84
|
updatedAt: toIsoNow(),
|
|
57
85
|
};
|
|
86
|
+
|
|
87
|
+
if (!useV2) {
|
|
88
|
+
return {
|
|
89
|
+
version: 1,
|
|
90
|
+
...base,
|
|
91
|
+
nlMessages: cloneMessages(source.nlMessages),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
version: 2,
|
|
97
|
+
...base,
|
|
98
|
+
transcript: {
|
|
99
|
+
path: getTranscriptFilePath(base.workspaceRoot, sessionId),
|
|
100
|
+
},
|
|
101
|
+
artifacts: {
|
|
102
|
+
indexPath: path.join(base.workspaceRoot, ".ufoo", "agent", "ucode", "artifacts", sessionId),
|
|
103
|
+
},
|
|
104
|
+
contextPolicy: normalizeContextPolicy(source.contextPolicy),
|
|
105
|
+
summary: String(source.summary || "").trim(),
|
|
106
|
+
projectSnapshot: source.projectSnapshot && typeof source.projectSnapshot === "object"
|
|
107
|
+
? source.projectSnapshot
|
|
108
|
+
: null,
|
|
109
|
+
taskContract: source.taskContract && typeof source.taskContract === "object"
|
|
110
|
+
? source.taskContract
|
|
111
|
+
: emptyTaskContract(),
|
|
112
|
+
stateEpoch: source.stateEpoch && typeof source.stateEpoch === "object"
|
|
113
|
+
? source.stateEpoch
|
|
114
|
+
: null,
|
|
115
|
+
workingSet: Array.isArray(source.workingSet) ? source.workingSet : emptyWorkingSet(),
|
|
116
|
+
executionState: source.executionState && typeof source.executionState === "object"
|
|
117
|
+
? source.executionState
|
|
118
|
+
: emptyExecutionState(),
|
|
119
|
+
activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
|
|
120
|
+
toolCallsSinceCommit: Number.isFinite(source.toolCallsSinceCommit)
|
|
121
|
+
? Math.max(0, Math.floor(source.toolCallsSinceCommit))
|
|
122
|
+
: 0,
|
|
123
|
+
// In-memory compatibility for callers still reading nlMessages
|
|
124
|
+
nlMessages: cloneMessages(source.nlMessages),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hydrateSessionFromDisk(snapshot = {}, workspaceRoot = process.cwd()) {
|
|
129
|
+
const payload = buildSessionSnapshot({
|
|
130
|
+
...snapshot,
|
|
131
|
+
workspaceRoot: workspaceRoot || snapshot.workspaceRoot,
|
|
132
|
+
});
|
|
133
|
+
if (payload.version < 2) return payload;
|
|
134
|
+
|
|
135
|
+
const sessionId = payload.sessionId;
|
|
136
|
+
const transcript = loadTranscript(workspaceRoot, sessionId);
|
|
137
|
+
if (transcript.events.length > 0) {
|
|
138
|
+
const { transcriptEventsToMessages } = require("./context/transcript");
|
|
139
|
+
payload.nlMessages = transcriptEventsToMessages(transcript.events);
|
|
140
|
+
return payload;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(snapshot.nlMessages) && snapshot.nlMessages.length > 0) {
|
|
144
|
+
migrateNlMessagesToTranscript(workspaceRoot, sessionId, snapshot.nlMessages);
|
|
145
|
+
const reloaded = loadTranscript(workspaceRoot, sessionId);
|
|
146
|
+
const { transcriptEventsToMessages } = require("./context/transcript");
|
|
147
|
+
payload.nlMessages = transcriptEventsToMessages(reloaded.events);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return payload;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function syncTranscriptFromNlMessages(workspaceRoot = process.cwd(), sessionId = "", nlMessages = []) {
|
|
154
|
+
const messages = Array.isArray(nlMessages) ? nlMessages : [];
|
|
155
|
+
if (!sessionId || messages.length === 0) return;
|
|
156
|
+
const existing = loadTranscript(workspaceRoot, sessionId);
|
|
157
|
+
if (existing.events.length === 0) {
|
|
158
|
+
migrateNlMessagesToTranscript(workspaceRoot, sessionId, messages);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { matchTranscriptBaseline } = require("./context/assembler");
|
|
162
|
+
const priorMessages = transcriptEventsToMessages(existing.events);
|
|
163
|
+
const baseline = matchTranscriptBaseline(priorMessages, messages);
|
|
164
|
+
if (messages.length > baseline) {
|
|
165
|
+
appendTranscriptMessages(workspaceRoot, sessionId, messages.slice(baseline));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function listSessionSummaries(workspaceRoot = process.cwd(), { limit = 40 } = {}) {
|
|
170
|
+
const dir = getSessionsDir(workspaceRoot);
|
|
171
|
+
const cap = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 40;
|
|
172
|
+
if (!fs.existsSync(dir)) return [];
|
|
173
|
+
let names = [];
|
|
174
|
+
try {
|
|
175
|
+
names = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rows = [];
|
|
181
|
+
for (const name of names) {
|
|
182
|
+
const filePath = path.join(dir, name);
|
|
183
|
+
let stat = null;
|
|
184
|
+
try {
|
|
185
|
+
stat = fs.statSync(filePath);
|
|
186
|
+
} catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
let parsed = null;
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
192
|
+
} catch {
|
|
193
|
+
parsed = null;
|
|
194
|
+
}
|
|
195
|
+
const sessionId = normalizeSessionId(
|
|
196
|
+
(parsed && parsed.sessionId) || name.replace(/\.json$/i, ""),
|
|
197
|
+
);
|
|
198
|
+
if (!sessionId) continue;
|
|
199
|
+
const updatedAt = String(
|
|
200
|
+
(parsed && (parsed.updatedAt || parsed.createdAt))
|
|
201
|
+
|| (stat && stat.mtime && stat.mtime.toISOString())
|
|
202
|
+
|| "",
|
|
203
|
+
).trim();
|
|
204
|
+
const summary = String((parsed && parsed.summary) || "").trim().replace(/\s+/g, " ");
|
|
205
|
+
const model = String((parsed && parsed.model) || "").trim();
|
|
206
|
+
const bits = [
|
|
207
|
+
updatedAt ? updatedAt.slice(0, 19).replace("T", " ") : "",
|
|
208
|
+
model,
|
|
209
|
+
summary ? summary.slice(0, 48) : "",
|
|
210
|
+
].filter(Boolean);
|
|
211
|
+
rows.push({
|
|
212
|
+
id: sessionId,
|
|
213
|
+
cmd: sessionId,
|
|
214
|
+
alias: sessionId,
|
|
215
|
+
desc: bits.join(" · "),
|
|
216
|
+
updatedAt,
|
|
217
|
+
mtimeMs: stat && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : 0,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
rows.sort((left, right) => {
|
|
222
|
+
const byTime = (right.mtimeMs || 0) - (left.mtimeMs || 0);
|
|
223
|
+
if (byTime !== 0) return byTime;
|
|
224
|
+
return String(left.id).localeCompare(String(right.id));
|
|
225
|
+
});
|
|
226
|
+
return rows.slice(0, cap);
|
|
58
227
|
}
|
|
59
228
|
|
|
60
229
|
function getSessionFilePath(workspaceRoot = process.cwd(), sessionId = "") {
|
|
@@ -79,20 +248,19 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
|
|
|
79
248
|
};
|
|
80
249
|
}
|
|
81
250
|
|
|
251
|
+
const toWrite = { ...payload };
|
|
252
|
+
if (toWrite.version >= 2) {
|
|
253
|
+
syncTranscriptFromNlMessages(normalizedRoot, payload.sessionId, payload.nlMessages);
|
|
254
|
+
// nlMessages live in transcript.jsonl; keep session.json light
|
|
255
|
+
delete toWrite.nlMessages;
|
|
256
|
+
}
|
|
257
|
+
|
|
82
258
|
try {
|
|
83
259
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
-
|
|
85
|
-
// corrupted session JSON behind.
|
|
260
|
+
fs.mkdirSync(getTranscriptsDir(normalizedRoot), { recursive: true });
|
|
86
261
|
const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
|
|
87
|
-
fs.writeFileSync(tmpFile, `${JSON.stringify(
|
|
262
|
+
fs.writeFileSync(tmpFile, `${JSON.stringify(toWrite, null, 2)}\n`, "utf8");
|
|
88
263
|
fs.renameSync(tmpFile, filePath);
|
|
89
|
-
return {
|
|
90
|
-
ok: true,
|
|
91
|
-
error: "",
|
|
92
|
-
sessionId: payload.sessionId,
|
|
93
|
-
filePath,
|
|
94
|
-
snapshot: payload,
|
|
95
|
-
};
|
|
96
264
|
} catch (err) {
|
|
97
265
|
return {
|
|
98
266
|
ok: false,
|
|
@@ -101,6 +269,26 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
|
|
|
101
269
|
filePath,
|
|
102
270
|
};
|
|
103
271
|
}
|
|
272
|
+
|
|
273
|
+
// Artifact GC is throttled (default 2m) so long sessions do not accumulate
|
|
274
|
+
// unbounded tool result files between explicit maintenance runs.
|
|
275
|
+
let artifactGc = null;
|
|
276
|
+
try {
|
|
277
|
+
artifactGc = maybeGcSessionArtifacts(normalizedRoot, payload.sessionId, {
|
|
278
|
+
...(snapshot.artifactGc && typeof snapshot.artifactGc === "object" ? snapshot.artifactGc : {}),
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
artifactGc = { ok: false, error: "artifact gc failed", skipped: true };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
error: "",
|
|
287
|
+
sessionId: payload.sessionId,
|
|
288
|
+
filePath,
|
|
289
|
+
snapshot: payload,
|
|
290
|
+
artifactGc,
|
|
291
|
+
};
|
|
104
292
|
}
|
|
105
293
|
|
|
106
294
|
function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
|
|
@@ -130,12 +318,13 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
|
|
|
130
318
|
try {
|
|
131
319
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
132
320
|
const parsed = JSON.parse(raw);
|
|
133
|
-
const snapshot =
|
|
321
|
+
const snapshot = hydrateSessionFromDisk({
|
|
134
322
|
...parsed,
|
|
135
323
|
sessionId: normalizedId,
|
|
136
324
|
workspaceRoot: normalizedRoot,
|
|
137
325
|
createdAt: parsed && parsed.createdAt ? parsed.createdAt : "",
|
|
138
|
-
|
|
326
|
+
nlMessages: parsed && parsed.nlMessages ? parsed.nlMessages : [],
|
|
327
|
+
}, normalizedRoot);
|
|
139
328
|
return {
|
|
140
329
|
ok: true,
|
|
141
330
|
error: "",
|
|
@@ -154,13 +343,36 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
|
|
|
154
343
|
}
|
|
155
344
|
}
|
|
156
345
|
|
|
346
|
+
function deleteSessionData(workspaceRoot = process.cwd(), sessionId = "") {
|
|
347
|
+
const normalizedId = normalizeSessionId(sessionId);
|
|
348
|
+
if (!normalizedId) return { ok: false, error: "invalid session id" };
|
|
349
|
+
const filePath = getSessionFilePath(workspaceRoot, normalizedId);
|
|
350
|
+
try {
|
|
351
|
+
if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
352
|
+
deleteTranscript(workspaceRoot, normalizedId);
|
|
353
|
+
deleteSessionArtifacts(workspaceRoot, normalizedId);
|
|
354
|
+
deleteSessionCommitLog(workspaceRoot, normalizedId);
|
|
355
|
+
return { ok: true, error: "" };
|
|
356
|
+
} catch (err) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
error: err && err.message ? err.message : "failed to delete session data",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
157
364
|
module.exports = {
|
|
158
365
|
getSessionsDir,
|
|
366
|
+
getTranscriptsDir,
|
|
367
|
+
getTranscriptFilePath,
|
|
159
368
|
normalizeSessionId,
|
|
160
369
|
createSessionId,
|
|
161
370
|
resolveSessionId,
|
|
162
371
|
buildSessionSnapshot,
|
|
372
|
+
hydrateSessionFromDisk,
|
|
163
373
|
getSessionFilePath,
|
|
164
374
|
saveSessionSnapshot,
|
|
165
375
|
loadSessionSnapshot,
|
|
376
|
+
listSessionSummaries,
|
|
377
|
+
deleteSessionData,
|
|
166
378
|
};
|
package/src/code/skills/index.js
CHANGED
|
@@ -9,6 +9,12 @@ const {
|
|
|
9
9
|
const {
|
|
10
10
|
buildSkillInjections,
|
|
11
11
|
} = require("./injection");
|
|
12
|
+
const {
|
|
13
|
+
buildSkillManifest,
|
|
14
|
+
buildSkillManifests,
|
|
15
|
+
renderSkillManifestSection,
|
|
16
|
+
renderActiveSkillBlock,
|
|
17
|
+
} = require("./manifest");
|
|
12
18
|
|
|
13
19
|
function showSkill({ name = "", workspaceRoot = process.cwd(), asJson = false } = {}) {
|
|
14
20
|
const outcome = listUcodeSkills({ workspaceRoot });
|
|
@@ -70,5 +76,9 @@ module.exports = {
|
|
|
70
76
|
renderSkillsSection,
|
|
71
77
|
formatSkillsList,
|
|
72
78
|
buildSkillInjections,
|
|
79
|
+
buildSkillManifest,
|
|
80
|
+
buildSkillManifests,
|
|
81
|
+
renderSkillManifestSection,
|
|
82
|
+
renderActiveSkillBlock,
|
|
73
83
|
showSkill,
|
|
74
84
|
};
|
|
@@ -5,6 +5,10 @@ const {
|
|
|
5
5
|
findSkillsByName,
|
|
6
6
|
listUcodeSkills,
|
|
7
7
|
} = require("./loader");
|
|
8
|
+
const {
|
|
9
|
+
buildSkillManifest,
|
|
10
|
+
renderActiveSkillBlock,
|
|
11
|
+
} = require("./manifest");
|
|
8
12
|
|
|
9
13
|
function markdownSkillLinks(prompt = "") {
|
|
10
14
|
const text = String(prompt || "");
|
|
@@ -71,9 +75,41 @@ function sanitizeSkillContent(content = "") {
|
|
|
71
75
|
return text;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
function readSkillBlock(skill) {
|
|
78
|
+
function readSkillBlock(skill, options = {}) {
|
|
75
79
|
const content = sanitizeSkillContent(fs.readFileSync(skill.path, "utf8"));
|
|
76
|
-
|
|
80
|
+
const useActive = options.useActiveSkillTag === true;
|
|
81
|
+
const tag = useActive ? "active_skill" : "skill";
|
|
82
|
+
const bodyArtifactId = String(options.bodyArtifactId || "").trim();
|
|
83
|
+
const header = [
|
|
84
|
+
`<${tag}>`,
|
|
85
|
+
`<name>${skill.name}</name>`,
|
|
86
|
+
`<path>${String(skill.path).replace(/\\/g, "/")}</path>`,
|
|
87
|
+
bodyArtifactId ? `<bodyArtifactId>${bodyArtifactId}</bodyArtifactId>` : "",
|
|
88
|
+
].filter(Boolean).join("\n");
|
|
89
|
+
return `${header}\n${content}\n</${tag}>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function persistSkillBodyArtifact(skill = {}, content = "", options = {}) {
|
|
93
|
+
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
94
|
+
const sessionId = String(options.sessionId || "").trim();
|
|
95
|
+
if (!sessionId) return "";
|
|
96
|
+
try {
|
|
97
|
+
const { saveArtifact } = require("../context/artifacts");
|
|
98
|
+
const saved = saveArtifact(workspaceRoot, sessionId, {
|
|
99
|
+
type: "skill_body",
|
|
100
|
+
tool: "skill",
|
|
101
|
+
source: skill.path || "",
|
|
102
|
+
args: { skill: skill.name || "" },
|
|
103
|
+
raw: { ok: true, path: skill.path || "", content: String(content || "") },
|
|
104
|
+
summary: `skill body ${skill.name || ""}`,
|
|
105
|
+
createdBy: "skill_injection",
|
|
106
|
+
});
|
|
107
|
+
return saved && saved.artifact && saved.artifact.artifactId
|
|
108
|
+
? saved.artifact.artifactId
|
|
109
|
+
: "";
|
|
110
|
+
} catch {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
77
113
|
}
|
|
78
114
|
|
|
79
115
|
function buildSkillInjections({
|
|
@@ -81,6 +117,9 @@ function buildSkillInjections({
|
|
|
81
117
|
workspaceRoot = process.cwd(),
|
|
82
118
|
skillsOutcome = null,
|
|
83
119
|
loadSkills = listUcodeSkills,
|
|
120
|
+
sessionId = "",
|
|
121
|
+
persistBodies = false,
|
|
122
|
+
useActiveSkillTag = false,
|
|
84
123
|
} = {}) {
|
|
85
124
|
const outcome = skillsOutcome || loadSkills({ workspaceRoot });
|
|
86
125
|
const skills = Array.isArray(outcome.skills) ? outcome.skills : [];
|
|
@@ -111,9 +150,28 @@ function buildSkillInjections({
|
|
|
111
150
|
}
|
|
112
151
|
|
|
113
152
|
const blocks = [];
|
|
153
|
+
const manifests = [];
|
|
154
|
+
const activeSkills = [];
|
|
114
155
|
for (const skill of selected.values()) {
|
|
115
156
|
try {
|
|
116
|
-
|
|
157
|
+
const rawContent = fs.readFileSync(skill.path, "utf8");
|
|
158
|
+
const content = sanitizeSkillContent(rawContent);
|
|
159
|
+
let bodyArtifactId = "";
|
|
160
|
+
if (persistBodies && sessionId) {
|
|
161
|
+
bodyArtifactId = persistSkillBodyArtifact(skill, content, { workspaceRoot, sessionId });
|
|
162
|
+
}
|
|
163
|
+
const manifest = buildSkillManifest(skill, { bodyArtifactId });
|
|
164
|
+
manifests.push(manifest);
|
|
165
|
+
activeSkills.push({
|
|
166
|
+
name: skill.name,
|
|
167
|
+
path: skill.path,
|
|
168
|
+
bodyArtifactId,
|
|
169
|
+
});
|
|
170
|
+
if (useActiveSkillTag) {
|
|
171
|
+
blocks.push(renderActiveSkillBlock(manifest, content));
|
|
172
|
+
} else {
|
|
173
|
+
blocks.push(readSkillBlock(skill, { bodyArtifactId, useActiveSkillTag: false }));
|
|
174
|
+
}
|
|
117
175
|
} catch (err) {
|
|
118
176
|
warnings.push(`failed to read skill ${skill.path}: ${err && err.message ? err.message : "read failed"}`);
|
|
119
177
|
}
|
|
@@ -121,6 +179,8 @@ function buildSkillInjections({
|
|
|
121
179
|
|
|
122
180
|
return {
|
|
123
181
|
blocks,
|
|
182
|
+
manifests,
|
|
183
|
+
activeSkills,
|
|
124
184
|
warnings,
|
|
125
185
|
skills,
|
|
126
186
|
errors: Array.isArray(outcome.errors) ? outcome.errors : [],
|
|
@@ -132,5 +192,7 @@ module.exports = {
|
|
|
132
192
|
markdownSkillLinks,
|
|
133
193
|
resolveSkillLinkTarget,
|
|
134
194
|
findSkillByPath,
|
|
195
|
+
readSkillBlock,
|
|
196
|
+
persistSkillBodyArtifact,
|
|
135
197
|
buildSkillInjections,
|
|
136
198
|
};
|
|
@@ -98,11 +98,32 @@ function parseSkillFile(filePath, rootInfo = {}) {
|
|
|
98
98
|
|| data.shortDescription
|
|
99
99
|
|| ""
|
|
100
100
|
).trim();
|
|
101
|
+
const workflowSummary = String(
|
|
102
|
+
data.workflowSummary
|
|
103
|
+
|| data["workflow-summary"]
|
|
104
|
+
|| metadata.workflowSummary
|
|
105
|
+
|| metadata["workflow-summary"]
|
|
106
|
+
|| shortDescription
|
|
107
|
+
|| ""
|
|
108
|
+
).trim();
|
|
109
|
+
const triggersRaw = data.triggers != null
|
|
110
|
+
? data.triggers
|
|
111
|
+
: (data.trigger != null
|
|
112
|
+
? data.trigger
|
|
113
|
+
: (metadata.triggers != null ? metadata.triggers : metadata.trigger));
|
|
114
|
+
let triggers = [];
|
|
115
|
+
if (Array.isArray(triggersRaw)) {
|
|
116
|
+
triggers = triggersRaw.map((item) => String(item || "").trim()).filter(Boolean);
|
|
117
|
+
} else if (typeof triggersRaw === "string" && triggersRaw.trim()) {
|
|
118
|
+
triggers = triggersRaw.split(/[,|]/).map((item) => item.trim()).filter(Boolean);
|
|
119
|
+
}
|
|
101
120
|
|
|
102
121
|
return {
|
|
103
122
|
name,
|
|
104
123
|
description,
|
|
105
124
|
shortDescription,
|
|
125
|
+
workflowSummary,
|
|
126
|
+
triggers,
|
|
106
127
|
path: skillPath,
|
|
107
128
|
dir,
|
|
108
129
|
scope: rootInfo.scope || "repo",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function asStringList(value) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
if (typeof value === "string" && value.trim()) {
|
|
8
|
+
return value.split(/[,|]/).map((item) => item.trim()).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildSkillManifest(skill = {}, options = {}) {
|
|
14
|
+
const source = skill && typeof skill === "object" ? skill : {};
|
|
15
|
+
const bodyArtifactId = String(options.bodyArtifactId || source.bodyArtifactId || "").trim();
|
|
16
|
+
const triggers = Array.isArray(source.triggers) && source.triggers.length > 0
|
|
17
|
+
? source.triggers.map(String).filter(Boolean)
|
|
18
|
+
: asStringList(source.trigger);
|
|
19
|
+
const workflowSummary = String(
|
|
20
|
+
source.workflowSummary
|
|
21
|
+
|| source.shortDescription
|
|
22
|
+
|| source.description
|
|
23
|
+
|| "",
|
|
24
|
+
).trim().slice(0, 400);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name: String(source.name || "").trim(),
|
|
28
|
+
description: String(source.description || "").trim(),
|
|
29
|
+
shortDescription: String(source.shortDescription || "").trim(),
|
|
30
|
+
triggers,
|
|
31
|
+
workflowSummary,
|
|
32
|
+
path: String(source.path || "").replace(/\\/g, "/"),
|
|
33
|
+
scope: String(source.scope || "").trim(),
|
|
34
|
+
bodyArtifactId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildSkillManifests(skills = [], options = {}) {
|
|
39
|
+
return (Array.isArray(skills) ? skills : [])
|
|
40
|
+
.filter((skill) => skill && skill.enabled !== false)
|
|
41
|
+
.map((skill) => buildSkillManifest(skill, options));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderSkillManifestSection(manifests = []) {
|
|
45
|
+
const list = Array.isArray(manifests) ? manifests.filter((m) => m && m.name) : [];
|
|
46
|
+
if (list.length === 0) return "";
|
|
47
|
+
|
|
48
|
+
const lines = [
|
|
49
|
+
"## Skill Manifests",
|
|
50
|
+
"Lightweight skill cards for selection. Full skill body loads only when a skill is explicitly activated.",
|
|
51
|
+
];
|
|
52
|
+
for (const manifest of list) {
|
|
53
|
+
lines.push(`### ${manifest.name}`);
|
|
54
|
+
if (manifest.workflowSummary) lines.push(`- Workflow: ${manifest.workflowSummary}`);
|
|
55
|
+
if (manifest.triggers && manifest.triggers.length > 0) {
|
|
56
|
+
lines.push(`- Triggers: ${manifest.triggers.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
if (manifest.bodyArtifactId) {
|
|
59
|
+
lines.push(`- Body artifact: artifact://${manifest.bodyArtifactId}`);
|
|
60
|
+
} else if (manifest.path) {
|
|
61
|
+
lines.push(`- Path: ${manifest.path}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderActiveSkillBlock(manifest = {}, body = "") {
|
|
68
|
+
const name = String(manifest.name || "").trim() || "skill";
|
|
69
|
+
const pathText = String(manifest.path || "").replace(/\\/g, "/");
|
|
70
|
+
const bodyArtifactId = String(manifest.bodyArtifactId || "").trim();
|
|
71
|
+
const header = [
|
|
72
|
+
`<active_skill>`,
|
|
73
|
+
`<name>${name}</name>`,
|
|
74
|
+
pathText ? `<path>${pathText}</path>` : "",
|
|
75
|
+
bodyArtifactId ? `<bodyArtifactId>${bodyArtifactId}</bodyArtifactId>` : "",
|
|
76
|
+
manifest.workflowSummary ? `<workflowSummary>${manifest.workflowSummary}</workflowSummary>` : "",
|
|
77
|
+
].filter(Boolean).join("\n");
|
|
78
|
+
return `${header}\n${String(body || "").trim()}\n</active_skill>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
asStringList,
|
|
83
|
+
buildSkillManifest,
|
|
84
|
+
buildSkillManifests,
|
|
85
|
+
renderSkillManifestSection,
|
|
86
|
+
renderActiveSkillBlock,
|
|
87
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
const {
|
|
2
|
+
buildSkillManifests,
|
|
3
|
+
renderSkillManifestSection,
|
|
4
|
+
} = require("./manifest");
|
|
5
|
+
|
|
6
|
+
function renderSkillsSection(skills = [], options = {}) {
|
|
2
7
|
const list = (Array.isArray(skills) ? skills : []).filter((skill) => skill && skill.enabled !== false);
|
|
3
8
|
if (list.length === 0) return "";
|
|
4
9
|
|
|
@@ -19,6 +24,15 @@ function renderSkillsSection(skills = []) {
|
|
|
19
24
|
lines.push("- When a skill is selected, read only the specific skill body and nearby referenced files needed for the task.");
|
|
20
25
|
lines.push("- If a skill is ambiguous, missing, or unreadable, say so briefly and continue with the best fallback.");
|
|
21
26
|
|
|
27
|
+
if (options.includeManifests !== false) {
|
|
28
|
+
const manifests = buildSkillManifests(list);
|
|
29
|
+
const manifestSection = renderSkillManifestSection(manifests);
|
|
30
|
+
if (manifestSection) {
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push(manifestSection);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
return lines.join("\n");
|
|
23
37
|
}
|
|
24
38
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { runNativeAgentTask } = require("./nativeRunner");
|
|
7
|
+
const { isContextV2Enabled } = require("./context/featureFlag");
|
|
8
|
+
const { assembleModelContext, recordToolCallInSession } = require("./context/assembler");
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Decompose a bug fix task into manageable steps
|
|
@@ -132,10 +134,14 @@ async function runDecomposedTask({
|
|
|
132
134
|
systemPrompt,
|
|
133
135
|
messages = [],
|
|
134
136
|
sessionId = "",
|
|
137
|
+
state = null,
|
|
138
|
+
contextV2 = false,
|
|
139
|
+
systemBlocks = null,
|
|
135
140
|
}) {
|
|
136
141
|
const steps = decomposeBugFixTask(task);
|
|
137
142
|
const results = [];
|
|
138
143
|
let aborted = false;
|
|
144
|
+
const useV2 = Boolean(contextV2 || isContextV2Enabled());
|
|
139
145
|
|
|
140
146
|
// Check if already aborted
|
|
141
147
|
if (signal && signal.aborted) {
|
|
@@ -165,19 +171,43 @@ async function runDecomposedTask({
|
|
|
165
171
|
try {
|
|
166
172
|
// Run the step with its own timeout
|
|
167
173
|
const stepPrompt = buildStepPrompt(step, results);
|
|
174
|
+
let stepMessages = messages;
|
|
175
|
+
let stepSystemPrompt = systemPrompt;
|
|
176
|
+
let stepSystemBlocks = systemBlocks;
|
|
177
|
+
if (useV2 && state) {
|
|
178
|
+
const assembled = assembleModelContext(state, {
|
|
179
|
+
workspaceRoot,
|
|
180
|
+
model,
|
|
181
|
+
provider,
|
|
182
|
+
turnDynamic: stepPrompt,
|
|
183
|
+
});
|
|
184
|
+
stepMessages = assembled.messages;
|
|
185
|
+
stepSystemPrompt = assembled.systemPrompt;
|
|
186
|
+
stepSystemBlocks = assembled.systemBlocks;
|
|
187
|
+
}
|
|
168
188
|
const stepResult = await runNativeAgentTask({
|
|
169
189
|
workspaceRoot,
|
|
170
190
|
provider,
|
|
171
191
|
model,
|
|
172
192
|
prompt: stepPrompt,
|
|
173
|
-
systemPrompt,
|
|
174
|
-
|
|
193
|
+
systemPrompt: stepSystemPrompt,
|
|
194
|
+
systemBlocks: stepSystemBlocks,
|
|
195
|
+
messages: stepMessages,
|
|
175
196
|
sessionId,
|
|
176
197
|
timeoutMs: step.timeoutMs,
|
|
177
198
|
onToolEvent,
|
|
178
199
|
signal,
|
|
200
|
+
contextV2: useV2,
|
|
201
|
+
onArtifactPersisted: useV2 && state
|
|
202
|
+
? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
|
|
203
|
+
: null,
|
|
179
204
|
});
|
|
180
205
|
|
|
206
|
+
if (useV2 && state && stepResult && Array.isArray(stepResult.messages)) {
|
|
207
|
+
const { syncMessagesToTranscript } = require("./context/assembler");
|
|
208
|
+
syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
|
|
209
|
+
}
|
|
210
|
+
|
|
181
211
|
results.push({
|
|
182
212
|
step: step.id,
|
|
183
213
|
name: step.name,
|