osborn 0.9.9 → 0.9.13
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.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface ClaudeLLMOptions {
|
|
|
21
21
|
model?: string;
|
|
22
22
|
voiceMode?: 'direct' | 'realtime';
|
|
23
23
|
skipTTSQueue?: boolean;
|
|
24
|
+
onCompactionEvent?: (event: {
|
|
25
|
+
type: 'compaction_started' | 'compaction_complete';
|
|
26
|
+
trigger?: string;
|
|
27
|
+
skillsWritten?: number;
|
|
28
|
+
}) => void;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Claude LLM - Wraps Claude Agent SDK for LiveKit
|
package/dist/claude-llm.js
CHANGED
|
@@ -548,7 +548,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
548
548
|
*/
|
|
549
549
|
pushMessage(userText, sdkOptions, callbacks) {
|
|
550
550
|
// Lower compaction threshold to 65% so PreCompact fires earlier and context is preserved
|
|
551
|
-
process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = '
|
|
551
|
+
process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = '75';
|
|
552
552
|
const userMessage = {
|
|
553
553
|
type: 'user',
|
|
554
554
|
message: { role: 'user', content: [{ type: 'text', text: userText }] },
|
|
@@ -914,136 +914,19 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
914
914
|
}
|
|
915
915
|
}]
|
|
916
916
|
}],
|
|
917
|
-
// ── PreCompact:
|
|
918
|
-
// Fires before the SDK compresses the conversation.
|
|
919
|
-
//
|
|
920
|
-
//
|
|
917
|
+
// ── PreCompact: inject extraction instruction only ──
|
|
918
|
+
// Fires before the SDK compresses the conversation. Injects the compact-learnings
|
|
919
|
+
// instruction so the SDK compaction summary includes the four structured sections.
|
|
920
|
+
// Skill extraction happens in PostCompact, which reads input.compact_summary directly.
|
|
921
921
|
PreCompact: [{
|
|
922
922
|
matcher: '.*',
|
|
923
923
|
hooks: [async (input) => {
|
|
924
924
|
try {
|
|
925
|
-
|
|
926
|
-
const { join } = await import('node:path');
|
|
927
|
-
// 1. Read the instruction file
|
|
925
|
+
this.#opts.onCompactionEvent?.({ type: 'compaction_started', trigger: input?.trigger });
|
|
928
926
|
const instructionPath = join(__claudeLlmDir, 'prompts', 'compact-learnings-instruction.md');
|
|
929
927
|
const instruction = existsSync(instructionPath) ? readFileSync(instructionPath, 'utf-8') : '';
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
let transcriptContent = '';
|
|
933
|
-
if (transcriptPath && existsSync(transcriptPath)) {
|
|
934
|
-
try {
|
|
935
|
-
transcriptContent = readFileSync(transcriptPath, 'utf-8');
|
|
936
|
-
}
|
|
937
|
-
catch (readErr) {
|
|
938
|
-
console.warn('⚠️ PreCompact: could not read transcript:', readErr instanceof Error ? readErr.message : readErr);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
const skillDir = this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd();
|
|
942
|
-
const skillsRoot = join(skillDir, '.claude', 'skills');
|
|
943
|
-
// 3. Load existing skills for context (cap each at 500 chars)
|
|
944
|
-
let existingSkillsBlock = '';
|
|
945
|
-
if (existsSync(skillsRoot)) {
|
|
946
|
-
try {
|
|
947
|
-
const names = readdirSync(skillsRoot).slice(0, 15);
|
|
948
|
-
const fragments = [];
|
|
949
|
-
for (const name of names) {
|
|
950
|
-
const p = join(skillsRoot, name, 'SKILL.md');
|
|
951
|
-
if (existsSync(p)) {
|
|
952
|
-
const body = readFileSync(p, 'utf-8');
|
|
953
|
-
const cleaned = body.replace(/^===.*===\s*$/gm, '').substring(0, 500);
|
|
954
|
-
fragments.push(`### ${name}\n${cleaned}`);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
if (fragments.length) {
|
|
958
|
-
existingSkillsBlock = `EXISTING SKILLS (do NOT re-emit unchanged):\n${fragments.join('\n\n')}`;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
catch (skillErr) {
|
|
962
|
-
console.warn('⚠️ PreCompact: skills load failed:', skillErr instanceof Error ? skillErr.message : skillErr);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
// 4. If transcript available, use direct Anthropic API call to extract everything
|
|
966
|
-
if (transcriptContent && process.env.ANTHROPIC_API_KEY) {
|
|
967
|
-
try {
|
|
968
|
-
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
969
|
-
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
970
|
-
const extractionPrompt = [
|
|
971
|
-
instruction,
|
|
972
|
-
existingSkillsBlock,
|
|
973
|
-
`FULL SESSION TRANSCRIPT (JSONL format — extract the four sections from this):`,
|
|
974
|
-
transcriptContent,
|
|
975
|
-
].filter(Boolean).join('\n\n---\n\n');
|
|
976
|
-
console.log(`🧠 PreCompact: calling extraction API (${extractionPrompt.length} chars, trigger=${input?.trigger || 'unknown'})`);
|
|
977
|
-
const response = await client.messages.create({
|
|
978
|
-
model: 'claude-haiku-4-5-20251001',
|
|
979
|
-
max_tokens: 4096,
|
|
980
|
-
messages: [{ role: 'user', content: extractionPrompt }],
|
|
981
|
-
});
|
|
982
|
-
const extractedText = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
983
|
-
if (extractedText) {
|
|
984
|
-
// Write SKILL_CANDIDATES to disk directly
|
|
985
|
-
const skillRegex = /--- SKILL: ([a-z][a-z0-9-]{1,39}) ---\n([\s\S]*?)\n--- END SKILL ---/g;
|
|
986
|
-
let match;
|
|
987
|
-
while ((match = skillRegex.exec(extractedText)) !== null) {
|
|
988
|
-
const [, name, body] = match;
|
|
989
|
-
if (/^[a-z][a-z0-9-]{1,39}$/.test(name)) {
|
|
990
|
-
const today = new Date().toISOString().split('T')[0];
|
|
991
|
-
const sessionId = this.#sessionId || 'unknown';
|
|
992
|
-
const skillFolder = join(skillsRoot, name);
|
|
993
|
-
mkdirSync(skillFolder, { recursive: true });
|
|
994
|
-
writeFileSync(join(skillFolder, 'SKILL.md'), `# ${name}\nAuto-extracted: ${today} | Session: ${sessionId.substring(0, 8)}\n\n${body}`);
|
|
995
|
-
console.log(`🧠 PreCompact: wrote skill '${name}' to ${skillFolder}`);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
// Write BEHAVIORAL_LEARNINGS to learned-behaviors skill
|
|
999
|
-
const blMarker = '=== BEHAVIORAL_LEARNINGS ===';
|
|
1000
|
-
const blIdx = extractedText.indexOf(blMarker);
|
|
1001
|
-
if (blIdx !== -1) {
|
|
1002
|
-
const blContent = extractedText.substring(blIdx);
|
|
1003
|
-
if (blContent.length > 30) {
|
|
1004
|
-
const today = new Date().toISOString().split('T')[0];
|
|
1005
|
-
const sessionId = this.#sessionId || 'unknown';
|
|
1006
|
-
const learnedDir = join(skillsRoot, 'learned-behaviors');
|
|
1007
|
-
mkdirSync(learnedDir, { recursive: true });
|
|
1008
|
-
const header = `# Learned Behaviors\n\nAuto-extracted from voice sessions via PreCompact.\nLast updated: ${today} | Session: ${sessionId.substring(0, 8)}...\n\n`;
|
|
1009
|
-
writeFileSync(join(learnedDir, 'SKILL.md'), header + blContent);
|
|
1010
|
-
console.log(`🧠 PreCompact: wrote learned-behaviors (${blContent.length} chars)`);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
// Write project-scoped DECISIONS
|
|
1014
|
-
const decMarker = '=== DECISIONS ===';
|
|
1015
|
-
const decIdx = extractedText.indexOf(decMarker);
|
|
1016
|
-
if (decIdx !== -1) {
|
|
1017
|
-
const decEnd = extractedText.indexOf('===', decIdx + decMarker.length);
|
|
1018
|
-
const decContent = extractedText.substring(decIdx + decMarker.length, decEnd !== -1 ? decEnd : undefined);
|
|
1019
|
-
const projectDecisions = decContent.split('\n').filter((l) => l.includes('SCOPE: project')).join('\n');
|
|
1020
|
-
if (projectDecisions.trim()) {
|
|
1021
|
-
const decDir = join(skillsRoot, 'project-decisions');
|
|
1022
|
-
mkdirSync(decDir, { recursive: true });
|
|
1023
|
-
const decFile = join(decDir, 'SKILL.md');
|
|
1024
|
-
const existing = existsSync(decFile) ? readFileSync(decFile, 'utf-8') : '# Project Decisions\n\n';
|
|
1025
|
-
writeFileSync(decFile, existing + '\n' + projectDecisions);
|
|
1026
|
-
console.log(`🧠 PreCompact: appended project decisions`);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
// Return extracted content as systemMessage for compact summary
|
|
1030
|
-
const systemMessage = [
|
|
1031
|
-
'## Pre-Compaction Extraction Complete',
|
|
1032
|
-
'Skill files have been written to disk. Include the following in your compact summary verbatim:',
|
|
1033
|
-
extractedText.substring(0, 8000),
|
|
1034
|
-
].join('\n\n').substring(0, 9500);
|
|
1035
|
-
console.log(`🧠 PreCompact: extraction complete, returning systemMessage (${systemMessage.length} chars)`);
|
|
1036
|
-
return { systemMessage };
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
catch (apiErr) {
|
|
1040
|
-
console.error('⚠️ PreCompact: API extraction failed, falling back to basic mode:', apiErr instanceof Error ? apiErr.message : apiErr);
|
|
1041
|
-
// Fall through to basic fallback
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
// 5. Fallback: basic mode — just inject the instruction
|
|
1045
|
-
console.log(`🧠 PreCompact: basic mode (no transcript or API key unavailable)`);
|
|
1046
|
-
return { systemMessage: instruction.substring(0, 9500) };
|
|
928
|
+
console.log(`🧠 PreCompact: injecting instruction (${instruction.length} chars, trigger=${input?.trigger || 'unknown'})`);
|
|
929
|
+
return { systemMessage: instruction };
|
|
1047
930
|
}
|
|
1048
931
|
catch (err) {
|
|
1049
932
|
console.error('⚠️ PreCompact hook error:', err instanceof Error ? err.message : err);
|
|
@@ -1051,16 +934,19 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1051
934
|
}
|
|
1052
935
|
}]
|
|
1053
936
|
}],
|
|
1054
|
-
// ── PostCompact:
|
|
937
|
+
// ── PostCompact: parse compact_summary from stdin payload and persist skill sections ──
|
|
938
|
+
// input.compact_summary is the full summary text the SDK just wrote.
|
|
939
|
+
// We read the four sections directly from it — no JSONL file access needed.
|
|
1055
940
|
PostCompact: [{
|
|
1056
941
|
matcher: '.*',
|
|
1057
942
|
hooks: [async (input) => {
|
|
1058
943
|
try {
|
|
1059
944
|
const summary = input?.compact_summary || '';
|
|
1060
|
-
const { mkdirSync, writeFileSync: writeSyncFs } = await import('fs');
|
|
945
|
+
const { mkdirSync, writeFileSync: writeSyncFs, readFileSync: readSyncFs, existsSync: existsSyncFs } = await import('node:fs');
|
|
1061
946
|
const skillDir = this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd();
|
|
1062
947
|
const today = new Date().toISOString().split('T')[0];
|
|
1063
948
|
const sessionId = this.#sessionId || 'unknown';
|
|
949
|
+
let skillsWritten = 0;
|
|
1064
950
|
// Helper: extract text between a marker and the next === marker (or end of string)
|
|
1065
951
|
const extractSection = (marker) => {
|
|
1066
952
|
const idx = summary.indexOf(marker);
|
|
@@ -1070,7 +956,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1070
956
|
const nextMarker = summary.indexOf('=== ', start);
|
|
1071
957
|
return (nextMarker === -1 ? summary.substring(start) : summary.substring(start, nextMarker)).trim();
|
|
1072
958
|
};
|
|
1073
|
-
// ── Section 1: HANDOFF_STATE —
|
|
959
|
+
// ── Section 1: HANDOFF_STATE — stays in compact summary only, not written to disk ──
|
|
1074
960
|
const handoff = extractSection('=== HANDOFF_STATE ===');
|
|
1075
961
|
if (handoff) {
|
|
1076
962
|
console.log(`🧠 PostCompact: HANDOFF_STATE present (${handoff.length} chars) — not written to disk`);
|
|
@@ -1083,26 +969,22 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1083
969
|
.split('\n')
|
|
1084
970
|
.filter(l => /DECISION:.*SCOPE:\s*project/i.test(l));
|
|
1085
971
|
if (projectLines.length) {
|
|
1086
|
-
const decFolder = join(skillDir, '.claude', 'skills', '
|
|
972
|
+
const decFolder = join(skillDir, '.claude', 'skills', 'decisions');
|
|
1087
973
|
const decPath = join(decFolder, 'SKILL.md');
|
|
1088
974
|
mkdirSync(decFolder, { recursive: true });
|
|
1089
|
-
const existing = (
|
|
1090
|
-
return require('fs').readFileSync(decPath, 'utf-8');
|
|
1091
|
-
}
|
|
1092
|
-
catch {
|
|
1093
|
-
return '';
|
|
1094
|
-
} })();
|
|
975
|
+
const existing = existsSyncFs(decPath) ? readSyncFs(decPath, 'utf-8') : '';
|
|
1095
976
|
const header = existing ? '' : `# Project Decisions\n\nAuto-extracted from compact summaries.\n\n`;
|
|
1096
977
|
const entry = `\n## ${today} (session ${sessionId.substring(0, 8)})\n${projectLines.join('\n')}\n`;
|
|
1097
978
|
writeSyncFs(decPath, header + existing + entry, 'utf-8');
|
|
1098
979
|
console.log(`🧠 PostCompact: appended ${projectLines.length} decision(s) to ${decPath}`);
|
|
980
|
+
skillsWritten++;
|
|
1099
981
|
}
|
|
1100
982
|
}
|
|
1101
983
|
}
|
|
1102
984
|
catch (decErr) {
|
|
1103
985
|
console.error('⚠️ PostCompact: DECISIONS write failed:', decErr instanceof Error ? decErr.message : decErr);
|
|
1104
986
|
}
|
|
1105
|
-
// ── Section 3: SKILL_CANDIDATES — parse and write each
|
|
987
|
+
// ── Section 3: SKILL_CANDIDATES — parse individual blocks and write each ──
|
|
1106
988
|
try {
|
|
1107
989
|
const skillsSection = extractSection('=== SKILL_CANDIDATES ===');
|
|
1108
990
|
if (skillsSection) {
|
|
@@ -1122,37 +1004,34 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1122
1004
|
const header = `# ${name}\nAuto-extracted: ${today} | Session: ${sessionId.substring(0, 8)}\n\n`;
|
|
1123
1005
|
writeSyncFs(skillPath, header + body + '\n', 'utf-8');
|
|
1124
1006
|
console.log(`🧠 PostCompact: wrote skill '${name}' to ${skillPath}`);
|
|
1007
|
+
skillsWritten++;
|
|
1125
1008
|
}
|
|
1126
1009
|
}
|
|
1127
1010
|
}
|
|
1128
1011
|
catch (skillErr) {
|
|
1129
1012
|
console.error('⚠️ PostCompact: SKILL_CANDIDATES write failed:', skillErr instanceof Error ? skillErr.message : skillErr);
|
|
1130
1013
|
}
|
|
1131
|
-
// ── Section 4: BEHAVIORAL_LEARNINGS — write to learned-behaviors
|
|
1014
|
+
// ── Section 4: BEHAVIORAL_LEARNINGS — write to learned-behaviors/SKILL.md ──
|
|
1132
1015
|
try {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
writeSyncFs(skillPath, header + learnings + '\n', 'utf-8');
|
|
1143
|
-
console.log(`🧠 PostCompact: wrote learned behaviors to ${skillPath} (${learnings.length} chars)`);
|
|
1144
|
-
}
|
|
1145
|
-
else {
|
|
1146
|
-
console.log('🧠 PostCompact: BEHAVIORAL_LEARNINGS section too short — skipping');
|
|
1147
|
-
}
|
|
1016
|
+
const learnings = extractSection('=== BEHAVIORAL_LEARNINGS ===');
|
|
1017
|
+
if (learnings.length >= 30) {
|
|
1018
|
+
const skillFolder = join(skillDir, '.claude', 'skills', 'learned-behaviors');
|
|
1019
|
+
const skillPath = join(skillFolder, 'SKILL.md');
|
|
1020
|
+
mkdirSync(skillFolder, { recursive: true });
|
|
1021
|
+
const header = `# Learned Behaviors\n\nAuto-extracted from voice sessions via PostCompact.\nLast updated: ${today} | Session: ${sessionId.substring(0, 8)}...\n\n`;
|
|
1022
|
+
writeSyncFs(skillPath, header + learnings + '\n', 'utf-8');
|
|
1023
|
+
console.log(`🧠 PostCompact: wrote learned behaviors to ${skillPath} (${learnings.length} chars)`);
|
|
1024
|
+
skillsWritten++;
|
|
1148
1025
|
}
|
|
1149
1026
|
else {
|
|
1150
|
-
console.log('🧠 PostCompact: no BEHAVIORAL_LEARNINGS section found
|
|
1027
|
+
console.log('🧠 PostCompact: no BEHAVIORAL_LEARNINGS section found or too short — skipping');
|
|
1151
1028
|
}
|
|
1152
1029
|
}
|
|
1153
1030
|
catch (blErr) {
|
|
1154
1031
|
console.error('⚠️ PostCompact: BEHAVIORAL_LEARNINGS write failed:', blErr instanceof Error ? blErr.message : blErr);
|
|
1155
1032
|
}
|
|
1033
|
+
this.#opts.onCompactionEvent?.({ type: 'compaction_complete', skillsWritten });
|
|
1034
|
+
console.log(`🧠 PostCompact: complete — ${skillsWritten} skill file(s) written`);
|
|
1156
1035
|
}
|
|
1157
1036
|
catch (err) {
|
|
1158
1037
|
console.error('⚠️ PostCompact hook error:', err instanceof Error ? err.message : err);
|
package/dist/index.js
CHANGED
|
@@ -1288,6 +1288,12 @@ async function main() {
|
|
|
1288
1288
|
resumeSessionId,
|
|
1289
1289
|
voiceMode: 'direct',
|
|
1290
1290
|
skipTTSQueue: true,
|
|
1291
|
+
onCompactionEvent: (event) => {
|
|
1292
|
+
try {
|
|
1293
|
+
sendToFrontend({ type: event.type, trigger: event.trigger, skillsWritten: event.skillsWritten });
|
|
1294
|
+
}
|
|
1295
|
+
catch { /* non-fatal */ }
|
|
1296
|
+
},
|
|
1291
1297
|
});
|
|
1292
1298
|
currentLLM = directLLM;
|
|
1293
1299
|
// Reset the session always-allow list for each new direct session
|
|
@@ -1615,6 +1621,12 @@ async function main() {
|
|
|
1615
1621
|
sessionBaseDir,
|
|
1616
1622
|
mcpServers,
|
|
1617
1623
|
resumeSessionId,
|
|
1624
|
+
onCompactionEvent: (event) => {
|
|
1625
|
+
try {
|
|
1626
|
+
sendToFrontend({ type: event.type, trigger: event.trigger, skillsWritten: event.skillsWritten });
|
|
1627
|
+
}
|
|
1628
|
+
catch { /* non-fatal */ }
|
|
1629
|
+
},
|
|
1618
1630
|
});
|
|
1619
1631
|
currentLLM = realtimeClaudeHandler;
|
|
1620
1632
|
// For resumed sessions, eagerly create workspace (we know the real ID)
|
|
@@ -13,6 +13,8 @@ Include:
|
|
|
13
13
|
- **Test results**: What was tried, what it showed, what it ruled out
|
|
14
14
|
|
|
15
15
|
Keep this under 800 characters. This stays in the compact summary — it is NOT written to disk.
|
|
16
|
+
- **ANNOUNCE**: End HANDOFF_STATE with one line formatted as:
|
|
17
|
+
`ANNOUNCE: <natural-language sentence for Osborn to speak aloud when resuming — mention that session memory was crystallized and how many skills/decisions were updated. Keep it to one sentence, conversational tone, like a colleague mentioning it in passing.>`
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
@@ -10,6 +10,8 @@ Session workspace: ${workspacePath}
|
|
|
10
10
|
· You CAN write other files to the workspace (detailed findings, diffs, notes, code samples) that the user sees in a files panel
|
|
11
11
|
|
|
12
12
|
Working principle: SPEAK the thinking, WRITE the details.
|
|
13
|
+
|
|
14
|
+
COMPACT RESUME BEHAVIOR: When this conversation has been compacted, the compact summary may contain a line beginning with "ANNOUNCE:". If it does, speak that line aloud as your first response before picking up any pending task. This is NOT recapping the summary — it is delivering a brief memory-crystallization update that the user is expecting. After speaking it, continue normally.
|
|
13
15
|
</context>
|
|
14
16
|
|
|
15
17
|
<objective>
|
package/dist/prompts.js
CHANGED
|
@@ -554,6 +554,8 @@ Quick lookups (1-2 calls) you can do directly. Everything else goes to an agent.
|
|
|
554
554
|
You do NOT produce findings from training data alone. You use tools to confirm every specific fact — file names, version numbers, function signatures, configuration values, URLs. If a tool is not available to verify a claim, you say so.
|
|
555
555
|
|
|
556
556
|
IF INTERRUPTED OR RESTARTED: Check ~/.claude/projects/ subagents folder for recent sub-agent JSONL files. Read the last entries to understand what was completed. Resume from that point.
|
|
557
|
+
|
|
558
|
+
Exception: if the compact summary contains a line beginning with "ANNOUNCE:", speak that line to the user as your first response before picking up the task. This is NOT recapping the summary — it is delivering a brief memory-crystallization update that the user is expecting.
|
|
557
559
|
</role>
|
|
558
560
|
|
|
559
561
|
<write-rules>
|