nemoris 0.1.0 → 0.1.2
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/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
import { ProviderRegistry } from "../providers/registry.js";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_MAX_TURNS = 8;
|
|
4
|
-
const DEFAULT_KEEP_RECENT_TURNS = 4;
|
|
5
|
-
const DEFAULT_COMPACTION_MODEL = "ollama/qwen3:8b";
|
|
6
|
-
|
|
7
|
-
const SUMMARISE_PROMPT =
|
|
8
|
-
"Summarise the key facts, decisions, and context from this conversation history in 3-5 sentences. " +
|
|
9
|
-
"Preserve any specific values, filenames, decisions, or action items mentioned.";
|
|
10
|
-
|
|
11
|
-
const CONDENSE_PROMPT =
|
|
12
|
-
"Condense these conversation summaries into a single high-level summary. " +
|
|
13
|
-
"Focus on long-term goals and key outcomes.";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Compact a turns array by summarising old turns into a single context message.
|
|
17
|
-
*
|
|
18
|
-
* @param {Array} turns - Array of {role, content, timestamp} objects
|
|
19
|
-
* @param {object} options
|
|
20
|
-
* @param {number} [options.maxTurns=8] - Trigger compaction when turns.length >= maxTurns
|
|
21
|
-
* @param {number} [options.keepRecentTurns=4] - Always keep the last N turns verbatim
|
|
22
|
-
* @param {string} [options.compactionModel] - Model to use for summarisation
|
|
23
|
-
* @param {object} [options.registry] - ProviderRegistry instance (for testing)
|
|
24
|
-
* @param {object} [options.ledger] - ContextLedger instance
|
|
25
|
-
* @param {string} [options.sessionId] - Session ID
|
|
26
|
-
* @param {number} [options.condensedFanout=4] - Number of depth-0 summaries before depth-1 condensation
|
|
27
|
-
* @returns {{ compacted: boolean, turns: Array, summary?: string }}
|
|
28
|
-
*/
|
|
29
|
-
export async function compactSessionContext(turns, options = {}) {
|
|
30
|
-
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
31
|
-
const keepRecentTurns = options.keepRecentTurns ?? DEFAULT_KEEP_RECENT_TURNS;
|
|
32
|
-
const compactionModel = options.compactionModel ?? DEFAULT_COMPACTION_MODEL;
|
|
33
|
-
const condensedFanout = options.condensedFanout ?? 4;
|
|
34
|
-
const ledger = options.ledger;
|
|
35
|
-
const sessionId = options.sessionId;
|
|
36
|
-
|
|
37
|
-
if (!Array.isArray(turns) || turns.length < maxTurns) {
|
|
38
|
-
return { compacted: false, turns };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const oldTurns = turns.slice(0, turns.length - keepRecentTurns);
|
|
42
|
-
const recentTurns = turns.slice(-keepRecentTurns);
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const registry = options.registry ?? new ProviderRegistry();
|
|
46
|
-
const adapter = getAdapter(registry, compactionModel);
|
|
47
|
-
const modelName = getModelName(compactionModel);
|
|
48
|
-
|
|
49
|
-
const historyText = JSON.stringify(oldTurns, null, 2);
|
|
50
|
-
const payload = {
|
|
51
|
-
model: modelName,
|
|
52
|
-
system: SUMMARISE_PROMPT,
|
|
53
|
-
messages: [{ role: "user", content: historyText }],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
let summary;
|
|
57
|
-
try {
|
|
58
|
-
const raw = await adapter.invoke(payload);
|
|
59
|
-
const normalized = adapter.normalizeResponse(raw);
|
|
60
|
-
summary = normalized.output || normalized.summary || "";
|
|
61
|
-
|
|
62
|
-
if (!summary || summary.length > 2000) {
|
|
63
|
-
throw new Error(summary.length > 2000 ? "Summary too long" : "Empty summary");
|
|
64
|
-
}
|
|
65
|
-
} catch (err) {
|
|
66
|
-
summary = level3Fallback(oldTurns, sessionId);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// DAG integration
|
|
70
|
-
if (ledger && sessionId) {
|
|
71
|
-
try {
|
|
72
|
-
ledger.saveContextSummary({
|
|
73
|
-
session_id: sessionId,
|
|
74
|
-
depth: 0,
|
|
75
|
-
source_event_ids: "[]", // We don't have individual event IDs here
|
|
76
|
-
summary_text: summary,
|
|
77
|
-
token_count: 0,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const depth0s = ledger.getContextSummaries({ session_id: sessionId, depth: 0 });
|
|
81
|
-
if (depth0s.length >= condensedFanout) {
|
|
82
|
-
await runDepth1Condensation(ledger, sessionId, depth0s, { adapter, modelName, condensedFanout });
|
|
83
|
-
}
|
|
84
|
-
} catch (dagErr) {
|
|
85
|
-
console.error(JSON.stringify({ service: "session_compactor", event: "dag_error", error: dagErr.message }));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
compacted: true,
|
|
91
|
-
turns: [
|
|
92
|
-
{
|
|
93
|
-
role: "assistant",
|
|
94
|
-
content: `[Context summary] ${summary}`,
|
|
95
|
-
timestamp: new Date().toISOString(),
|
|
96
|
-
compacted: true,
|
|
97
|
-
},
|
|
98
|
-
...recentTurns,
|
|
99
|
-
],
|
|
100
|
-
summary,
|
|
101
|
-
};
|
|
102
|
-
} catch (err) {
|
|
103
|
-
console.error(
|
|
104
|
-
JSON.stringify({ service: "session_compactor", error: err.message })
|
|
105
|
-
);
|
|
106
|
-
return { compacted: false, turns };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function getAdapter(registry, compactionModel) {
|
|
111
|
-
const [providerPrefix] = compactionModel.split("/");
|
|
112
|
-
let providerConfig;
|
|
113
|
-
if (providerPrefix === "ollama") {
|
|
114
|
-
providerConfig = {
|
|
115
|
-
id: "ollama",
|
|
116
|
-
baseUrl: process.env.OLLAMA_BASE_URL || "http://localhost:11434",
|
|
117
|
-
};
|
|
118
|
-
} else if (providerPrefix === "openrouter") {
|
|
119
|
-
providerConfig = {
|
|
120
|
-
id: "openrouter",
|
|
121
|
-
adapter: "openrouter",
|
|
122
|
-
baseUrl: "https://openrouter.ai/api/v1",
|
|
123
|
-
authRef: "env:OPENROUTER_API_KEY",
|
|
124
|
-
};
|
|
125
|
-
} else {
|
|
126
|
-
providerConfig = {
|
|
127
|
-
id: providerPrefix,
|
|
128
|
-
adapter: providerPrefix,
|
|
129
|
-
baseUrl: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
130
|
-
authRef: "env:ANTHROPIC_API_KEY",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return registry.create(providerConfig);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getModelName(compactionModel) {
|
|
137
|
-
const [, ...rest] = compactionModel.split("/");
|
|
138
|
-
return rest.join("/") || compactionModel;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function level3Fallback(oldTurns, sessionId) {
|
|
142
|
-
const raw = JSON.stringify(oldTurns);
|
|
143
|
-
const truncated = raw.slice(0, 1500);
|
|
144
|
-
const summary = `${truncated} [compacted — full history in context_events]`;
|
|
145
|
-
console.warn(`[session-compactor] Level 3 fallback triggered for session ${sessionId || "unknown"}`);
|
|
146
|
-
return summary;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function runDepth1Condensation(ledger, sessionId, depth0s, { adapter, modelName }) {
|
|
150
|
-
const combinedText = depth0s.map(s => s.summary_text).join("\n---\n");
|
|
151
|
-
|
|
152
|
-
let condensed;
|
|
153
|
-
try {
|
|
154
|
-
const payload = {
|
|
155
|
-
model: modelName,
|
|
156
|
-
system: CONDENSE_PROMPT,
|
|
157
|
-
messages: [{ role: "user", content: combinedText }],
|
|
158
|
-
};
|
|
159
|
-
const raw = await adapter.invoke(payload);
|
|
160
|
-
const normalized = adapter.normalizeResponse(raw);
|
|
161
|
-
condensed = normalized.output || normalized.summary || "";
|
|
162
|
-
|
|
163
|
-
if (!condensed || condensed.length > 2000) {
|
|
164
|
-
throw new Error("Depth-1 summary failed or too long");
|
|
165
|
-
}
|
|
166
|
-
} catch (err) {
|
|
167
|
-
// Depth-1 fallback: concatenate first 500 chars of each depth-0 summary
|
|
168
|
-
condensed = depth0s.map(s => s.summary_text.slice(0, 500)).join("\n---\n");
|
|
169
|
-
console.warn(`[session-compactor] Level 3 fallback triggered for depth-1 session ${sessionId}`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
ledger.saveContextSummary({
|
|
173
|
-
session_id: sessionId,
|
|
174
|
-
depth: 1,
|
|
175
|
-
source_event_ids: JSON.stringify(depth0s.map(s => s.id)),
|
|
176
|
-
summary_text: condensed,
|
|
177
|
-
token_count: 0,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Delete source depth-0 summaries
|
|
181
|
-
ledger.deleteContextSummaries({ session_id: sessionId, depth: 0 });
|
|
182
|
-
}
|
|
1
|
+
import { ProviderRegistry } from "../providers/registry.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_TURNS = 8;
|
|
4
|
+
const DEFAULT_KEEP_RECENT_TURNS = 4;
|
|
5
|
+
const DEFAULT_COMPACTION_MODEL = "ollama/qwen3:8b";
|
|
6
|
+
|
|
7
|
+
const SUMMARISE_PROMPT =
|
|
8
|
+
"Summarise the key facts, decisions, and context from this conversation history in 3-5 sentences. " +
|
|
9
|
+
"Preserve any specific values, filenames, decisions, or action items mentioned.";
|
|
10
|
+
|
|
11
|
+
const CONDENSE_PROMPT =
|
|
12
|
+
"Condense these conversation summaries into a single high-level summary. " +
|
|
13
|
+
"Focus on long-term goals and key outcomes.";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compact a turns array by summarising old turns into a single context message.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array} turns - Array of {role, content, timestamp} objects
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @param {number} [options.maxTurns=8] - Trigger compaction when turns.length >= maxTurns
|
|
21
|
+
* @param {number} [options.keepRecentTurns=4] - Always keep the last N turns verbatim
|
|
22
|
+
* @param {string} [options.compactionModel] - Model to use for summarisation
|
|
23
|
+
* @param {object} [options.registry] - ProviderRegistry instance (for testing)
|
|
24
|
+
* @param {object} [options.ledger] - ContextLedger instance
|
|
25
|
+
* @param {string} [options.sessionId] - Session ID
|
|
26
|
+
* @param {number} [options.condensedFanout=4] - Number of depth-0 summaries before depth-1 condensation
|
|
27
|
+
* @returns {{ compacted: boolean, turns: Array, summary?: string }}
|
|
28
|
+
*/
|
|
29
|
+
export async function compactSessionContext(turns, options = {}) {
|
|
30
|
+
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
31
|
+
const keepRecentTurns = options.keepRecentTurns ?? DEFAULT_KEEP_RECENT_TURNS;
|
|
32
|
+
const compactionModel = options.compactionModel ?? DEFAULT_COMPACTION_MODEL;
|
|
33
|
+
const condensedFanout = options.condensedFanout ?? 4;
|
|
34
|
+
const ledger = options.ledger;
|
|
35
|
+
const sessionId = options.sessionId;
|
|
36
|
+
|
|
37
|
+
if (!Array.isArray(turns) || turns.length < maxTurns) {
|
|
38
|
+
return { compacted: false, turns };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const oldTurns = turns.slice(0, turns.length - keepRecentTurns);
|
|
42
|
+
const recentTurns = turns.slice(-keepRecentTurns);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const registry = options.registry ?? new ProviderRegistry();
|
|
46
|
+
const adapter = getAdapter(registry, compactionModel);
|
|
47
|
+
const modelName = getModelName(compactionModel);
|
|
48
|
+
|
|
49
|
+
const historyText = JSON.stringify(oldTurns, null, 2);
|
|
50
|
+
const payload = {
|
|
51
|
+
model: modelName,
|
|
52
|
+
system: SUMMARISE_PROMPT,
|
|
53
|
+
messages: [{ role: "user", content: historyText }],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let summary;
|
|
57
|
+
try {
|
|
58
|
+
const raw = await adapter.invoke(payload);
|
|
59
|
+
const normalized = adapter.normalizeResponse(raw);
|
|
60
|
+
summary = normalized.output || normalized.summary || "";
|
|
61
|
+
|
|
62
|
+
if (!summary || summary.length > 2000) {
|
|
63
|
+
throw new Error(summary.length > 2000 ? "Summary too long" : "Empty summary");
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
summary = level3Fallback(oldTurns, sessionId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// DAG integration
|
|
70
|
+
if (ledger && sessionId) {
|
|
71
|
+
try {
|
|
72
|
+
ledger.saveContextSummary({
|
|
73
|
+
session_id: sessionId,
|
|
74
|
+
depth: 0,
|
|
75
|
+
source_event_ids: "[]", // We don't have individual event IDs here
|
|
76
|
+
summary_text: summary,
|
|
77
|
+
token_count: 0,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const depth0s = ledger.getContextSummaries({ session_id: sessionId, depth: 0 });
|
|
81
|
+
if (depth0s.length >= condensedFanout) {
|
|
82
|
+
await runDepth1Condensation(ledger, sessionId, depth0s, { adapter, modelName, condensedFanout });
|
|
83
|
+
}
|
|
84
|
+
} catch (dagErr) {
|
|
85
|
+
console.error(JSON.stringify({ service: "session_compactor", event: "dag_error", error: dagErr.message }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
compacted: true,
|
|
91
|
+
turns: [
|
|
92
|
+
{
|
|
93
|
+
role: "assistant",
|
|
94
|
+
content: `[Context summary] ${summary}`,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
compacted: true,
|
|
97
|
+
},
|
|
98
|
+
...recentTurns,
|
|
99
|
+
],
|
|
100
|
+
summary,
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(
|
|
104
|
+
JSON.stringify({ service: "session_compactor", error: err.message })
|
|
105
|
+
);
|
|
106
|
+
return { compacted: false, turns };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getAdapter(registry, compactionModel) {
|
|
111
|
+
const [providerPrefix] = compactionModel.split("/");
|
|
112
|
+
let providerConfig;
|
|
113
|
+
if (providerPrefix === "ollama") {
|
|
114
|
+
providerConfig = {
|
|
115
|
+
id: "ollama",
|
|
116
|
+
baseUrl: process.env.OLLAMA_BASE_URL || "http://localhost:11434",
|
|
117
|
+
};
|
|
118
|
+
} else if (providerPrefix === "openrouter") {
|
|
119
|
+
providerConfig = {
|
|
120
|
+
id: "openrouter",
|
|
121
|
+
adapter: "openrouter",
|
|
122
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
123
|
+
authRef: "env:OPENROUTER_API_KEY",
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
providerConfig = {
|
|
127
|
+
id: providerPrefix,
|
|
128
|
+
adapter: providerPrefix,
|
|
129
|
+
baseUrl: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
130
|
+
authRef: "env:ANTHROPIC_API_KEY",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return registry.create(providerConfig);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getModelName(compactionModel) {
|
|
137
|
+
const [, ...rest] = compactionModel.split("/");
|
|
138
|
+
return rest.join("/") || compactionModel;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function level3Fallback(oldTurns, sessionId) {
|
|
142
|
+
const raw = JSON.stringify(oldTurns);
|
|
143
|
+
const truncated = raw.slice(0, 1500);
|
|
144
|
+
const summary = `${truncated} [compacted — full history in context_events]`;
|
|
145
|
+
console.warn(`[session-compactor] Level 3 fallback triggered for session ${sessionId || "unknown"}`);
|
|
146
|
+
return summary;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runDepth1Condensation(ledger, sessionId, depth0s, { adapter, modelName }) {
|
|
150
|
+
const combinedText = depth0s.map(s => s.summary_text).join("\n---\n");
|
|
151
|
+
|
|
152
|
+
let condensed;
|
|
153
|
+
try {
|
|
154
|
+
const payload = {
|
|
155
|
+
model: modelName,
|
|
156
|
+
system: CONDENSE_PROMPT,
|
|
157
|
+
messages: [{ role: "user", content: combinedText }],
|
|
158
|
+
};
|
|
159
|
+
const raw = await adapter.invoke(payload);
|
|
160
|
+
const normalized = adapter.normalizeResponse(raw);
|
|
161
|
+
condensed = normalized.output || normalized.summary || "";
|
|
162
|
+
|
|
163
|
+
if (!condensed || condensed.length > 2000) {
|
|
164
|
+
throw new Error("Depth-1 summary failed or too long");
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// Depth-1 fallback: concatenate first 500 chars of each depth-0 summary
|
|
168
|
+
condensed = depth0s.map(s => s.summary_text.slice(0, 500)).join("\n---\n");
|
|
169
|
+
console.warn(`[session-compactor] Level 3 fallback triggered for depth-1 session ${sessionId}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ledger.saveContextSummary({
|
|
173
|
+
session_id: sessionId,
|
|
174
|
+
depth: 1,
|
|
175
|
+
source_event_ids: JSON.stringify(depth0s.map(s => s.id)),
|
|
176
|
+
summary_text: condensed,
|
|
177
|
+
token_count: 0,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Delete source depth-0 summaries
|
|
181
|
+
ledger.deleteContextSummaries({ session_id: sessionId, depth: 0 });
|
|
182
|
+
}
|