openhermes 1.12.1 → 2.5.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/README.md +126 -207
- package/autorecall.mjs +79 -12
- package/bootstrap.mjs +123 -24
- package/curator.mjs +4 -40
- package/harness/commands/harness-audit.md +1 -1
- package/harness/commands/learn.md +2 -2
- package/harness/commands/memory-search.md +2 -2
- package/harness/commands/ohc.md +13 -0
- package/harness/constitution/soul.md +16 -4
- package/harness/instructions/RUNTIME.md +6 -3
- package/harness/prompts/architect.txt +14 -0
- package/harness/prompts/build-cpp.md +15 -1
- package/harness/prompts/build-error-resolver.md +15 -9
- package/harness/prompts/build-go.md +14 -0
- package/harness/prompts/build-java.md +15 -1
- package/harness/prompts/build-kotlin.md +15 -1
- package/harness/prompts/build-rust.md +14 -0
- package/harness/prompts/code-reviewer.md +15 -9
- package/harness/prompts/doc-updater.md +13 -0
- package/harness/prompts/docs-lookup.md +11 -0
- package/harness/prompts/e2e-runner.txt +12 -0
- package/harness/prompts/explore.md +16 -4
- package/harness/prompts/harness-optimizer.md +12 -0
- package/harness/prompts/loop-operator.md +11 -0
- package/harness/prompts/planner.md +15 -9
- package/harness/prompts/refactor-cleaner.md +14 -0
- package/harness/prompts/review-cpp.md +14 -1
- package/harness/prompts/review-database.md +13 -0
- package/harness/prompts/review-go.md +13 -0
- package/harness/prompts/review-java.md +14 -1
- package/harness/prompts/review-kotlin.md +13 -0
- package/harness/prompts/review-python.md +14 -1
- package/harness/prompts/review-rust.md +13 -0
- package/harness/prompts/security-reviewer.md +15 -9
- package/harness/prompts/tdd-guide.md +14 -0
- package/harness/rules/audit.md +2 -2
- package/harness/rules/delegation.md +0 -2
- package/harness/rules/handoff.md +267 -0
- package/harness/rules/memory-management.md +4 -4
- package/harness/rules/precedence.md +1 -1
- package/harness/rules/retrieval.md +5 -5
- package/harness/rules/runtime-guards.md +1 -1
- package/harness/rules/self-heal.md +1 -1
- package/harness/rules/session-start.md +5 -5
- package/harness/rules/skills-management.md +2 -2
- package/harness/rules/verification.md +4 -4
- package/harness/scripts/sync-commands.mjs +259 -0
- package/index.mjs +6 -2
- package/lib/ambient-memory.mjs +167 -0
- package/lib/handoff.mjs +176 -0
- package/lib/hardening.mjs +13 -8
- package/lib/memory-tools-plugin.mjs +107 -54
- package/lib/ohc/block-sync.mjs +69 -0
- package/lib/ohc/compress/search.mjs +152 -0
- package/lib/ohc/compress/state.mjs +76 -0
- package/lib/ohc/config.mjs +172 -16
- package/lib/ohc/message-ids.mjs +168 -0
- package/lib/ohc/notify.mjs +150 -0
- package/lib/ohc/protected-patterns.mjs +54 -0
- package/lib/ohc/prune-apply.mjs +134 -0
- package/lib/ohc/pruner.mjs +406 -55
- package/lib/ohc/reaper.mjs +12 -3
- package/lib/ohc/state.mjs +246 -15
- package/lib/ohc/strategies/deduplication.mjs +72 -0
- package/lib/ohc/strategies/index.mjs +2 -0
- package/lib/ohc/strategies/purge-errors.mjs +43 -0
- package/lib/ohc/token-utils.mjs +26 -0
- package/lib/ohc/updater.mjs +36 -13
- package/lib/paths.mjs +0 -3
- package/lib/search.mjs +48 -0
- package/package.json +6 -2
- package/schemas/audit.schema.json +22 -1
- package/schemas/backlog.schema.json +23 -2
- package/schemas/checkpoint.schema.json +23 -2
- package/schemas/constraint.schema.json +23 -2
- package/schemas/decision.schema.json +23 -2
- package/schemas/instinct.schema.json +23 -2
- package/schemas/mistake.schema.json +23 -2
- package/schemas/verification_receipt.schema.json +23 -2
- package/skill-builder.mjs +12 -23
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
function formatTokenCount(tokens) {
|
|
2
|
+
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`.replace(".0K", "K")
|
|
3
|
+
return String(tokens)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function buildProgressBar(prunedCount, visibleCount, width) {
|
|
7
|
+
width = width || 30
|
|
8
|
+
const total = prunedCount + visibleCount
|
|
9
|
+
if (total === 0) return `\u2502${"\u2591".repeat(width)}\u2502 0% active`
|
|
10
|
+
const activeRatio = visibleCount / total
|
|
11
|
+
const activeW = Math.round(activeRatio * width)
|
|
12
|
+
const prunedW = width - activeW
|
|
13
|
+
const bar = "\u2588".repeat(Math.min(activeW, width)) + "\u2591".repeat(Math.min(prunedW, width))
|
|
14
|
+
return `\u2502${bar.slice(0, width)}\u2502 ${Math.round(activeRatio * 100)}% active`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildMinimal(count, tokensRemoved, savedTotal, blockCount) {
|
|
18
|
+
const label = "Compression"
|
|
19
|
+
return `\u25A3 OHC | ~${formatTokenCount(savedTotal)} saved total \u2014 ${label}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildDetailed(count, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount, summary) {
|
|
23
|
+
const label = "Compression"
|
|
24
|
+
let msg = `\u25A3 OHC | ~${formatTokenCount(savedTotal)} saved total`
|
|
25
|
+
if (prunedCount + visibleCount > 0) {
|
|
26
|
+
msg += `\n\n${buildProgressBar(prunedCount, visibleCount)}`
|
|
27
|
+
}
|
|
28
|
+
msg += `\n\n\u25A3 ${label} #${blockCount}`
|
|
29
|
+
msg += `\n\u2192 ${count} message${count === 1 ? "" : "s"} removed`
|
|
30
|
+
if (summary) msg += `\n\u2192 Summary: ${summary}`
|
|
31
|
+
return msg
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildStrategyNotification(strategy, count, detail) {
|
|
35
|
+
return `\u25A3 OHC | ${strategy}: ${count} pruned${detail ? ` (${detail})` : ""}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function sendCompressNotification(client, sessionId, config, count, summary, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount) {
|
|
39
|
+
if (count === 0) return false
|
|
40
|
+
|
|
41
|
+
const notifType = config.notification ?? "toast"
|
|
42
|
+
const notifMode = config.notificationMode ?? "minimal"
|
|
43
|
+
|
|
44
|
+
if (notifType === "off") return false
|
|
45
|
+
|
|
46
|
+
if (notifType === "toast") {
|
|
47
|
+
const message = notifMode === "minimal"
|
|
48
|
+
? buildMinimal(count, tokensRemoved, savedTotal, blockCount)
|
|
49
|
+
: buildDetailed(count, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount, summary)
|
|
50
|
+
try {
|
|
51
|
+
await client.tui.showToast({
|
|
52
|
+
body: {
|
|
53
|
+
title: "OHC: Compression",
|
|
54
|
+
message,
|
|
55
|
+
variant: "info",
|
|
56
|
+
duration: 5000,
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
} catch {}
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await client.session.prompt({
|
|
65
|
+
path: { id: sessionId },
|
|
66
|
+
body: {
|
|
67
|
+
noReply: true,
|
|
68
|
+
parts: [{ type: "text", text: buildDetailed(count, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount, summary), ignored: true }],
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
} catch {}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function sendStrategyNotification(client, sessionId, config, strategy, count, detail) {
|
|
76
|
+
if (count === 0) return false
|
|
77
|
+
const notifType = config.notification ?? "toast"
|
|
78
|
+
if (notifType === "off") return false
|
|
79
|
+
|
|
80
|
+
const message = buildStrategyNotification(strategy, count, detail)
|
|
81
|
+
|
|
82
|
+
if (notifType === "toast") {
|
|
83
|
+
try {
|
|
84
|
+
await client.tui.showToast({
|
|
85
|
+
body: {
|
|
86
|
+
title: `OHC: ${strategy}`,
|
|
87
|
+
message,
|
|
88
|
+
variant: "info",
|
|
89
|
+
duration: 3000,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
} catch {}
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await client.session.prompt({
|
|
98
|
+
path: { id: sessionId },
|
|
99
|
+
body: {
|
|
100
|
+
noReply: true,
|
|
101
|
+
parts: [{ type: "text", text: message, ignored: true }],
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
} catch {}
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function sendMemoryNotification(client, sessionId, config, action, cls, id, summary) {
|
|
109
|
+
const notifType = config.notification ?? "toast"
|
|
110
|
+
const notifMode = config.notificationMode ?? "minimal"
|
|
111
|
+
|
|
112
|
+
if (notifType === "off") return false
|
|
113
|
+
|
|
114
|
+
const buildHeader = (action, summary) => `[Memory] ${summary}`
|
|
115
|
+
const buildDetailed = (action, cls, id, summary) => {
|
|
116
|
+
let msg = `\u25A3 Memory ${action}`
|
|
117
|
+
if (cls) msg += `\n\u2192 Class: ${cls}`
|
|
118
|
+
if (id) msg += `\n\u2192 ID: ${id}`
|
|
119
|
+
if (summary) msg += `\n\u2192 ${summary}`
|
|
120
|
+
return msg
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (notifType === "toast") {
|
|
124
|
+
const message = notifMode === "minimal"
|
|
125
|
+
? buildHeader(action, summary)
|
|
126
|
+
: buildDetailed(action, cls, id, summary)
|
|
127
|
+
try {
|
|
128
|
+
await client.tui.showToast({
|
|
129
|
+
body: {
|
|
130
|
+
title: "Memory",
|
|
131
|
+
message,
|
|
132
|
+
variant: "info",
|
|
133
|
+
duration: 4000,
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
} catch {}
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await client.session.prompt({
|
|
142
|
+
path: { id: sessionId },
|
|
143
|
+
body: {
|
|
144
|
+
noReply: true,
|
|
145
|
+
parts: [{ type: "text", text: buildDetailed(action, cls, id, summary), ignored: true }],
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
} catch {}
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DEFAULT_PROTECTED_TOOLS = new Set([
|
|
2
|
+
"task", "skill", "todowrite", "todoread",
|
|
3
|
+
"compress", "batch", "plan_enter", "plan_exit",
|
|
4
|
+
"write", "edit",
|
|
5
|
+
])
|
|
6
|
+
|
|
7
|
+
export function isToolNameProtected(toolName, extraProtected = []) {
|
|
8
|
+
if (DEFAULT_PROTECTED_TOOLS.has(toolName)) return true
|
|
9
|
+
return extraProtected.includes(toolName)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getFilePathsFromParameters(tool, params) {
|
|
13
|
+
if (!params || typeof params !== "object") return []
|
|
14
|
+
if (params.filePath) return [params.filePath]
|
|
15
|
+
if (params.file_path) return [params.file_path]
|
|
16
|
+
if (params.path) return [params.path]
|
|
17
|
+
if (params.target) return [params.target]
|
|
18
|
+
if (params.directory) return [params.directory]
|
|
19
|
+
if (tool === "glob" && params.pattern) return [params.pattern]
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isFilePathProtected(filePaths, protectedPatterns = []) {
|
|
24
|
+
if (!protectedPatterns?.length) return false
|
|
25
|
+
if (!filePaths?.length) return false
|
|
26
|
+
for (const fp of filePaths) {
|
|
27
|
+
for (const pattern of protectedPatterns) {
|
|
28
|
+
if (globMatch(fp, pattern)) return true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function globMatch(filePath, pattern) {
|
|
35
|
+
const regexStr = pattern
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
37
|
+
.replace(/\*/g, ".*")
|
|
38
|
+
.replace(/\?/g, ".")
|
|
39
|
+
try {
|
|
40
|
+
return new RegExp(`^${regexStr}$`, "i").test(filePath)
|
|
41
|
+
} catch {
|
|
42
|
+
return filePath.toLowerCase() === pattern.toLowerCase()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getTurnProtectionTags(state) {
|
|
47
|
+
const config = state.protectedTurns || {}
|
|
48
|
+
if (!config.enabled || !config.turns) return []
|
|
49
|
+
const threshold = state.currentTurn - config.turns
|
|
50
|
+
return [...state.toolIdList].filter(id => {
|
|
51
|
+
const entry = state.toolParameters.get(id)
|
|
52
|
+
return entry && entry.turn > threshold
|
|
53
|
+
})
|
|
54
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const PRUNED_OUTPUT_PLACEHOLDER = "[Output removed — information superseded or no longer needed]"
|
|
2
|
+
const PRUNED_ERROR_INPUT_PLACEHOLDER = "[input removed — failed tool call]"
|
|
3
|
+
const PRUNED_QUESTION_INPUT_PLACEHOLDER = "[questions removed — see output for answers]"
|
|
4
|
+
|
|
5
|
+
export function applyPruneTools(state, messages) {
|
|
6
|
+
if (!state.prune.tools.size) return 0
|
|
7
|
+
|
|
8
|
+
let prunedCount = 0
|
|
9
|
+
|
|
10
|
+
for (const msg of messages) {
|
|
11
|
+
if (!Array.isArray(msg.parts)) continue
|
|
12
|
+
|
|
13
|
+
for (const part of msg.parts) {
|
|
14
|
+
if (part.type !== "tool" || !part.callID) continue
|
|
15
|
+
if (!state.prune.tools.has(part.callID)) continue
|
|
16
|
+
|
|
17
|
+
prunedCount++
|
|
18
|
+
|
|
19
|
+
if (part.state?.status === "completed") {
|
|
20
|
+
if (part.tool === "question") {
|
|
21
|
+
if (part.state.input?.questions !== undefined) {
|
|
22
|
+
part.state.input.questions = PRUNED_QUESTION_INPUT_PLACEHOLDER
|
|
23
|
+
}
|
|
24
|
+
} else if (part.tool !== "edit" && part.tool !== "write") {
|
|
25
|
+
part.state = {
|
|
26
|
+
...part.state,
|
|
27
|
+
output: PRUNED_OUTPUT_PLACEHOLDER,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (part.state?.status === "error") {
|
|
33
|
+
const input = part.state.input
|
|
34
|
+
if (input && typeof input === "object") {
|
|
35
|
+
for (const key of Object.keys(input)) {
|
|
36
|
+
if (typeof input[key] === "string") {
|
|
37
|
+
input[key] = PRUNED_ERROR_INPUT_PLACEHOLDER
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return prunedCount
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function filterCompressedBlocks(state, messages) {
|
|
49
|
+
const ms = state.prune?.messages
|
|
50
|
+
if (!ms || !ms.activeBlockIds?.size || !ms.blocksById?.size) return { removed: 0, injected: 0 }
|
|
51
|
+
|
|
52
|
+
const coveredMessageIds = new Set()
|
|
53
|
+
for (const [rawId, entry] of ms.byMessageId) {
|
|
54
|
+
const hasActive = Array.isArray(entry.activeBlockIds) && entry.activeBlockIds.some(id => ms.activeBlockIds.has(id))
|
|
55
|
+
if (hasActive) coveredMessageIds.add(rawId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!coveredMessageIds.size) return { removed: 0, injected: 0 }
|
|
59
|
+
|
|
60
|
+
const activeBlocks = [...ms.activeBlockIds].map(id => ms.blocksById.get(id)).filter(Boolean)
|
|
61
|
+
const summaryByAnchor = new Map()
|
|
62
|
+
for (const block of activeBlocks) {
|
|
63
|
+
if (block.anchorMessageId && !summaryByAnchor.has(block.anchorMessageId)) {
|
|
64
|
+
summaryByAnchor.set(block.anchorMessageId, block)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let removed = 0
|
|
69
|
+
let injected = 0
|
|
70
|
+
const result = []
|
|
71
|
+
const blocksDeployed = new Set()
|
|
72
|
+
|
|
73
|
+
for (const msg of messages) {
|
|
74
|
+
const mid = msg.info?.id
|
|
75
|
+
|
|
76
|
+
if (mid && coveredMessageIds.has(mid)) {
|
|
77
|
+
if (!blocksDeployed.has(mid)) {
|
|
78
|
+
blocksDeployed.add(mid)
|
|
79
|
+
const block = summaryByAnchor.get(mid)
|
|
80
|
+
if (block) {
|
|
81
|
+
result.push({
|
|
82
|
+
parts: [{ type: "text", text: block.summary }],
|
|
83
|
+
info: { role: "system" },
|
|
84
|
+
})
|
|
85
|
+
injected++
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
removed++
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result.push(msg)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
messages.length = 0
|
|
96
|
+
messages.push(...result)
|
|
97
|
+
|
|
98
|
+
return { removed, injected }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function applyFullToolRemoval(state, messages) {
|
|
102
|
+
if (!state.prune.tools.size) return 0
|
|
103
|
+
|
|
104
|
+
const removed = []
|
|
105
|
+
|
|
106
|
+
for (const msg of messages) {
|
|
107
|
+
if (!Array.isArray(msg.parts)) continue
|
|
108
|
+
|
|
109
|
+
const toRemove = []
|
|
110
|
+
for (const part of msg.parts) {
|
|
111
|
+
if (part.type !== "tool" || !part.callID) continue
|
|
112
|
+
if (!state.prune.tools.has(part.callID)) continue
|
|
113
|
+
if (part.tool !== "edit" && part.tool !== "write") continue
|
|
114
|
+
toRemove.push(part.callID)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!toRemove.length) continue
|
|
118
|
+
|
|
119
|
+
const before = msg.parts.length
|
|
120
|
+
msg.parts = msg.parts.filter(p => p.type !== "tool" || !toRemove.includes(p.callID))
|
|
121
|
+
const after = msg.parts.length
|
|
122
|
+
if (after === 0 && before > 0) {
|
|
123
|
+
removed.push(msg.info.id)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (removed.length) {
|
|
128
|
+
const result = messages.filter(m => !removed.includes(m.info.id))
|
|
129
|
+
messages.length = 0
|
|
130
|
+
messages.push(...result)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return removed.length
|
|
134
|
+
}
|