osborn 0.9.214 → 0.9.216
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/dist/claude-llm.js +90 -76
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getResearchSystemPrompt, getDirectModeResearchPrompt, getGroundingBlock
|
|
|
15
15
|
import { getIndexPath } from './summary-index.js';
|
|
16
16
|
import { openStore, recall, storeExists, updateSessionStore, getStorePath } from './session-store.js';
|
|
17
17
|
import { getEmbedder } from './embedder.js';
|
|
18
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
19
19
|
import { join, dirname, resolve, basename } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
@@ -139,32 +139,6 @@ function getSubagentsDir(workingDir) {
|
|
|
139
139
|
mkdirSync(dir, { recursive: true });
|
|
140
140
|
return dir;
|
|
141
141
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Load skill files from agent/.claude/skills/{name}/SKILL.md
|
|
144
|
-
* Injects into system prompt so Claude sees them as available capabilities.
|
|
145
|
-
* Skills execute via Bash — no SDK settingSources needed.
|
|
146
|
-
*/
|
|
147
|
-
function loadSkillsFromDir(agentDir) {
|
|
148
|
-
const skillsDir = join(agentDir, '.claude', 'skills');
|
|
149
|
-
if (!existsSync(skillsDir))
|
|
150
|
-
return '';
|
|
151
|
-
const skills = [];
|
|
152
|
-
try {
|
|
153
|
-
for (const skillName of readdirSync(skillsDir)) {
|
|
154
|
-
const skillFile = join(skillsDir, skillName, 'SKILL.md');
|
|
155
|
-
if (existsSync(skillFile)) {
|
|
156
|
-
skills.push(readFileSync(skillFile, 'utf-8').trim());
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
console.warn('⚠️ Failed to load skills:', err);
|
|
162
|
-
}
|
|
163
|
-
if (skills.length === 0)
|
|
164
|
-
return '';
|
|
165
|
-
console.log(`📚 Loaded ${skills.length} skill(s) from ${skillsDir}`);
|
|
166
|
-
return `<available-skills>\n${skills.join('\n\n---\n\n')}\n</available-skills>`;
|
|
167
|
-
}
|
|
168
142
|
/**
|
|
169
143
|
* Loads skills from both ~/.claude/skills/ (home dir) and {workingDir}/.claude/skills/ (project dir).
|
|
170
144
|
* Merges results, deduplicating by skill directory name — home dir wins on conflicts.
|
|
@@ -200,66 +174,105 @@ function loadAllSkills(_workingDir) {
|
|
|
200
174
|
return `<available-skills>\n${[...skillMap.values()].join('\n\n---\n\n')}\n</available-skills>`;
|
|
201
175
|
}
|
|
202
176
|
/**
|
|
203
|
-
* Enumerate the agent's CURRENT skills
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* (
|
|
208
|
-
*
|
|
177
|
+
* Enumerate the agent's CURRENT skills from ~/.claude/skills for injection into
|
|
178
|
+
* the PreCompact instruction — the material the compaction model reads so it
|
|
179
|
+
* refines/merges existing skills instead of re-emitting duplicates.
|
|
180
|
+
*
|
|
181
|
+
* Containment principle (why this is cheap AND complete):
|
|
182
|
+
* - SKILL.md is the canonical knowledge file and is small (~1–15KB each). We
|
|
183
|
+
* inject it IN FULL so the model sees every minute implementation detail and
|
|
184
|
+
* its UPDATE_SECTION edits are never blind. Bodies are emitted up to a total
|
|
185
|
+
* char budget; any overflow skill degrades to headings-only (still enough to
|
|
186
|
+
* dedup / target a section) so a pathological skill count can't blow the turn.
|
|
187
|
+
* - Satellite files in the folder (scripts/tools/reference docs) can be huge and
|
|
188
|
+
* unbounded, so we NEVER inject their contents — only a manifest line
|
|
189
|
+
* (name + size). The model can flag one for update; editing it is deferred to
|
|
190
|
+
* the user or a future tool-equipped merger that reads just that one file.
|
|
209
191
|
*/
|
|
192
|
+
// ~50K tokens of full SKILL.md text — ~5% of the 1M-context summarizer window, so
|
|
193
|
+
// completeness (the user's priority) wins over frugality. 16 defaults already use
|
|
194
|
+
// ~59KB, so a real user's defaults + learned + UI skills need real headroom here.
|
|
195
|
+
// NOTE: skills are walked alphabetically, so once the budget is hit the *late-alphabet*
|
|
196
|
+
// skills degrade to headings-only. A future refinement is to order by session-relevance
|
|
197
|
+
// (skills touched this session first) rather than name, so the overflow is the least
|
|
198
|
+
// relevant skills rather than an alphabetical accident.
|
|
199
|
+
const SKILL_BODY_BUDGET = 200_000;
|
|
200
|
+
function extractSkillDescription(raw) {
|
|
201
|
+
let desc = '';
|
|
202
|
+
const fm = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
203
|
+
if (fm) {
|
|
204
|
+
const m = fm[1].match(/^description:\s*(.+)$/m);
|
|
205
|
+
if (m)
|
|
206
|
+
desc = m[1].trim();
|
|
207
|
+
}
|
|
208
|
+
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
209
|
+
if (!desc) {
|
|
210
|
+
const when = body.match(/^\s*WHEN:\s*(.+)$/mi);
|
|
211
|
+
if (when)
|
|
212
|
+
desc = when[1].trim();
|
|
213
|
+
}
|
|
214
|
+
if (!desc) {
|
|
215
|
+
const first = body.split('\n').map(l => l.trim()).find(l => l && !l.startsWith('#'));
|
|
216
|
+
desc = first || '(no description)';
|
|
217
|
+
}
|
|
218
|
+
return desc.length > 200 ? desc.slice(0, 197) + '…' : desc;
|
|
219
|
+
}
|
|
210
220
|
function enumerateSkillsForCompaction() {
|
|
211
221
|
const dir = join(homedir(), '.claude', 'skills');
|
|
212
222
|
if (!existsSync(dir))
|
|
213
223
|
return '';
|
|
214
|
-
const
|
|
224
|
+
const blocks = [];
|
|
225
|
+
let budgetLeft = SKILL_BODY_BUDGET;
|
|
215
226
|
try {
|
|
216
227
|
for (const name of readdirSync(dir).sort()) {
|
|
217
|
-
const
|
|
228
|
+
const folder = join(dir, name);
|
|
229
|
+
const file = join(folder, 'SKILL.md');
|
|
218
230
|
if (!existsSync(file))
|
|
219
231
|
continue;
|
|
220
|
-
let desc = '';
|
|
221
232
|
try {
|
|
222
|
-
const raw = readFileSync(file, 'utf-8');
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
const raw = readFileSync(file, 'utf-8').trim();
|
|
234
|
+
const desc = extractSkillDescription(raw);
|
|
235
|
+
// Manifest of satellite files (name + size), contents NEVER injected.
|
|
236
|
+
let manifest = '';
|
|
237
|
+
try {
|
|
238
|
+
const others = readdirSync(folder)
|
|
239
|
+
.filter(f => f !== 'SKILL.md')
|
|
240
|
+
.map(f => {
|
|
241
|
+
try {
|
|
242
|
+
const st = statSync(join(folder, f));
|
|
243
|
+
const kb = st.isDirectory() ? '(dir)' : `${Math.max(1, Math.round(st.size / 1024))}KB`;
|
|
244
|
+
return `${f} ${kb}`;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return f;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
if (others.length)
|
|
251
|
+
manifest = `files (contents NOT shown — flag for update if needed): ${others.join(', ')}\n`;
|
|
228
252
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
253
|
+
catch { /* folder read race — skip manifest */ }
|
|
254
|
+
const header = `########## SKILL: ${name} ##########\ndescription: ${desc}\n${manifest}`;
|
|
255
|
+
if (raw.length <= budgetLeft) {
|
|
256
|
+
budgetLeft -= raw.length;
|
|
257
|
+
blocks.push(`${header}${raw}`);
|
|
234
258
|
}
|
|
235
|
-
|
|
259
|
+
else {
|
|
260
|
+
// Over budget → headings-only fallback (still enough to dedup + target a section).
|
|
236
261
|
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
237
|
-
const
|
|
238
|
-
|
|
262
|
+
const headings = (body.match(/^#{2,6}\s+.+$/gm) || [])
|
|
263
|
+
.map(h => h.replace(/^#+\s+/, '').trim());
|
|
264
|
+
blocks.push(`${header}(full body omitted — over injection budget)\nsections: ${headings.join(' | ') || '(none)'}`);
|
|
239
265
|
}
|
|
240
|
-
// Option A: expose the skill's SECTION HEADINGS so the compaction model can
|
|
241
|
-
// target a specific section for a surgical update (via UPDATE_SECTION) rather
|
|
242
|
-
// than re-dumping a whole new skill. The summarizer has no tool access — this
|
|
243
|
-
// is the only way it can "see inside" the file.
|
|
244
|
-
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
245
|
-
const headings = (body.match(/^#{2,3}\s+.+$/gm) || [])
|
|
246
|
-
.map(h => h.replace(/^#{2,3}\s+/, '').trim())
|
|
247
|
-
.filter(Boolean);
|
|
248
|
-
if (desc.length > 180)
|
|
249
|
-
desc = desc.slice(0, 177) + '…';
|
|
250
|
-
lines.push(`- ${name}: ${desc}`);
|
|
251
|
-
if (headings.length)
|
|
252
|
-
lines.push(` sections: ${headings.join(' | ')}`);
|
|
253
266
|
}
|
|
254
267
|
catch {
|
|
255
|
-
|
|
268
|
+
blocks.push(`########## SKILL: ${name} ##########\n(unreadable)`);
|
|
256
269
|
}
|
|
257
270
|
}
|
|
258
271
|
}
|
|
259
272
|
catch (err) {
|
|
260
273
|
console.warn('⚠️ enumerateSkillsForCompaction failed:', err instanceof Error ? err.message : err);
|
|
261
274
|
}
|
|
262
|
-
return
|
|
275
|
+
return blocks.join('\n\n');
|
|
263
276
|
}
|
|
264
277
|
// Compaction threshold: Fable 5 runs a 1M context window, so let sessions use
|
|
265
278
|
// all of it before auto-compacting. autoCompactWindow max is 1_000_000; the SDK
|
|
@@ -2174,24 +2187,25 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
2174
2187
|
// proliferated duplicates. We hand it the current skill set + an adversarial,
|
|
2175
2188
|
// self-critical directive to merge/refine rather than create anew.
|
|
2176
2189
|
const existingSkills = enumerateSkillsForCompaction();
|
|
2177
|
-
//
|
|
2190
|
+
// Each skill is delimited by a `########## SKILL: <name> ##########` header.
|
|
2178
2191
|
const skillCount = existingSkills
|
|
2179
|
-
? existingSkills.
|
|
2192
|
+
? (existingSkills.match(/^########## SKILL: /gm) || []).length
|
|
2180
2193
|
: 0;
|
|
2181
2194
|
const criticBlock = existingSkills
|
|
2182
|
-
? `\n\n---\n\n=== EXISTING SKILLS (${skillCount}) ===\n`
|
|
2183
|
-
+ `
|
|
2184
|
-
+
|
|
2185
|
-
+ `
|
|
2186
|
-
+ `
|
|
2195
|
+
? `\n\n---\n\n=== EXISTING SKILLS (${skillCount}) — FULL TEXT ===\n`
|
|
2196
|
+
+ `Below is the COMPLETE current text of each skill the user already has (delimited by `
|
|
2197
|
+
+ `\`########## SKILL: <name> ##########\`). A \`files:\` line lists any satellite files in the `
|
|
2198
|
+
+ `skill folder (scripts/tools/docs) whose CONTENTS are not shown — flag one only if it clearly needs updating.\n`
|
|
2199
|
+
+ `Read these before emitting SKILL_CANDIDATES or BEHAVIORAL_LEARNINGS, and act as an ADVERSARIAL reviewer of your own output:\n`
|
|
2200
|
+
+ `1. If what you learned refines ONE part of an existing skill, do NOT re-emit the whole skill. Emit a `
|
|
2187
2201
|
+ `surgical section update inside SKILL_CANDIDATES using this exact block form (the heading must match a `
|
|
2188
|
-
+
|
|
2202
|
+
+ `\`## \`/\`### \` heading that appears verbatim in that skill's text above):\n`
|
|
2189
2203
|
+ ` --- UPDATE_SECTION: <skill-name> > <exact section heading> ---\n`
|
|
2190
|
-
+ ` <the new markdown that should REPLACE that section's body>\n`
|
|
2204
|
+
+ ` <the new markdown that should REPLACE that section's body — preserve detail already present that is still correct>\n`
|
|
2191
2205
|
+ ` --- END UPDATE_SECTION ---\n`
|
|
2192
|
-
+ `2. If a candidate duplicates or substantially overlaps one
|
|
2206
|
+
+ `2. If a candidate duplicates or substantially overlaps one above but isn't a single-section tweak, `
|
|
2193
2207
|
+ `re-emit it under the EXACT SAME kebab-case name, and only if it needs a substantive update; otherwise omit it.\n`
|
|
2194
|
-
+ `3. Propose a brand-new skill ONLY if nothing
|
|
2208
|
+
+ `3. Propose a brand-new skill ONLY if nothing above covers it, it was CONFIRMED working this session, `
|
|
2195
2209
|
+ `and it generalizes to future sessions on different tasks.\n`
|
|
2196
2210
|
+ `4. Prefer refining/section-updating over proliferating. A small set of sharp, non-overlapping skills is the goal — `
|
|
2197
2211
|
+ `reject your own low-signal or one-off candidates.\n\n`
|