@chengchenccc/oh-my-agent 0.1.1-rc.1
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 +79 -0
- package/dist/cli/args.d.ts +24 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +120 -0
- package/dist/cli/initial-input.d.ts +32 -0
- package/dist/cli/initial-input.d.ts.map +1 -0
- package/dist/cli/initial-input.js +131 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3 -0
- package/dist/core/__fixtures__/echo-model.d.ts +6 -0
- package/dist/core/__fixtures__/echo-model.d.ts.map +1 -0
- package/dist/core/__fixtures__/echo-model.js +10 -0
- package/dist/core/__fixtures__/mcp-echo-server.d.ts +2 -0
- package/dist/core/__fixtures__/mcp-echo-server.d.ts.map +1 -0
- package/dist/core/__fixtures__/mcp-echo-server.js +54 -0
- package/dist/core/agent-runtime.d.ts +21 -0
- package/dist/core/agent-runtime.d.ts.map +1 -0
- package/dist/core/agent-runtime.js +14 -0
- package/dist/core/memory/autonomous-memory.d.ts +21 -0
- package/dist/core/memory/autonomous-memory.d.ts.map +1 -0
- package/dist/core/memory/autonomous-memory.js +202 -0
- package/dist/core/persistence/in-memory-session-store.d.ts +3 -0
- package/dist/core/persistence/in-memory-session-store.d.ts.map +1 -0
- package/dist/core/persistence/in-memory-session-store.js +109 -0
- package/dist/core/persistence/session-store.contract.d.ts +5 -0
- package/dist/core/persistence/session-store.contract.d.ts.map +1 -0
- package/dist/core/persistence/session-store.contract.js +183 -0
- package/dist/core/persistence/session-store.d.ts +33 -0
- package/dist/core/persistence/session-store.d.ts.map +1 -0
- package/dist/core/persistence/session-store.js +22 -0
- package/dist/core/persistence/session-tree.d.ts +49 -0
- package/dist/core/persistence/session-tree.d.ts.map +1 -0
- package/dist/core/persistence/session-tree.js +1 -0
- package/dist/core/plugins/plugin-code.d.ts +13 -0
- package/dist/core/plugins/plugin-code.d.ts.map +1 -0
- package/dist/core/plugins/plugin-code.js +62 -0
- package/dist/core/plugins/plugin-marketplace.d.ts +72 -0
- package/dist/core/plugins/plugin-marketplace.d.ts.map +1 -0
- package/dist/core/plugins/plugin-marketplace.js +340 -0
- package/dist/core/plugins/plugin-resolve.d.ts +32 -0
- package/dist/core/plugins/plugin-resolve.d.ts.map +1 -0
- package/dist/core/plugins/plugin-resolve.js +101 -0
- package/dist/core/plugins/plugin-trust.d.ts +24 -0
- package/dist/core/plugins/plugin-trust.d.ts.map +1 -0
- package/dist/core/plugins/plugin-trust.js +89 -0
- package/dist/core/runtime/agent-event.d.ts +98 -0
- package/dist/core/runtime/agent-event.d.ts.map +1 -0
- package/dist/core/runtime/agent-event.js +1 -0
- package/dist/core/runtime/agent-loop.d.ts +142 -0
- package/dist/core/runtime/agent-loop.d.ts.map +1 -0
- package/dist/core/runtime/agent-loop.js +1008 -0
- package/dist/core/runtime/approval.d.ts +23 -0
- package/dist/core/runtime/approval.d.ts.map +1 -0
- package/dist/core/runtime/approval.js +25 -0
- package/dist/core/runtime/compaction.d.ts +22 -0
- package/dist/core/runtime/compaction.d.ts.map +1 -0
- package/dist/core/runtime/compaction.js +90 -0
- package/dist/core/runtime/context-estimate.d.ts +42 -0
- package/dist/core/runtime/context-estimate.d.ts.map +1 -0
- package/dist/core/runtime/context-estimate.js +34 -0
- package/dist/core/runtime/create-runtime.d.ts +81 -0
- package/dist/core/runtime/create-runtime.d.ts.map +1 -0
- package/dist/core/runtime/create-runtime.js +250 -0
- package/dist/core/runtime/fake-provider.d.ts +3 -0
- package/dist/core/runtime/fake-provider.d.ts.map +1 -0
- package/dist/core/runtime/fake-provider.js +87 -0
- package/dist/core/runtime/logger.d.ts +13 -0
- package/dist/core/runtime/logger.d.ts.map +1 -0
- package/dist/core/runtime/logger.js +38 -0
- package/dist/core/runtime/loop-input.d.ts +39 -0
- package/dist/core/runtime/loop-input.d.ts.map +1 -0
- package/dist/core/runtime/loop-input.js +38 -0
- package/dist/core/runtime/message-cache.d.ts +16 -0
- package/dist/core/runtime/message-cache.d.ts.map +1 -0
- package/dist/core/runtime/message-cache.js +23 -0
- package/dist/core/runtime/model-catalog.d.ts +10 -0
- package/dist/core/runtime/model-catalog.d.ts.map +1 -0
- package/dist/core/runtime/model-catalog.js +14 -0
- package/dist/core/runtime/plugin-runtime.d.ts +41 -0
- package/dist/core/runtime/plugin-runtime.d.ts.map +1 -0
- package/dist/core/runtime/plugin-runtime.js +1 -0
- package/dist/core/runtime/plugin.d.ts +86 -0
- package/dist/core/runtime/plugin.d.ts.map +1 -0
- package/dist/core/runtime/plugin.js +23 -0
- package/dist/core/runtime/process-tree.d.ts +12 -0
- package/dist/core/runtime/process-tree.d.ts.map +1 -0
- package/dist/core/runtime/process-tree.js +40 -0
- package/dist/core/runtime/prompt.d.ts +23 -0
- package/dist/core/runtime/prompt.d.ts.map +1 -0
- package/dist/core/runtime/prompt.js +29 -0
- package/dist/core/runtime/prompts.d.ts +16 -0
- package/dist/core/runtime/prompts.d.ts.map +1 -0
- package/dist/core/runtime/prompts.js +36 -0
- package/dist/core/runtime/retry.d.ts +20 -0
- package/dist/core/runtime/retry.d.ts.map +1 -0
- package/dist/core/runtime/retry.js +60 -0
- package/dist/core/runtime/run-runtime.d.ts +93 -0
- package/dist/core/runtime/run-runtime.d.ts.map +1 -0
- package/dist/core/runtime/run-runtime.js +664 -0
- package/dist/core/runtime/runtime-catalog.d.ts +12 -0
- package/dist/core/runtime/runtime-catalog.d.ts.map +1 -0
- package/dist/core/runtime/runtime-catalog.js +89 -0
- package/dist/core/runtime/stream-rules.d.ts +9 -0
- package/dist/core/runtime/stream-rules.d.ts.map +1 -0
- package/dist/core/runtime/stream-rules.js +44 -0
- package/dist/core/runtime/title.d.ts +7 -0
- package/dist/core/runtime/title.d.ts.map +1 -0
- package/dist/core/runtime/title.js +114 -0
- package/dist/core/runtime/todo.d.ts +16 -0
- package/dist/core/runtime/todo.d.ts.map +1 -0
- package/dist/core/runtime/todo.js +40 -0
- package/dist/core/runtime/tool-filter.d.ts +12 -0
- package/dist/core/runtime/tool-filter.d.ts.map +1 -0
- package/dist/core/runtime/tool-filter.js +23 -0
- package/dist/core/runtime/tool-pruning.d.ts +29 -0
- package/dist/core/runtime/tool-pruning.d.ts.map +1 -0
- package/dist/core/runtime/tool-pruning.js +105 -0
- package/dist/core/session/input-history.d.ts +8 -0
- package/dist/core/session/input-history.d.ts.map +1 -0
- package/dist/core/session/input-history.js +41 -0
- package/dist/core/session/session-file.d.ts +84 -0
- package/dist/core/session/session-file.d.ts.map +1 -0
- package/dist/core/session/session-file.js +392 -0
- package/dist/core/session/session-loop.d.ts +33 -0
- package/dist/core/session/session-loop.d.ts.map +1 -0
- package/dist/core/session/session-loop.js +30 -0
- package/dist/core/settings/project-settings.d.ts +46 -0
- package/dist/core/settings/project-settings.d.ts.map +1 -0
- package/dist/core/settings/project-settings.js +85 -0
- package/dist/core/settings/workspace-context.d.ts +10 -0
- package/dist/core/settings/workspace-context.d.ts.map +1 -0
- package/dist/core/settings/workspace-context.js +48 -0
- package/dist/core/tools/agent-fs-like.d.ts +15 -0
- package/dist/core/tools/agent-fs-like.d.ts.map +1 -0
- package/dist/core/tools/agent-fs-like.js +4 -0
- package/dist/core/tools/ask-question.d.ts +5 -0
- package/dist/core/tools/ask-question.d.ts.map +1 -0
- package/dist/core/tools/ask-question.js +51 -0
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -0
- package/dist/core/tools/bash.js +109 -0
- package/dist/core/tools/eval.d.ts +10 -0
- package/dist/core/tools/eval.d.ts.map +1 -0
- package/dist/core/tools/eval.js +83 -0
- package/dist/core/tools/file-tools.d.ts +18 -0
- package/dist/core/tools/file-tools.d.ts.map +1 -0
- package/dist/core/tools/file-tools.js +249 -0
- package/dist/core/tools/glob.d.ts +5 -0
- package/dist/core/tools/glob.d.ts.map +1 -0
- package/dist/core/tools/glob.js +54 -0
- package/dist/core/tools/grep.d.ts +5 -0
- package/dist/core/tools/grep.d.ts.map +1 -0
- package/dist/core/tools/grep.js +61 -0
- package/dist/core/tools/index.d.ts +14 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js +14 -0
- package/dist/core/tools/ls-tree.d.ts +10 -0
- package/dist/core/tools/ls-tree.d.ts.map +1 -0
- package/dist/core/tools/ls-tree.js +177 -0
- package/dist/core/tools/mcp-mount.d.ts +63 -0
- package/dist/core/tools/mcp-mount.d.ts.map +1 -0
- package/dist/core/tools/mcp-mount.js +287 -0
- package/dist/core/tools/read-image.d.ts +6 -0
- package/dist/core/tools/read-image.d.ts.map +1 -0
- package/dist/core/tools/read-image.js +98 -0
- package/dist/core/tools/skill.d.ts +11 -0
- package/dist/core/tools/skill.d.ts.map +1 -0
- package/dist/core/tools/skill.js +65 -0
- package/dist/core/tools/skills.d.ts +10 -0
- package/dist/core/tools/skills.d.ts.map +1 -0
- package/dist/core/tools/skills.js +116 -0
- package/dist/core/tools/todo-store.d.ts +8 -0
- package/dist/core/tools/todo-store.d.ts.map +1 -0
- package/dist/core/tools/todo-store.js +100 -0
- package/dist/core/tools/todo.d.ts +8 -0
- package/dist/core/tools/todo.d.ts.map +1 -0
- package/dist/core/tools/todo.js +91 -0
- package/dist/core/tools/url-guard.d.ts +12 -0
- package/dist/core/tools/url-guard.d.ts.map +1 -0
- package/dist/core/tools/url-guard.js +58 -0
- package/dist/core/tools/web-ports-std.d.ts +6 -0
- package/dist/core/tools/web-ports-std.d.ts.map +1 -0
- package/dist/core/tools/web-ports-std.js +145 -0
- package/dist/core/tools/web-ports.d.ts +56 -0
- package/dist/core/tools/web-ports.d.ts.map +1 -0
- package/dist/core/tools/web-ports.js +62 -0
- package/dist/core/tools/workspace-sandbox.d.ts +20 -0
- package/dist/core/tools/workspace-sandbox.d.ts.map +1 -0
- package/dist/core/tools/workspace-sandbox.js +84 -0
- package/dist/core/workflow/subagent-registry.d.ts +40 -0
- package/dist/core/workflow/subagent-registry.d.ts.map +1 -0
- package/dist/core/workflow/subagent-registry.js +118 -0
- package/dist/core/workflow/workflow-evaluator.d.ts +23 -0
- package/dist/core/workflow/workflow-evaluator.d.ts.map +1 -0
- package/dist/core/workflow/workflow-evaluator.js +21 -0
- package/dist/core/workflow/workflow-executor.d.ts +108 -0
- package/dist/core/workflow/workflow-executor.d.ts.map +1 -0
- package/dist/core/workflow/workflow-executor.js +549 -0
- package/dist/core/workflow/workflow-tools.d.ts +50 -0
- package/dist/core/workflow/workflow-tools.d.ts.map +1 -0
- package/dist/core/workflow/workflow-tools.js +217 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +97 -0
- package/dist/modes/json-mode.d.ts +5 -0
- package/dist/modes/json-mode.d.ts.map +1 -0
- package/dist/modes/json-mode.js +46 -0
- package/dist/modes/print-mode.d.ts +20 -0
- package/dist/modes/print-mode.d.ts.map +1 -0
- package/dist/modes/print-mode.js +82 -0
- package/dist/modes/rpc/jsonl.d.ts +23 -0
- package/dist/modes/rpc/jsonl.d.ts.map +1 -0
- package/dist/modes/rpc/jsonl.js +88 -0
- package/dist/modes/rpc/rpc-mode.d.ts +18 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-mode.js +326 -0
- package/dist/modes/tui/settings-overlay.d.ts +23 -0
- package/dist/modes/tui/settings-overlay.d.ts.map +1 -0
- package/dist/modes/tui/settings-overlay.js +136 -0
- package/dist/modes/tui/tui-commands-session.d.ts +3 -0
- package/dist/modes/tui/tui-commands-session.d.ts.map +1 -0
- package/dist/modes/tui/tui-commands-session.js +191 -0
- package/dist/modes/tui/tui-commands.d.ts +52 -0
- package/dist/modes/tui/tui-commands.d.ts.map +1 -0
- package/dist/modes/tui/tui-commands.js +373 -0
- package/dist/modes/tui/tui-components.d.ts +81 -0
- package/dist/modes/tui/tui-components.d.ts.map +1 -0
- package/dist/modes/tui/tui-components.js +248 -0
- package/dist/modes/tui/tui-format.d.ts +65 -0
- package/dist/modes/tui/tui-format.d.ts.map +1 -0
- package/dist/modes/tui/tui-format.js +246 -0
- package/dist/modes/tui/tui-frame-provider.d.ts +16 -0
- package/dist/modes/tui/tui-frame-provider.d.ts.map +1 -0
- package/dist/modes/tui/tui-frame-provider.js +31 -0
- package/dist/modes/tui/tui-interactive.d.ts +16 -0
- package/dist/modes/tui/tui-interactive.d.ts.map +1 -0
- package/dist/modes/tui/tui-interactive.js +123 -0
- package/dist/modes/tui/tui-io.d.ts +12 -0
- package/dist/modes/tui/tui-io.d.ts.map +1 -0
- package/dist/modes/tui/tui-io.js +620 -0
- package/dist/modes/tui/tui-mode.d.ts +120 -0
- package/dist/modes/tui/tui-mode.d.ts.map +1 -0
- package/dist/modes/tui/tui-mode.js +391 -0
- package/dist/modes/tui/tui-render.d.ts +54 -0
- package/dist/modes/tui/tui-render.d.ts.map +1 -0
- package/dist/modes/tui/tui-render.js +399 -0
- package/dist/modes/tui/tui-slash.d.ts +12 -0
- package/dist/modes/tui/tui-slash.d.ts.map +1 -0
- package/dist/modes/tui/tui-slash.js +64 -0
- package/dist/modes/tui/tui-tool-render.d.ts +6 -0
- package/dist/modes/tui/tui-tool-render.d.ts.map +1 -0
- package/dist/modes/tui/tui-tool-render.js +79 -0
- package/dist/modes/tui/tui-transcript-reconciler.d.ts +25 -0
- package/dist/modes/tui/tui-transcript-reconciler.d.ts.map +1 -0
- package/dist/modes/tui/tui-transcript-reconciler.js +97 -0
- package/dist/modes/tui/view-state.d.ts +67 -0
- package/dist/modes/tui/view-state.d.ts.map +1 -0
- package/dist/modes/tui/view-state.js +353 -0
- package/dist/prompts/agents/subagent.md +26 -0
- package/dist/prompts/system/base.md +20 -0
- package/dist/prompts/system/memory.md +23 -0
- package/dist/prompts/system/safety.md +12 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +2 -0
- package/dist/protocol/mapping.d.ts +25 -0
- package/dist/protocol/mapping.d.ts.map +1 -0
- package/dist/protocol/mapping.js +115 -0
- package/dist/protocol/transport.d.ts +1396 -0
- package/dist/protocol/transport.d.ts.map +1 -0
- package/dist/protocol/transport.js +170 -0
- package/package.json +49 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { extractText } from "@chengchenccc/message";
|
|
4
|
+
/**
|
|
5
|
+
* Autonomous memory pipeline (v1, OMP AutoLearn-style but per-Run).
|
|
6
|
+
*
|
|
7
|
+
* After a COMPLETED oma run the child:
|
|
8
|
+
* 1. asks the model to extract durable facts from the run transcript
|
|
9
|
+
* 2. writes them to `.oma/memory/facts/<runId>.md` (idempotent per run);
|
|
10
|
+
* 3. merges them into `.oma/memory/memory_summary.md` (a second call);
|
|
11
|
+
* 4. prepends the summary to the NEXT Run's system prompt (read side).
|
|
12
|
+
*
|
|
13
|
+
* `.oma/memory/MEMORY.md` stays agent-written — the pipeline never touches it.
|
|
14
|
+
* Every failure is swallowed: memory must never fail or slow the Run.
|
|
15
|
+
* Env: OMA_MEMORY_EXTRACT=0 disables; OMA_MEMORY_MODEL=<provider>/<model>
|
|
16
|
+
* overrides the extract/merge model (default: the Run's own model).
|
|
17
|
+
*/
|
|
18
|
+
// omp AutoLearn bar: capture sparingly, only after the run actually SOLVED
|
|
19
|
+
// or REVEALED something — one strong reusable lesson beats several vague
|
|
20
|
+
// ones, and a run that merely chatted must learn nothing.
|
|
21
|
+
const EXTRACT_PROMPT = `You are a memory extractor for a coding agent.
|
|
22
|
+
Extract ONLY lessons that would change how a future run works in this
|
|
23
|
+
workspace: a non-obvious fix, a discovered project convention, a decision
|
|
24
|
+
with its why, a pitfall with its fix, or a workflow that worked.
|
|
25
|
+
|
|
26
|
+
The bar is HIGH. The run must have actually solved or revealed something;
|
|
27
|
+
a greeting, a question answered from general knowledge, or plain chatter
|
|
28
|
+
has NOTHING to learn — return {"facts":[]} without hesitation.
|
|
29
|
+
|
|
30
|
+
NEVER extract:
|
|
31
|
+
- static configuration: skill/tool listings, model catalogs, agent
|
|
32
|
+
capabilities, repository or directory structure — re-derivable any time
|
|
33
|
+
- summaries of what the run did (the transcript already knows that)
|
|
34
|
+
- transient details without reuse value, or unverified guesses
|
|
35
|
+
|
|
36
|
+
Capture sparingly: one strong, specific lesson beats several vague ones.
|
|
37
|
+
When in doubt, don't write.
|
|
38
|
+
|
|
39
|
+
Return STRICT JSON only:
|
|
40
|
+
{"facts":[{"content":"one durable lesson","context":"where it applies (file/module/scope)"}]}`;
|
|
41
|
+
const CONSOLIDATE_PROMPT = `You maintain the agent's long-term memory summary
|
|
42
|
+
(memory/memory_summary.md). Merge the NEW facts into the EXISTING summary,
|
|
43
|
+
keeping it a compact digest of durable knowledge. Drop facts already
|
|
44
|
+
covered, and drop anything that is static configuration or re-derivable
|
|
45
|
+
from the repo. Return the new summary text only (markdown), no preamble.`;
|
|
46
|
+
const EXTRACT_TIMEOUT_MS = 60_000;
|
|
47
|
+
const TRANSCRIPT_BUDGET_CHARS = 8_000;
|
|
48
|
+
export async function extractAutonomousMemory(input) {
|
|
49
|
+
try {
|
|
50
|
+
if (process.env.OMA_MEMORY_EXTRACT === "0")
|
|
51
|
+
return { ran: false, freshFacts: 0 };
|
|
52
|
+
// Default: the cheapest available catalog model (memory extraction is
|
|
53
|
+
// quality-tolerant); OMA_MEMORY_MODEL explicitly overrides; fall back to
|
|
54
|
+
// the Run's own model when the catalog is empty/unreadable.
|
|
55
|
+
const modelRef = await resolveMemoryModel(input.modelRuntime, input.modelId);
|
|
56
|
+
const transcript = buildTranscript(input.messages, input.compactions);
|
|
57
|
+
if (transcript.trim().length === 0)
|
|
58
|
+
return { ran: false, freshFacts: 0 };
|
|
59
|
+
const facts = parseFacts(await callModel(input.modelRuntime, modelRef, `${EXTRACT_PROMPT}\n\n<transcript>\n${transcript}\n</transcript>`));
|
|
60
|
+
if (facts.length === 0)
|
|
61
|
+
return { ran: true, freshFacts: 0 };
|
|
62
|
+
const memDir = join(input.workspaceRoot, ".oma", "memory");
|
|
63
|
+
const factsDir = join(memDir, "facts");
|
|
64
|
+
// Cross-run dedup: only genuinely NEW facts are persisted per run (and
|
|
65
|
+
// fed to consolidation). Repeated learnings from later runs are dropped,
|
|
66
|
+
// so memory/facts/*.md stays a unique-facts set, not an append log.
|
|
67
|
+
const existing = readExistingFactContents(factsDir);
|
|
68
|
+
const fresh = facts.filter((f) => !existing.has(normalizeFact(f.content)));
|
|
69
|
+
if (fresh.length === 0)
|
|
70
|
+
return { ran: true, freshFacts: 0 };
|
|
71
|
+
mkdirSync(factsDir, { recursive: true });
|
|
72
|
+
writeFileSync(join(factsDir, `${input.runId}.md`), renderFacts(input.runId, fresh), "utf-8");
|
|
73
|
+
const oldSummary = readTextOrNull(join(memDir, "memory_summary.md"));
|
|
74
|
+
const newSummary = await callModel(input.modelRuntime, modelRef, `${CONSOLIDATE_PROMPT}\n\n<existing_summary>\n${oldSummary ?? "(none)"}\n</existing_summary>\n\n<new_facts>\n${renderFacts(input.runId, fresh)}\n</new_facts>`);
|
|
75
|
+
if (newSummary)
|
|
76
|
+
writeFileSync(join(memDir, "memory_summary.md"), newSummary, "utf-8");
|
|
77
|
+
return { ran: true, freshFacts: fresh.length };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Memory is best-effort: never fail or slow the Run over it.
|
|
81
|
+
console.error("[oma] autonomous memory skipped:", err instanceof Error ? err.message : String(err));
|
|
82
|
+
return { ran: false, freshFacts: 0 };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function resolveMemoryModel(modelRuntime, runModelId) {
|
|
86
|
+
const explicit = process.env.OMA_MEMORY_MODEL;
|
|
87
|
+
if (explicit)
|
|
88
|
+
return explicit;
|
|
89
|
+
try {
|
|
90
|
+
const catalog = await modelRuntime.getCatalog();
|
|
91
|
+
const candidates = catalog.models.filter((m) => m.available !== false);
|
|
92
|
+
if (candidates.length > 0) {
|
|
93
|
+
const costOf = (m) => m.cost.input + m.cost.output;
|
|
94
|
+
const cheapest = candidates.reduce((a, b) => (costOf(a) <= costOf(b) ? a : b));
|
|
95
|
+
// On a cost tie prefer the Run's own model: never surprise-upgrade
|
|
96
|
+
// (or downgrade) the memory model when the catalog gives no reason.
|
|
97
|
+
const runEntry = candidates.find((m) => `${m.providerId}/${m.modelId}` === runModelId);
|
|
98
|
+
if (runEntry && costOf(runEntry) <= costOf(cheapest))
|
|
99
|
+
return runModelId;
|
|
100
|
+
return `${cheapest.providerId}/${cheapest.modelId}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* unreadable catalog — fall back to the Run's model */
|
|
105
|
+
}
|
|
106
|
+
return runModelId;
|
|
107
|
+
}
|
|
108
|
+
function buildTranscript(messages, compactions) {
|
|
109
|
+
const lines = [];
|
|
110
|
+
for (const m of messages) {
|
|
111
|
+
const label = m.role === "assistant" ? "Assistant" : m.role === "user" ? "User" : m.role;
|
|
112
|
+
const text = extractText(m).trim();
|
|
113
|
+
if (text)
|
|
114
|
+
lines.push(`${label}: ${text}`);
|
|
115
|
+
}
|
|
116
|
+
for (const c of compactions) {
|
|
117
|
+
if (c.trim())
|
|
118
|
+
lines.push(`[compaction summary]\n${c.trim()}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n\n").slice(0, TRANSCRIPT_BUDGET_CHARS);
|
|
121
|
+
}
|
|
122
|
+
async function callModel(modelRuntime, modelRef, prompt) {
|
|
123
|
+
const slash = modelRef.indexOf("/");
|
|
124
|
+
const providerId = slash > 0 ? modelRef.slice(0, slash) : modelRef;
|
|
125
|
+
const modelId = slash > 0 ? modelRef.slice(slash + 1) : modelRef;
|
|
126
|
+
let text = "";
|
|
127
|
+
for await (const chunk of modelRuntime.stream(providerId, modelId, [{ role: "user", text: prompt }], {
|
|
128
|
+
signal: AbortSignal.timeout(EXTRACT_TIMEOUT_MS),
|
|
129
|
+
})) {
|
|
130
|
+
if (chunk.delta?.type === "text" && chunk.delta.text)
|
|
131
|
+
text += chunk.delta.text;
|
|
132
|
+
}
|
|
133
|
+
return text.trim();
|
|
134
|
+
}
|
|
135
|
+
function parseFacts(raw) {
|
|
136
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
137
|
+
if (!m)
|
|
138
|
+
return [];
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(m[0]);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(parsed.facts))
|
|
147
|
+
return [];
|
|
148
|
+
return parsed.facts.filter((f) => typeof f?.content === "string" && f.content.trim().length > 0);
|
|
149
|
+
}
|
|
150
|
+
function renderFacts(runId, facts) {
|
|
151
|
+
const lines = [`---`, `runId: ${runId}`, `createdAt: ${Date.now()}`, `---`, ""];
|
|
152
|
+
for (const f of facts) {
|
|
153
|
+
lines.push(f.context?.trim() ? `- **${f.context.trim()}** ${f.content.trim()}` : `- ${f.content.trim()}`);
|
|
154
|
+
}
|
|
155
|
+
return `${lines.join("\n")}\n`;
|
|
156
|
+
}
|
|
157
|
+
/** Normalized fact identity: trimmed, case-folded bullet content. */
|
|
158
|
+
function normalizeFact(content) {
|
|
159
|
+
return content.trim().toLowerCase();
|
|
160
|
+
}
|
|
161
|
+
/** Every fact bullet already persisted across all facts/*.md files
|
|
162
|
+
* (pipeline and agent-written alike), best-effort parse of `- ` lines. */
|
|
163
|
+
function readExistingFactContents(factsDir) {
|
|
164
|
+
const set = new Set();
|
|
165
|
+
let files;
|
|
166
|
+
try {
|
|
167
|
+
files = readdirSync(factsDir).filter((f) => f.endsWith(".md"));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return set;
|
|
171
|
+
}
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
let text;
|
|
174
|
+
try {
|
|
175
|
+
text = readFileSync(join(factsDir, file), "utf-8");
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const line of text.split("\n")) {
|
|
181
|
+
const t = line.trim();
|
|
182
|
+
if (!t.startsWith("- "))
|
|
183
|
+
continue;
|
|
184
|
+
const content = t
|
|
185
|
+
.slice(2)
|
|
186
|
+
.replace(/^\*\*[^*]+\*\*\s*/, "")
|
|
187
|
+
.trim();
|
|
188
|
+
if (content)
|
|
189
|
+
set.add(normalizeFact(content));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return set;
|
|
193
|
+
}
|
|
194
|
+
function readTextOrNull(path) {
|
|
195
|
+
try {
|
|
196
|
+
const text = readFileSync(path, "utf-8");
|
|
197
|
+
return text.trim() || null;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-session-store.d.ts","sourceRoot":"","sources":["../../../src/core/persistence/in-memory-session-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAuC,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAS5F,wBAAgB,0BAA0B,IAAI,YAAY,CAgIzD"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { validateBatch } from "./session-store.js";
|
|
2
|
+
export function createInMemorySessionStore() {
|
|
3
|
+
const sessions = new Map();
|
|
4
|
+
return {
|
|
5
|
+
async create(metadata) {
|
|
6
|
+
if (sessions.has(metadata.sessionId)) {
|
|
7
|
+
throw new Error(`Session ${metadata.sessionId} already exists`);
|
|
8
|
+
}
|
|
9
|
+
sessions.set(metadata.sessionId, {
|
|
10
|
+
metadata: { ...metadata },
|
|
11
|
+
entries: [],
|
|
12
|
+
operations: [],
|
|
13
|
+
productEntrySet: new Set(),
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
async open(sessionId) {
|
|
17
|
+
const s = sessions.get(sessionId);
|
|
18
|
+
if (!s)
|
|
19
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
20
|
+
return {
|
|
21
|
+
metadata: { ...s.metadata },
|
|
22
|
+
entries: [...s.entries],
|
|
23
|
+
operations: [...s.operations],
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
async delete(sessionId) {
|
|
27
|
+
if (!sessions.delete(sessionId)) {
|
|
28
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
async close() {
|
|
32
|
+
// In-memory store holds no external resources.
|
|
33
|
+
},
|
|
34
|
+
async appendBatch(sessionId, input) {
|
|
35
|
+
const s = sessions.get(sessionId);
|
|
36
|
+
if (!s)
|
|
37
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
38
|
+
// Validate before any mutation so a failed batch writes nothing.
|
|
39
|
+
validateBatch(input.entries);
|
|
40
|
+
const appendedIds = [];
|
|
41
|
+
let parentId = s.metadata.leafEntryId;
|
|
42
|
+
for (const entry of input.entries) {
|
|
43
|
+
const productEntryId = "productEntryId" in entry
|
|
44
|
+
? entry.productEntryId
|
|
45
|
+
: undefined;
|
|
46
|
+
if (productEntryId && s.productEntrySet.has(productEntryId))
|
|
47
|
+
continue;
|
|
48
|
+
const entryId = crypto.randomUUID().replace(/-/g, "").slice(0, 26);
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const full = {
|
|
51
|
+
...entry,
|
|
52
|
+
entryId,
|
|
53
|
+
parentId,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
};
|
|
56
|
+
s.entries.push(full);
|
|
57
|
+
if (productEntryId)
|
|
58
|
+
s.productEntrySet.add(productEntryId);
|
|
59
|
+
appendedIds.push(entryId);
|
|
60
|
+
parentId = entryId;
|
|
61
|
+
}
|
|
62
|
+
if (appendedIds.length > 0) {
|
|
63
|
+
const oldLeaf = s.metadata.leafEntryId;
|
|
64
|
+
s.metadata.leafEntryId = parentId;
|
|
65
|
+
s.metadata.updatedAt = Date.now();
|
|
66
|
+
// Mirror SQLite: entries + leaf_moved operation + cache are one logical
|
|
67
|
+
// transaction. The operation log lets reopen() reconstruct the leaf.
|
|
68
|
+
s.operations.push({ type: "leaf_moved", entryId: parentId, fromLeafId: oldLeaf });
|
|
69
|
+
}
|
|
70
|
+
return { appendedIds };
|
|
71
|
+
},
|
|
72
|
+
async moveLeaf(sessionId, entryId) {
|
|
73
|
+
const s = sessions.get(sessionId);
|
|
74
|
+
if (!s)
|
|
75
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
76
|
+
if (!s.entries.some((e) => e.entryId === entryId)) {
|
|
77
|
+
throw new Error(`Entry ${entryId} not found`);
|
|
78
|
+
}
|
|
79
|
+
s.operations.push({ type: "leaf_moved", entryId, fromLeafId: s.metadata.leafEntryId });
|
|
80
|
+
s.metadata.leafEntryId = entryId;
|
|
81
|
+
s.metadata.updatedAt = Date.now();
|
|
82
|
+
},
|
|
83
|
+
async readBranch(sessionId) {
|
|
84
|
+
const s = sessions.get(sessionId);
|
|
85
|
+
if (!s)
|
|
86
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
87
|
+
// Walk from leaf to root via parent links, then reverse
|
|
88
|
+
const result = [];
|
|
89
|
+
let currentId = s.metadata.leafEntryId;
|
|
90
|
+
const entryMap = new Map(s.entries.map((e) => [e.entryId, e]));
|
|
91
|
+
while (currentId) {
|
|
92
|
+
const entry = entryMap.get(currentId);
|
|
93
|
+
if (!entry)
|
|
94
|
+
break;
|
|
95
|
+
result.push(entry);
|
|
96
|
+
currentId = entry.parentId;
|
|
97
|
+
}
|
|
98
|
+
return result.reverse();
|
|
99
|
+
},
|
|
100
|
+
async findByProductEntryIds(sessionId, ids) {
|
|
101
|
+
const s = sessions.get(sessionId);
|
|
102
|
+
if (!s)
|
|
103
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
104
|
+
return s.entries.filter((e) => e.type === "message" &&
|
|
105
|
+
e.productEntryId &&
|
|
106
|
+
ids.includes(e.productEntryId));
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SessionStore } from "./session-store.js";
|
|
2
|
+
/** Reusable contract suite for SessionStore implementations.
|
|
3
|
+
* Each adapter test imports and invokes this. */
|
|
4
|
+
export declare function runSessionStoreContract(name: string, factory: () => Promise<SessionStore>, cleanup?: () => Promise<void>): void;
|
|
5
|
+
//# sourceMappingURL=session-store.contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.contract.d.ts","sourceRoot":"","sources":["../../../src/core/persistence/session-store.contract.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD;kDACkD;AAClD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,EACpC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC5B,IAAI,CA6MN"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
/** Reusable contract suite for SessionStore implementations.
|
|
3
|
+
* Each adapter test imports and invokes this. */
|
|
4
|
+
export function runSessionStoreContract(name, factory, cleanup) {
|
|
5
|
+
let store;
|
|
6
|
+
const sessionId = `sess-${Math.random().toString(36).slice(2, 10)}`;
|
|
7
|
+
const meta = {
|
|
8
|
+
sessionId,
|
|
9
|
+
backendKind: "oma",
|
|
10
|
+
workspaceRoot: "/ws",
|
|
11
|
+
leafEntryId: null,
|
|
12
|
+
createdAt: Date.now(),
|
|
13
|
+
updatedAt: Date.now(),
|
|
14
|
+
};
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
store = await factory();
|
|
17
|
+
await store.create(meta);
|
|
18
|
+
});
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await cleanup?.();
|
|
21
|
+
});
|
|
22
|
+
function messageEntry(text, productEntryId) {
|
|
23
|
+
return {
|
|
24
|
+
type: "message",
|
|
25
|
+
productEntryId,
|
|
26
|
+
role: "user",
|
|
27
|
+
source: "prompt",
|
|
28
|
+
message: { role: "user", text },
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
describe(`${name} SessionStore`, () => {
|
|
33
|
+
test("create and open returns correct metadata", async () => {
|
|
34
|
+
const snap = await store.open(sessionId);
|
|
35
|
+
expect(snap.metadata.sessionId).toBe(sessionId);
|
|
36
|
+
expect(snap.metadata.leafEntryId).toBeNull();
|
|
37
|
+
expect(snap.entries).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
test("appendBatch appends entries atomically and records a leaf_moved operation", async () => {
|
|
40
|
+
const result = await store.appendBatch(sessionId, {
|
|
41
|
+
entries: [messageEntry("hello", "pe1")],
|
|
42
|
+
});
|
|
43
|
+
expect(result.appendedIds).toHaveLength(1);
|
|
44
|
+
const snap = await store.open(sessionId);
|
|
45
|
+
expect(snap.metadata.leafEntryId).toBe(result.appendedIds[0]);
|
|
46
|
+
expect(snap.entries).toHaveLength(1);
|
|
47
|
+
// entries + leaf operation + cache are one logical transaction: a
|
|
48
|
+
// successful append must record a leaf_moved operation too.
|
|
49
|
+
const lastOp = snap.operations.at(-1);
|
|
50
|
+
expect(lastOp?.type).toBe("leaf_moved");
|
|
51
|
+
expect(lastOp?.entryId).toBe(result.appendedIds[0]);
|
|
52
|
+
});
|
|
53
|
+
test("appendBatch skips duplicate productEntryIds", async () => {
|
|
54
|
+
await store.appendBatch(sessionId, {
|
|
55
|
+
entries: [messageEntry("hello", "pe1")],
|
|
56
|
+
});
|
|
57
|
+
const snap = await store.open(sessionId);
|
|
58
|
+
expect(snap.entries).toHaveLength(1); // still 1, duplicate skipped
|
|
59
|
+
});
|
|
60
|
+
test("readBranch returns root-to-leaf order", async () => {
|
|
61
|
+
await store.appendBatch(sessionId, {
|
|
62
|
+
entries: [messageEntry("reply", "pe2")],
|
|
63
|
+
});
|
|
64
|
+
const branch = await store.readBranch(sessionId);
|
|
65
|
+
expect(branch.length).toBeGreaterThanOrEqual(2);
|
|
66
|
+
});
|
|
67
|
+
test("moveLeaf navigates to existing entry", async () => {
|
|
68
|
+
const entries = await store.readBranch(sessionId);
|
|
69
|
+
const firstEntryId = entries[0]?.entryId;
|
|
70
|
+
if (firstEntryId) {
|
|
71
|
+
await store.moveLeaf(sessionId, firstEntryId);
|
|
72
|
+
const snapAfter = await store.open(sessionId);
|
|
73
|
+
expect(snapAfter.metadata.leafEntryId).toBe(firstEntryId);
|
|
74
|
+
// leaf movement is a durable operation
|
|
75
|
+
expect(snapAfter.operations.at(-1)?.type).toBe("leaf_moved");
|
|
76
|
+
expect(snapAfter.operations.at(-1)?.entryId).toBe(firstEntryId);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
test("findByProductEntryIds returns matching entries", async () => {
|
|
80
|
+
const found = await store.findByProductEntryIds(sessionId, ["pe1", "nonexistent"]);
|
|
81
|
+
expect(found).toHaveLength(1);
|
|
82
|
+
expect(found[0]?.type).toBe("message");
|
|
83
|
+
});
|
|
84
|
+
test("failed batch rolls back and writes nothing", async () => {
|
|
85
|
+
const before = await store.open(sessionId);
|
|
86
|
+
const beforeCount = before.entries.length;
|
|
87
|
+
await expect(store.appendBatch(sessionId, {
|
|
88
|
+
entries: [messageEntry("valid"), { type: "bogus", createdAt: Date.now() }],
|
|
89
|
+
})).rejects.toThrow();
|
|
90
|
+
const after = await store.open(sessionId);
|
|
91
|
+
expect(after.entries).toHaveLength(beforeCount);
|
|
92
|
+
expect(after.metadata.leafEntryId).toBe(before.metadata.leafEntryId);
|
|
93
|
+
});
|
|
94
|
+
test("concurrent appends do not create accidental siblings", async () => {
|
|
95
|
+
// Capture the current leaf before appending.
|
|
96
|
+
const before = await store.open(sessionId);
|
|
97
|
+
const [a, b] = await Promise.all([
|
|
98
|
+
store.appendBatch(sessionId, { entries: [messageEntry("a")] }),
|
|
99
|
+
store.appendBatch(sessionId, { entries: [messageEntry("b")] }),
|
|
100
|
+
]);
|
|
101
|
+
expect(a.appendedIds).toHaveLength(1);
|
|
102
|
+
expect(b.appendedIds).toHaveLength(1);
|
|
103
|
+
// Both entries must be on one linear chain: leaf walks back through both.
|
|
104
|
+
const branch = await store.readBranch(sessionId);
|
|
105
|
+
const branchIds = branch.map((e) => e.entryId);
|
|
106
|
+
expect(branchIds).toContain(a.appendedIds[0]);
|
|
107
|
+
expect(branchIds).toContain(b.appendedIds[0]);
|
|
108
|
+
// No orphaned sibling: every entry is reachable from the leaf.
|
|
109
|
+
expect(branchIds).toHaveLength(new Set(branchIds).size);
|
|
110
|
+
// The two new entries are consecutive children (no shared parent).
|
|
111
|
+
const idxA = branchIds.indexOf(a.appendedIds[0]);
|
|
112
|
+
const idxB = branchIds.indexOf(b.appendedIds[0]);
|
|
113
|
+
expect(Math.abs(idxA - idxB)).toBe(1);
|
|
114
|
+
// New leaf is one of the two appended entries.
|
|
115
|
+
expect([a.appendedIds[0], b.appendedIds[0]]).toContain(branch.at(-1)?.entryId);
|
|
116
|
+
// The pre-existing leaf is the parent of the first new entry (both
|
|
117
|
+
// appends derived from the same leaf; no accidental sibling).
|
|
118
|
+
const firstNewIdx = Math.min(idxA, idxB);
|
|
119
|
+
const parentOfFirstNew = branch.at(firstNewIdx - 1)?.entryId;
|
|
120
|
+
expect(parentOfFirstNew ?? null).toBe(before.metadata.leafEntryId);
|
|
121
|
+
});
|
|
122
|
+
test("todo state persists and latest entry wins on read", async () => {
|
|
123
|
+
await store.appendBatch(sessionId, {
|
|
124
|
+
entries: [
|
|
125
|
+
{
|
|
126
|
+
type: "todo",
|
|
127
|
+
state: { items: [{ id: "t1", text: "first", status: "pending" }] },
|
|
128
|
+
createdAt: Date.now(),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
await store.appendBatch(sessionId, {
|
|
133
|
+
entries: [
|
|
134
|
+
{
|
|
135
|
+
type: "todo",
|
|
136
|
+
state: { items: [{ id: "t1", text: "first", status: "done" }] },
|
|
137
|
+
createdAt: Date.now(),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
const branch = await store.readBranch(sessionId);
|
|
142
|
+
const todoEntries = branch.filter((e) => e.type === "todo");
|
|
143
|
+
expect(todoEntries).toHaveLength(2);
|
|
144
|
+
// readTodo scans from the leaf; the latest state is the last todo entry
|
|
145
|
+
const latest = todoEntries.at(-1);
|
|
146
|
+
expect(latest.state.items[0]?.status).toBe("done");
|
|
147
|
+
});
|
|
148
|
+
test("compaction entry does not delete original entries", async () => {
|
|
149
|
+
await store.appendBatch(sessionId, {
|
|
150
|
+
entries: [
|
|
151
|
+
messageEntry("old-1", "pe3"),
|
|
152
|
+
messageEntry("old-2", "pe4"),
|
|
153
|
+
messageEntry("old-3", "pe5"),
|
|
154
|
+
messageEntry("old-4", "pe6"),
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
const branchBefore = await store.readBranch(sessionId);
|
|
158
|
+
const msgIds = branchBefore.filter((e) => e.type === "message").map((e) => e.entryId);
|
|
159
|
+
await store.appendBatch(sessionId, {
|
|
160
|
+
entries: [
|
|
161
|
+
{
|
|
162
|
+
type: "compaction",
|
|
163
|
+
summary: "compacted",
|
|
164
|
+
coversEntryIds: msgIds.slice(0, 2),
|
|
165
|
+
createdAt: Date.now(),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
const branchAfter = await store.open(sessionId);
|
|
170
|
+
// Originals retained
|
|
171
|
+
for (const id of msgIds) {
|
|
172
|
+
expect(branchAfter.entries.some((e) => e.entryId === id)).toBe(true);
|
|
173
|
+
}
|
|
174
|
+
const comp = branchAfter.entries.filter((e) => e.type === "compaction");
|
|
175
|
+
expect(comp).toHaveLength(1);
|
|
176
|
+
expect(comp[0].coversEntryIds).toEqual(msgIds.slice(0, 2));
|
|
177
|
+
});
|
|
178
|
+
test("delete removes session", async () => {
|
|
179
|
+
await store.delete(sessionId);
|
|
180
|
+
expect(store.open(sessionId)).rejects.toThrow();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CodingSessionEntry, CodingSessionMetadata, CodingSessionSnapshot } from "./session-tree.js";
|
|
2
|
+
export interface AppendBatchInput {
|
|
3
|
+
readonly entries: readonly Record<string, unknown>[];
|
|
4
|
+
}
|
|
5
|
+
export interface AppendBatchResult {
|
|
6
|
+
readonly appendedIds: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** Validate a batch before any write. Throws on the first invalid entry so
|
|
9
|
+
* both adapters fail atomically with identical semantics. */
|
|
10
|
+
export declare function validateBatch(entries: readonly Record<string, unknown>[]): void;
|
|
11
|
+
export interface SessionStore {
|
|
12
|
+
/** Create a new session. */
|
|
13
|
+
create(metadata: CodingSessionMetadata): Promise<void>;
|
|
14
|
+
/** Open an existing session, returning current snapshot. */
|
|
15
|
+
open(sessionId: string): Promise<CodingSessionSnapshot>;
|
|
16
|
+
/** Delete a session and all its data. */
|
|
17
|
+
delete(sessionId: string): Promise<void>;
|
|
18
|
+
/** Atomically append a batch of entries. ParentIds are derived from the
|
|
19
|
+
* current leaf. Duplicate productEntryIds in the batch are skipped.
|
|
20
|
+
* The leaf is updated only after the full batch succeeds. */
|
|
21
|
+
appendBatch(sessionId: string, input: AppendBatchInput): Promise<AppendBatchResult>;
|
|
22
|
+
/** Move the session leaf to an existing entry. Appends a leaf_moved operation. */
|
|
23
|
+
moveLeaf(sessionId: string, entryId: string): Promise<void>;
|
|
24
|
+
/** Read all entries on the current branch from root to leaf. */
|
|
25
|
+
readBranch(sessionId: string): Promise<readonly CodingSessionEntry[]>;
|
|
26
|
+
/** Find entries by their product entry IDs. */
|
|
27
|
+
findByProductEntryIds(sessionId: string, ids: readonly string[]): Promise<readonly CodingSessionEntry[]>;
|
|
28
|
+
/** Release backend resources (SQLite connection). Idempotent; the store is
|
|
29
|
+
* unusable afterwards. One-shot Workers call this before exiting so the
|
|
30
|
+
* session file is not held open after the outcome. */
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../../src/core/persistence/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;8DAC8D;AAC9D,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAmB/E;AAED,MAAM,WAAW,YAAY;IAC3B,4BAA4B;IAC5B,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvD,4DAA4D;IAC5D,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAExD,yCAAyC;IACzC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;kEAE8D;IAC9D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAEpF,kFAAkF;IAClF,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5D,gEAAgE;IAChE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;IAEtE,+CAA+C;IAC/C,qBAAqB,CACnB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,SAAS,MAAM,EAAE,GACrB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;IAE1C;;2DAEuD;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Validate a batch before any write. Throws on the first invalid entry so
|
|
2
|
+
* both adapters fail atomically with identical semantics. */
|
|
3
|
+
export function validateBatch(entries) {
|
|
4
|
+
for (const entry of entries) {
|
|
5
|
+
const type = entry.type;
|
|
6
|
+
if (type !== "message" && type !== "compaction" && type !== "todo") {
|
|
7
|
+
throw new Error(`Invalid entry type: ${String(type)}`);
|
|
8
|
+
}
|
|
9
|
+
if (type === "message") {
|
|
10
|
+
const msg = entry.message;
|
|
11
|
+
if (!msg || typeof msg !== "object" || typeof msg.role !== "string") {
|
|
12
|
+
throw new Error("Message entry requires a message object with a role");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (type === "compaction" && typeof entry.summary !== "string") {
|
|
16
|
+
throw new Error("Compaction entry requires a summary string");
|
|
17
|
+
}
|
|
18
|
+
if (type === "todo" && typeof entry.state !== "object") {
|
|
19
|
+
throw new Error("Todo entry requires a state object");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Message } from "@chengchenccc/message";
|
|
2
|
+
export interface MessageEntry {
|
|
3
|
+
readonly type: "message";
|
|
4
|
+
readonly entryId: string;
|
|
5
|
+
readonly parentId: string | null;
|
|
6
|
+
readonly role: "user" | "assistant" | "system" | "tool";
|
|
7
|
+
readonly source: "product_history" | "meta" | "prompt" | "steer" | "follow_up" | "assistant" | "tool_result";
|
|
8
|
+
readonly message: Message;
|
|
9
|
+
readonly createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CompactionEntry {
|
|
12
|
+
readonly type: "compaction";
|
|
13
|
+
readonly entryId: string;
|
|
14
|
+
readonly parentId: string | null;
|
|
15
|
+
readonly summary: string;
|
|
16
|
+
readonly coversEntryIds: readonly string[];
|
|
17
|
+
/** Estimated token count of covered messages before compaction. */
|
|
18
|
+
readonly tokensBefore?: number;
|
|
19
|
+
/** Entry IDs deliberately left uncovered (the retained tail). */
|
|
20
|
+
readonly retainedEntryIds?: readonly string[];
|
|
21
|
+
readonly createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TodoStateEntry {
|
|
24
|
+
readonly type: "todo";
|
|
25
|
+
readonly entryId: string;
|
|
26
|
+
readonly parentId: string | null;
|
|
27
|
+
readonly state: Readonly<Record<string, unknown>>;
|
|
28
|
+
readonly createdAt: number;
|
|
29
|
+
}
|
|
30
|
+
export type CodingSessionEntry = MessageEntry | CompactionEntry | TodoStateEntry;
|
|
31
|
+
export type CodingSessionOperation = {
|
|
32
|
+
readonly type: "leaf_moved";
|
|
33
|
+
readonly entryId: string;
|
|
34
|
+
readonly fromLeafId: string | null;
|
|
35
|
+
};
|
|
36
|
+
export interface CodingSessionMetadata {
|
|
37
|
+
readonly sessionId: string;
|
|
38
|
+
readonly backendKind: string;
|
|
39
|
+
readonly workspaceRoot: string;
|
|
40
|
+
readonly leafEntryId: string | null;
|
|
41
|
+
readonly createdAt: number;
|
|
42
|
+
readonly updatedAt: number;
|
|
43
|
+
}
|
|
44
|
+
export interface CodingSessionSnapshot {
|
|
45
|
+
readonly metadata: CodingSessionMetadata;
|
|
46
|
+
readonly entries: readonly CodingSessionEntry[];
|
|
47
|
+
readonly operations: readonly CodingSessionOperation[];
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=session-tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-tree.d.ts","sourceRoot":"","sources":["../../../src/core/persistence/session-tree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAIrD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxD,QAAQ,CAAC,MAAM,EACX,iBAAiB,GACjB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,WAAW,GACX,WAAW,GACX,aAAa,CAAC;IAClB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,mEAAmE;IACnE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AACD,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,eAAe,GAAG,cAAc,CAAC;AAEjF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC,CAAC;AAIF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,SAAS,sBAAsB,EAAE,CAAC;CACxD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PluginHooks, PluginTool } from "../runtime/plugin.js";
|
|
2
|
+
export interface PluginCodeResult {
|
|
3
|
+
readonly ok: boolean;
|
|
4
|
+
readonly tools?: readonly PluginTool[];
|
|
5
|
+
readonly hooks?: PluginHooks;
|
|
6
|
+
readonly warnings: readonly string[];
|
|
7
|
+
readonly error?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Load ONE code entry (tools or hooks) from an installed plugin root.
|
|
10
|
+
* Never throws: any failure degrades to {ok:false, error} (spec failure
|
|
11
|
+
* semantics — a broken plugin must never fail the Run). */
|
|
12
|
+
export declare function loadPluginCode(root: string, entry: string): Promise<PluginCodeResult>;
|
|
13
|
+
//# sourceMappingURL=plugin-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-code.d.ts","sourceRoot":"","sources":["../../../src/core/plugins/plugin-code.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAcpE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAYD;;4DAE4D;AAC5D,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqC3F"}
|