u-foo 2.5.14 → 3.0.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.
- package/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -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 +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -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 +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
package/src/code/sessionStore.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
|
+
const {
|
|
5
|
+
getTranscriptsDir,
|
|
6
|
+
getTranscriptFilePath,
|
|
7
|
+
loadTranscript,
|
|
8
|
+
migrateNlMessagesToTranscript,
|
|
9
|
+
appendTranscriptMessages,
|
|
10
|
+
transcriptEventsToMessages,
|
|
11
|
+
deleteTranscript,
|
|
12
|
+
} = require("./context/transcript");
|
|
13
|
+
const { deleteSessionArtifacts } = require("./context/artifacts");
|
|
14
|
+
const { deleteSessionCommitLog, maybeGcSessionArtifacts } = require("./context/artifactGc");
|
|
15
|
+
const { defaultContextPolicy } = require("./context/assembler");
|
|
16
|
+
const { emptyTaskContract } = require("./context/stateCommit");
|
|
17
|
+
const { emptyWorkingSet } = require("./context/workingSet");
|
|
18
|
+
const { emptyExecutionState } = require("./context/executionSegment");
|
|
4
19
|
|
|
5
20
|
function getSessionsDir(workspaceRoot = process.cwd()) {
|
|
6
21
|
const root = path.resolve(workspaceRoot || process.cwd());
|
|
@@ -40,21 +55,165 @@ function cloneMessages(value = []) {
|
|
|
40
55
|
}
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
function normalizeContextPolicy(value = {}) {
|
|
59
|
+
const defaults = defaultContextPolicy();
|
|
60
|
+
const source = value && typeof value === "object" ? value : {};
|
|
61
|
+
return {
|
|
62
|
+
...defaults,
|
|
63
|
+
...source,
|
|
64
|
+
transcriptWindow: Number.isFinite(source.transcriptWindow)
|
|
65
|
+
? Math.max(1, Math.floor(source.transcriptWindow))
|
|
66
|
+
: defaults.transcriptWindow,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
function buildSessionSnapshot(input = {}) {
|
|
44
71
|
const source = input && typeof input === "object" ? input : {};
|
|
45
72
|
const sessionId = resolveSessionId(source.sessionId);
|
|
46
73
|
const createdAt = String(source.createdAt || "").trim() || toIsoNow();
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
|
|
75
|
+
const base = {
|
|
49
76
|
sessionId,
|
|
50
77
|
workspaceRoot: String(source.workspaceRoot || process.cwd()).trim() || process.cwd(),
|
|
51
78
|
provider: String(source.provider || "").trim(),
|
|
52
79
|
model: String(source.model || "").trim(),
|
|
53
80
|
context: String(source.context || ""),
|
|
54
|
-
nlMessages: cloneMessages(source.nlMessages),
|
|
55
81
|
createdAt,
|
|
56
82
|
updatedAt: toIsoNow(),
|
|
57
83
|
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
version: 2,
|
|
87
|
+
...base,
|
|
88
|
+
transcript: {
|
|
89
|
+
path: getTranscriptFilePath(base.workspaceRoot, sessionId),
|
|
90
|
+
},
|
|
91
|
+
artifacts: {
|
|
92
|
+
indexPath: path.join(base.workspaceRoot, ".ufoo", "agent", "ucode", "artifacts", sessionId),
|
|
93
|
+
},
|
|
94
|
+
contextPolicy: normalizeContextPolicy(source.contextPolicy),
|
|
95
|
+
summary: String(source.summary || "").trim(),
|
|
96
|
+
projectSnapshot: source.projectSnapshot && typeof source.projectSnapshot === "object"
|
|
97
|
+
? source.projectSnapshot
|
|
98
|
+
: null,
|
|
99
|
+
taskContract: source.taskContract && typeof source.taskContract === "object"
|
|
100
|
+
? source.taskContract
|
|
101
|
+
: emptyTaskContract(),
|
|
102
|
+
stateEpoch: source.stateEpoch && typeof source.stateEpoch === "object"
|
|
103
|
+
? source.stateEpoch
|
|
104
|
+
: null,
|
|
105
|
+
workingSet: Array.isArray(source.workingSet) ? source.workingSet : emptyWorkingSet(),
|
|
106
|
+
executionState: source.executionState && typeof source.executionState === "object"
|
|
107
|
+
? source.executionState
|
|
108
|
+
: emptyExecutionState(),
|
|
109
|
+
activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
|
|
110
|
+
toolCallsSinceCommit: Number.isFinite(source.toolCallsSinceCommit)
|
|
111
|
+
? Math.max(0, Math.floor(source.toolCallsSinceCommit))
|
|
112
|
+
: 0,
|
|
113
|
+
// In-memory compatibility for callers still reading nlMessages
|
|
114
|
+
nlMessages: cloneMessages(source.nlMessages),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hydrateSessionFromDisk(snapshot = {}, workspaceRoot = process.cwd()) {
|
|
119
|
+
const payload = buildSessionSnapshot({
|
|
120
|
+
...snapshot,
|
|
121
|
+
workspaceRoot: workspaceRoot || snapshot.workspaceRoot,
|
|
122
|
+
});
|
|
123
|
+
if (payload.version < 2) return payload;
|
|
124
|
+
|
|
125
|
+
const sessionId = payload.sessionId;
|
|
126
|
+
const transcript = loadTranscript(workspaceRoot, sessionId);
|
|
127
|
+
if (transcript.events.length > 0) {
|
|
128
|
+
const { transcriptEventsToMessages } = require("./context/transcript");
|
|
129
|
+
payload.nlMessages = transcriptEventsToMessages(transcript.events);
|
|
130
|
+
return payload;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(snapshot.nlMessages) && snapshot.nlMessages.length > 0) {
|
|
134
|
+
migrateNlMessagesToTranscript(workspaceRoot, sessionId, snapshot.nlMessages);
|
|
135
|
+
const reloaded = loadTranscript(workspaceRoot, sessionId);
|
|
136
|
+
const { transcriptEventsToMessages } = require("./context/transcript");
|
|
137
|
+
payload.nlMessages = transcriptEventsToMessages(reloaded.events);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return payload;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function syncTranscriptFromNlMessages(workspaceRoot = process.cwd(), sessionId = "", nlMessages = []) {
|
|
144
|
+
const messages = Array.isArray(nlMessages) ? nlMessages : [];
|
|
145
|
+
if (!sessionId || messages.length === 0) return;
|
|
146
|
+
const existing = loadTranscript(workspaceRoot, sessionId);
|
|
147
|
+
if (existing.events.length === 0) {
|
|
148
|
+
migrateNlMessagesToTranscript(workspaceRoot, sessionId, messages);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const { matchTranscriptBaseline } = require("./context/assembler");
|
|
152
|
+
const priorMessages = transcriptEventsToMessages(existing.events);
|
|
153
|
+
const baseline = matchTranscriptBaseline(priorMessages, messages);
|
|
154
|
+
if (messages.length > baseline) {
|
|
155
|
+
appendTranscriptMessages(workspaceRoot, sessionId, messages.slice(baseline));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function listSessionSummaries(workspaceRoot = process.cwd(), { limit = 40 } = {}) {
|
|
160
|
+
const dir = getSessionsDir(workspaceRoot);
|
|
161
|
+
const cap = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 40;
|
|
162
|
+
if (!fs.existsSync(dir)) return [];
|
|
163
|
+
let names = [];
|
|
164
|
+
try {
|
|
165
|
+
names = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rows = [];
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
const filePath = path.join(dir, name);
|
|
173
|
+
let stat = null;
|
|
174
|
+
try {
|
|
175
|
+
stat = fs.statSync(filePath);
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
let parsed = null;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
182
|
+
} catch {
|
|
183
|
+
parsed = null;
|
|
184
|
+
}
|
|
185
|
+
const sessionId = normalizeSessionId(
|
|
186
|
+
(parsed && parsed.sessionId) || name.replace(/\.json$/i, ""),
|
|
187
|
+
);
|
|
188
|
+
if (!sessionId) continue;
|
|
189
|
+
const updatedAt = String(
|
|
190
|
+
(parsed && (parsed.updatedAt || parsed.createdAt))
|
|
191
|
+
|| (stat && stat.mtime && stat.mtime.toISOString())
|
|
192
|
+
|| "",
|
|
193
|
+
).trim();
|
|
194
|
+
const summary = String((parsed && parsed.summary) || "").trim().replace(/\s+/g, " ");
|
|
195
|
+
const model = String((parsed && parsed.model) || "").trim();
|
|
196
|
+
const bits = [
|
|
197
|
+
updatedAt ? updatedAt.slice(0, 19).replace("T", " ") : "",
|
|
198
|
+
model,
|
|
199
|
+
summary ? summary.slice(0, 48) : "",
|
|
200
|
+
].filter(Boolean);
|
|
201
|
+
rows.push({
|
|
202
|
+
id: sessionId,
|
|
203
|
+
cmd: sessionId,
|
|
204
|
+
alias: sessionId,
|
|
205
|
+
desc: bits.join(" · "),
|
|
206
|
+
updatedAt,
|
|
207
|
+
mtimeMs: stat && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : 0,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
rows.sort((left, right) => {
|
|
212
|
+
const byTime = (right.mtimeMs || 0) - (left.mtimeMs || 0);
|
|
213
|
+
if (byTime !== 0) return byTime;
|
|
214
|
+
return String(left.id).localeCompare(String(right.id));
|
|
215
|
+
});
|
|
216
|
+
return rows.slice(0, cap);
|
|
58
217
|
}
|
|
59
218
|
|
|
60
219
|
function getSessionFilePath(workspaceRoot = process.cwd(), sessionId = "") {
|
|
@@ -79,20 +238,19 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
|
|
|
79
238
|
};
|
|
80
239
|
}
|
|
81
240
|
|
|
241
|
+
const toWrite = { ...payload };
|
|
242
|
+
if (toWrite.version >= 2) {
|
|
243
|
+
syncTranscriptFromNlMessages(normalizedRoot, payload.sessionId, payload.nlMessages);
|
|
244
|
+
// nlMessages live in transcript.jsonl; keep session.json light
|
|
245
|
+
delete toWrite.nlMessages;
|
|
246
|
+
}
|
|
247
|
+
|
|
82
248
|
try {
|
|
83
249
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
-
|
|
85
|
-
// corrupted session JSON behind.
|
|
250
|
+
fs.mkdirSync(getTranscriptsDir(normalizedRoot), { recursive: true });
|
|
86
251
|
const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
|
|
87
|
-
fs.writeFileSync(tmpFile, `${JSON.stringify(
|
|
252
|
+
fs.writeFileSync(tmpFile, `${JSON.stringify(toWrite, null, 2)}\n`, "utf8");
|
|
88
253
|
fs.renameSync(tmpFile, filePath);
|
|
89
|
-
return {
|
|
90
|
-
ok: true,
|
|
91
|
-
error: "",
|
|
92
|
-
sessionId: payload.sessionId,
|
|
93
|
-
filePath,
|
|
94
|
-
snapshot: payload,
|
|
95
|
-
};
|
|
96
254
|
} catch (err) {
|
|
97
255
|
return {
|
|
98
256
|
ok: false,
|
|
@@ -101,6 +259,26 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
|
|
|
101
259
|
filePath,
|
|
102
260
|
};
|
|
103
261
|
}
|
|
262
|
+
|
|
263
|
+
// Artifact GC is throttled (default 2m) so long sessions do not accumulate
|
|
264
|
+
// unbounded tool result files between explicit maintenance runs.
|
|
265
|
+
let artifactGc = null;
|
|
266
|
+
try {
|
|
267
|
+
artifactGc = maybeGcSessionArtifacts(normalizedRoot, payload.sessionId, {
|
|
268
|
+
...(snapshot.artifactGc && typeof snapshot.artifactGc === "object" ? snapshot.artifactGc : {}),
|
|
269
|
+
});
|
|
270
|
+
} catch {
|
|
271
|
+
artifactGc = { ok: false, error: "artifact gc failed", skipped: true };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
error: "",
|
|
277
|
+
sessionId: payload.sessionId,
|
|
278
|
+
filePath,
|
|
279
|
+
snapshot: payload,
|
|
280
|
+
artifactGc,
|
|
281
|
+
};
|
|
104
282
|
}
|
|
105
283
|
|
|
106
284
|
function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
|
|
@@ -130,12 +308,13 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
|
|
|
130
308
|
try {
|
|
131
309
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
132
310
|
const parsed = JSON.parse(raw);
|
|
133
|
-
const snapshot =
|
|
311
|
+
const snapshot = hydrateSessionFromDisk({
|
|
134
312
|
...parsed,
|
|
135
313
|
sessionId: normalizedId,
|
|
136
314
|
workspaceRoot: normalizedRoot,
|
|
137
315
|
createdAt: parsed && parsed.createdAt ? parsed.createdAt : "",
|
|
138
|
-
|
|
316
|
+
nlMessages: parsed && parsed.nlMessages ? parsed.nlMessages : [],
|
|
317
|
+
}, normalizedRoot);
|
|
139
318
|
return {
|
|
140
319
|
ok: true,
|
|
141
320
|
error: "",
|
|
@@ -154,13 +333,36 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
|
|
|
154
333
|
}
|
|
155
334
|
}
|
|
156
335
|
|
|
336
|
+
function deleteSessionData(workspaceRoot = process.cwd(), sessionId = "") {
|
|
337
|
+
const normalizedId = normalizeSessionId(sessionId);
|
|
338
|
+
if (!normalizedId) return { ok: false, error: "invalid session id" };
|
|
339
|
+
const filePath = getSessionFilePath(workspaceRoot, normalizedId);
|
|
340
|
+
try {
|
|
341
|
+
if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
342
|
+
deleteTranscript(workspaceRoot, normalizedId);
|
|
343
|
+
deleteSessionArtifacts(workspaceRoot, normalizedId);
|
|
344
|
+
deleteSessionCommitLog(workspaceRoot, normalizedId);
|
|
345
|
+
return { ok: true, error: "" };
|
|
346
|
+
} catch (err) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
error: err && err.message ? err.message : "failed to delete session data",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
157
354
|
module.exports = {
|
|
158
355
|
getSessionsDir,
|
|
356
|
+
getTranscriptsDir,
|
|
357
|
+
getTranscriptFilePath,
|
|
159
358
|
normalizeSessionId,
|
|
160
359
|
createSessionId,
|
|
161
360
|
resolveSessionId,
|
|
162
361
|
buildSessionSnapshot,
|
|
362
|
+
hydrateSessionFromDisk,
|
|
163
363
|
getSessionFilePath,
|
|
164
364
|
saveSessionSnapshot,
|
|
165
365
|
loadSessionSnapshot,
|
|
366
|
+
listSessionSummaries,
|
|
367
|
+
deleteSessionData,
|
|
166
368
|
};
|
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,8 @@ module.exports = {
|
|
|
132
192
|
markdownSkillLinks,
|
|
133
193
|
resolveSkillLinkTarget,
|
|
134
194
|
findSkillByPath,
|
|
195
|
+
readSkillBlock,
|
|
196
|
+
persistSkillBodyArtifact,
|
|
135
197
|
buildSkillInjections,
|
|
198
|
+
sanitizeSkillContent,
|
|
136
199
|
};
|
|
@@ -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,34 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { runNativeAgentTask } = require("./nativeRunner");
|
|
7
|
+
const { assembleModelContext, recordToolCallInSession } = require("./context/assembler");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const {
|
|
10
|
+
buildSkillManifest,
|
|
11
|
+
renderActiveSkillBlock,
|
|
12
|
+
} = require("./skills");
|
|
13
|
+
const { sanitizeSkillContent } = require("./skills/injection");
|
|
14
|
+
|
|
15
|
+
function renderActiveSkillBodiesFromState(state = {}) {
|
|
16
|
+
const skills = Array.isArray(state.activeSkills) ? state.activeSkills : [];
|
|
17
|
+
const blocks = [];
|
|
18
|
+
for (const skill of skills) {
|
|
19
|
+
const skillPath = String(skill && skill.path || "").trim();
|
|
20
|
+
if (!skillPath) continue;
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(skillPath, "utf8");
|
|
23
|
+
const content = sanitizeSkillContent(raw);
|
|
24
|
+
const manifest = buildSkillManifest({
|
|
25
|
+
name: skill.name || "",
|
|
26
|
+
path: skillPath,
|
|
27
|
+
}, { bodyArtifactId: skill.bodyArtifactId || "" });
|
|
28
|
+
blocks.push(renderActiveSkillBlock(manifest, content));
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore missing skill bodies
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return blocks;
|
|
34
|
+
}
|
|
7
35
|
|
|
8
36
|
/**
|
|
9
37
|
* Decompose a bug fix task into manageable steps
|
|
@@ -132,6 +160,8 @@ async function runDecomposedTask({
|
|
|
132
160
|
systemPrompt,
|
|
133
161
|
messages = [],
|
|
134
162
|
sessionId = "",
|
|
163
|
+
state = null,
|
|
164
|
+
systemBlocks = null,
|
|
135
165
|
}) {
|
|
136
166
|
const steps = decomposeBugFixTask(task);
|
|
137
167
|
const results = [];
|
|
@@ -165,19 +195,43 @@ async function runDecomposedTask({
|
|
|
165
195
|
try {
|
|
166
196
|
// Run the step with its own timeout
|
|
167
197
|
const stepPrompt = buildStepPrompt(step, results);
|
|
198
|
+
let stepMessages = messages;
|
|
199
|
+
let stepSystemPrompt = systemPrompt;
|
|
200
|
+
let stepSystemBlocks = systemBlocks;
|
|
201
|
+
if (state) {
|
|
202
|
+
const skillBodies = renderActiveSkillBodiesFromState(state);
|
|
203
|
+
const assembled = assembleModelContext(state, {
|
|
204
|
+
workspaceRoot,
|
|
205
|
+
model,
|
|
206
|
+
provider,
|
|
207
|
+
turnDynamic: [...skillBodies, stepPrompt].filter(Boolean).join("\n\n"),
|
|
208
|
+
});
|
|
209
|
+
stepMessages = assembled.messages;
|
|
210
|
+
stepSystemPrompt = assembled.systemPrompt;
|
|
211
|
+
stepSystemBlocks = assembled.systemBlocks;
|
|
212
|
+
}
|
|
168
213
|
const stepResult = await runNativeAgentTask({
|
|
169
214
|
workspaceRoot,
|
|
170
215
|
provider,
|
|
171
216
|
model,
|
|
172
217
|
prompt: stepPrompt,
|
|
173
|
-
systemPrompt,
|
|
174
|
-
|
|
218
|
+
systemPrompt: stepSystemPrompt,
|
|
219
|
+
systemBlocks: stepSystemBlocks,
|
|
220
|
+
messages: stepMessages,
|
|
175
221
|
sessionId,
|
|
176
222
|
timeoutMs: step.timeoutMs,
|
|
177
223
|
onToolEvent,
|
|
178
224
|
signal,
|
|
225
|
+
onArtifactPersisted: state
|
|
226
|
+
? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
|
|
227
|
+
: null,
|
|
179
228
|
});
|
|
180
229
|
|
|
230
|
+
if (state && stepResult && Array.isArray(stepResult.messages)) {
|
|
231
|
+
const { syncMessagesToTranscript } = require("./context/assembler");
|
|
232
|
+
syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
|
|
233
|
+
}
|
|
234
|
+
|
|
181
235
|
results.push({
|
|
182
236
|
step: step.id,
|
|
183
237
|
name: step.name,
|