@mercury-fw/core 0.25.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/CHANGELOG.md +19 -0
- package/README.md +38 -0
- package/dist/index.d.ts +23 -0
- package/dist/src/admin/cli-routes.d.ts +22 -0
- package/dist/src/admin/env-file.d.ts +1 -0
- package/dist/src/admin/model-routes.d.ts +26 -0
- package/dist/src/admin/qdrant-scroll.d.ts +34 -0
- package/dist/src/admin/server.d.ts +40 -0
- package/dist/src/admin/wiki-routes.d.ts +31 -0
- package/dist/src/compose.d.ts +42 -0
- package/dist/src/config/define-config.d.ts +31 -0
- package/dist/src/cron/idle-session-cron.d.ts +80 -0
- package/dist/src/cron/idle-session-scanner.d.ts +16 -0
- package/dist/src/cron/self-review-cron.d.ts +55 -0
- package/dist/src/cron/semantic-consolidation.d.ts +71 -0
- package/dist/src/memory/embedder.d.ts +9 -0
- package/dist/src/memory/episodic-store.d.ts +121 -0
- package/dist/src/memory/memory-provider.d.ts +51 -0
- package/dist/src/memory/semantic-facts-store.d.ts +37 -0
- package/dist/src/memory/tool-corrections-store.d.ts +26 -0
- package/dist/src/memory/verbatim-archive-store.d.ts +86 -0
- package/dist/src/model/client.d.ts +24 -0
- package/dist/src/model/context-size.d.ts +30 -0
- package/dist/src/plugins/manifest.d.ts +29 -0
- package/dist/src/plugins/plugin-loader.d.ts +85 -0
- package/dist/src/router/channel-loader.d.ts +30 -0
- package/dist/src/router/provider.d.ts +7 -0
- package/dist/src/router/terminal-provider.d.ts +37 -0
- package/dist/src/router/terminal.d.ts +41 -0
- package/dist/src/router/tool-log.d.ts +65 -0
- package/dist/src/router/turn-runner.d.ts +86 -0
- package/dist/src/session/agent-turn.d.ts +266 -0
- package/dist/src/session/context-primer.d.ts +16 -0
- package/dist/src/session/episodic-summarizer.d.ts +25 -0
- package/dist/src/session/history.d.ts +95 -0
- package/dist/src/session/pending-confirmation.d.ts +8 -0
- package/dist/src/session/read-skill-tool.d.ts +4 -0
- package/dist/src/session/semantic-fact-extractor.d.ts +45 -0
- package/dist/src/session/step-info.d.ts +24 -0
- package/dist/src/session/summarizer.d.ts +23 -0
- package/dist/src/session/system-prompt.d.ts +38 -0
- package/dist/src/session/tool-correction-extractor.d.ts +43 -0
- package/dist/src/session/tool-log-buffer.d.ts +24 -0
- package/dist/src/session/tool-log-recall-tool.d.ts +18 -0
- package/dist/src/session/tool-start-hook.d.ts +57 -0
- package/dist/src/tools/display-store.d.ts +36 -0
- package/dist/src/tools/present-tool.d.ts +23 -0
- package/dist/src/wiki/frontmatter-schema.d.ts +53 -0
- package/dist/src/wiki/index-entry.d.ts +15 -0
- package/dist/src/wiki/orphan-detector.d.ts +1 -0
- package/dist/src/wiki/self-review-runner.d.ts +48 -0
- package/dist/src/wiki/self-review-tools.d.ts +22 -0
- package/dist/src/wiki/vault-cli.d.ts +2 -0
- package/dist/src/wiki/vault-init.d.ts +7 -0
- package/dist/src/wiki/wiki-note.d.ts +62 -0
- package/dist/src/wiki/wiki-read.d.ts +27 -0
- package/dist/src/wiki/wiki-tools.d.ts +7 -0
- package/index.ts +23 -0
- package/package.json +49 -0
- package/src/admin/cli-routes.ts +48 -0
- package/src/admin/env-file.ts +29 -0
- package/src/admin/model-routes.ts +71 -0
- package/src/admin/public/index.html +416 -0
- package/src/admin/qdrant-scroll.ts +45 -0
- package/src/admin/server.ts +188 -0
- package/src/admin/wiki-routes.ts +93 -0
- package/src/compose.ts +599 -0
- package/src/config/define-config.ts +35 -0
- package/src/cron/.gitkeep +0 -0
- package/src/cron/idle-session-cron.ts +144 -0
- package/src/cron/idle-session-scanner.ts +37 -0
- package/src/cron/self-review-cron.ts +103 -0
- package/src/cron/semantic-consolidation.ts +228 -0
- package/src/memory/.gitkeep +0 -0
- package/src/memory/embedder.ts +15 -0
- package/src/memory/episodic-store.ts +183 -0
- package/src/memory/memory-provider.ts +98 -0
- package/src/memory/semantic-facts-store.ts +89 -0
- package/src/memory/tool-corrections-store.ts +72 -0
- package/src/memory/verbatim-archive-store.ts +202 -0
- package/src/model/client.ts +33 -0
- package/src/model/context-size.ts +42 -0
- package/src/plugins/manifest.ts +47 -0
- package/src/plugins/plugin-loader.ts +205 -0
- package/src/router/channel-loader.ts +56 -0
- package/src/router/provider.ts +7 -0
- package/src/router/terminal-provider.ts +155 -0
- package/src/router/terminal.ts +151 -0
- package/src/router/tool-log.ts +116 -0
- package/src/router/turn-runner.ts +205 -0
- package/src/session/agent-turn.ts +391 -0
- package/src/session/context-primer.ts +134 -0
- package/src/session/episodic-summarizer.ts +38 -0
- package/src/session/history.ts +168 -0
- package/src/session/pending-confirmation.ts +8 -0
- package/src/session/read-skill-tool.ts +38 -0
- package/src/session/semantic-fact-extractor.ts +69 -0
- package/src/session/step-info.ts +27 -0
- package/src/session/summarizer.ts +36 -0
- package/src/session/system-prompt.ts +142 -0
- package/src/session/tool-correction-extractor.ts +133 -0
- package/src/session/tool-log-buffer.ts +73 -0
- package/src/session/tool-log-recall-tool.ts +38 -0
- package/src/session/tool-start-hook.ts +164 -0
- package/src/tools/display-store.ts +89 -0
- package/src/tools/present-tool.ts +41 -0
- package/src/wiki/.gitkeep +0 -0
- package/src/wiki/frontmatter-schema.ts +49 -0
- package/src/wiki/index-entry.ts +59 -0
- package/src/wiki/orphan-detector.ts +61 -0
- package/src/wiki/self-review-runner.ts +133 -0
- package/src/wiki/self-review-tools.ts +162 -0
- package/src/wiki/vault-cli.ts +143 -0
- package/src/wiki/vault-init.ts +43 -0
- package/src/wiki/wiki-note.ts +326 -0
- package/src/wiki/wiki-read.ts +122 -0
- package/src/wiki/wiki-tools.ts +112 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3 (Qdrant) episodic storage: one point per closed,
|
|
3
|
+
* summarized session — a raw, dated "what happened", not an
|
|
4
|
+
* interpretation. Consolidation into semantic memory (per-topic
|
|
5
|
+
* promotion into the wiki) is a separate, later concern that
|
|
6
|
+
* reads from this collection; this module only ever writes to it.
|
|
7
|
+
*
|
|
8
|
+
* `QdrantClientLike` describes only the subset of `@qdrant/js-client-rest`'s
|
|
9
|
+
* `QdrantClient` this file actually calls — real client instances satisfy
|
|
10
|
+
* it structurally, tests use a plain object instead of a real connection.
|
|
11
|
+
*/
|
|
12
|
+
export type QdrantClientLike = {
|
|
13
|
+
getCollections(): Promise<{
|
|
14
|
+
collections: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
}>;
|
|
17
|
+
}>;
|
|
18
|
+
createCollection(name: string, params: {
|
|
19
|
+
vectors: {
|
|
20
|
+
size: number;
|
|
21
|
+
distance: "Cosine" | "Euclid" | "Dot" | "Manhattan";
|
|
22
|
+
};
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
upsert(name: string, params: {
|
|
25
|
+
points: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
vector: number[];
|
|
28
|
+
payload: Record<string, unknown>;
|
|
29
|
+
}>;
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
/** @qdrant/js-client-rest 1.19 removed `search` in favour of the universal
|
|
32
|
+
* `query` endpoint: the vector moves to `query`, and results come back under
|
|
33
|
+
* `.points` instead of as a bare array. */
|
|
34
|
+
query(name: string, params: {
|
|
35
|
+
query: number[];
|
|
36
|
+
filter: Record<string, unknown>;
|
|
37
|
+
limit: number;
|
|
38
|
+
with_payload: boolean;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
points: Array<{
|
|
41
|
+
id: string | number;
|
|
42
|
+
score: number;
|
|
43
|
+
payload?: Record<string, unknown> | null;
|
|
44
|
+
}>;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Optional — not part of the similarity-search surface every caller
|
|
48
|
+
* needs, only used by `getLastSessionEpisodicSummaries` below. Optional
|
|
49
|
+
* so `semantic-facts-store.ts`/`tool-corrections-store.ts` (which share
|
|
50
|
+
* this type but never call `scroll`) don't need a stub in every test
|
|
51
|
+
* fixture.
|
|
52
|
+
*/
|
|
53
|
+
scroll?(name: string, params: {
|
|
54
|
+
filter: Record<string, unknown>;
|
|
55
|
+
order_by: {
|
|
56
|
+
key: string;
|
|
57
|
+
direction: "asc" | "desc";
|
|
58
|
+
};
|
|
59
|
+
limit: number;
|
|
60
|
+
/** Opaque pagination cursor (from a prior page's `next_page_offset`). */
|
|
61
|
+
offset?: string | number | Record<string, unknown> | null;
|
|
62
|
+
/** Whether to return point payloads (`listVerbatimBySession` needs them). */
|
|
63
|
+
with_payload?: boolean;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
points: Array<{
|
|
66
|
+
id: string | number;
|
|
67
|
+
payload?: Record<string, unknown> | null;
|
|
68
|
+
}>;
|
|
69
|
+
next_page_offset?: string | number | Record<string, unknown> | null;
|
|
70
|
+
}>;
|
|
71
|
+
/** Optional — same reasoning as `scroll` above: only `ensureEpisodicCollection` needs it. */
|
|
72
|
+
createPayloadIndex?(name: string, params: {
|
|
73
|
+
field_name: string;
|
|
74
|
+
field_schema: "datetime" | "keyword";
|
|
75
|
+
}): Promise<unknown>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Creates `collectionName` (cosine distance, `vectorSize`-dim) if it
|
|
79
|
+
* doesn't already exist, then ensures the `timestamp`/`userId` payload
|
|
80
|
+
* indexes exist regardless — `getLastSessionEpisodicSummaries`'s
|
|
81
|
+
* `order_by`/filter scroll queries fail with an HTTP 400 without them.
|
|
82
|
+
* Idempotent either way (Qdrant no-ops re-creating an existing index), so
|
|
83
|
+
* safe to call on every startup, including against a collection that
|
|
84
|
+
* predates this fix.
|
|
85
|
+
*/
|
|
86
|
+
export declare function ensureEpisodicCollection(client: QdrantClientLike, collectionName: string, vectorSize: number): Promise<void>;
|
|
87
|
+
export type EpisodicSummary = {
|
|
88
|
+
userId: string;
|
|
89
|
+
sessionKey: string;
|
|
90
|
+
summary: string;
|
|
91
|
+
timestamp: string;
|
|
92
|
+
};
|
|
93
|
+
/** Embeds `entry.summary` and upserts it as a new point in `collectionName`, payload carrying the full entry. */
|
|
94
|
+
export declare function storeEpisodicSummary(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, entry: EpisodicSummary): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Past episodic events for a specific user, most relevant to `queryText`
|
|
97
|
+
* (e.g. "notifications about KAN-123") — lets Mercury see how many times
|
|
98
|
+
* it already notified this user about a given item before composing a
|
|
99
|
+
* message. Not a general-purpose semantic consolidation/pattern-extraction
|
|
100
|
+
* engine (that doesn't exist here) — this only ever reads, never writes
|
|
101
|
+
* or promotes anything.
|
|
102
|
+
*/
|
|
103
|
+
export declare function searchEpisodicMemory(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, query: {
|
|
104
|
+
userId: string;
|
|
105
|
+
queryText: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
}): Promise<EpisodicSummary[]>;
|
|
108
|
+
/**
|
|
109
|
+
* The last closed session's episodic entries for `userId`, most recent
|
|
110
|
+
* first — up to `limit` entries, all sharing the same `sessionKey` as the
|
|
111
|
+
* single most recent entry. Used to seed a brand-new session's context
|
|
112
|
+
* primer with "what happened last time"; not similarity-scoped like
|
|
113
|
+
* `searchEpisodicMemory` (there's no query yet to compare against at
|
|
114
|
+
* session start). Returns an empty array (never throws) if the client
|
|
115
|
+
* doesn't support `scroll`, or if the user has no prior episodic entries —
|
|
116
|
+
* this is enrichment, the caller must work fine without it.
|
|
117
|
+
*/
|
|
118
|
+
export declare function getLastSessionEpisodicSummaries(client: QdrantClientLike, collectionName: string, query: {
|
|
119
|
+
userId: string;
|
|
120
|
+
limit?: number;
|
|
121
|
+
}): Promise<EpisodicSummary[]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory-provider seam (issue #4, built ahead of the broader #30
|
|
3
|
+
* design): a memory mechanism that hooks the turn — capturing the
|
|
4
|
+
* exchange after it happens, and contributing its own recall tools — kept
|
|
5
|
+
* behind a small interface so the composition root wires it like any
|
|
6
|
+
* other provider rather than hardwiring it into the pipeline. Only the two
|
|
7
|
+
* hooks #4 exercises live here; the wider generalization (priming,
|
|
8
|
+
* summarizer, wiki strategy as swappable providers) is deferred to #30.
|
|
9
|
+
*
|
|
10
|
+
* The one implementation today is the verbatim archive
|
|
11
|
+
* (`./verbatim-archive-store.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import { type Tool } from "ai";
|
|
14
|
+
import type { QdrantClientLike } from "./episodic-store.ts";
|
|
15
|
+
/** One side of an exchange to archive — the provider stamps its own durable timestamp. */
|
|
16
|
+
export type VerbatimExchange = {
|
|
17
|
+
userId: string;
|
|
18
|
+
sessionKey: string;
|
|
19
|
+
role: "user" | "assistant";
|
|
20
|
+
content: string;
|
|
21
|
+
};
|
|
22
|
+
/** Per-turn identity a provider's recall tools are scoped to. `userId` is what makes recall cross-session. */
|
|
23
|
+
export type MemoryProviderContext = {
|
|
24
|
+
sessionKey: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A memory mechanism the composition root can wire in. `captureExchange`
|
|
29
|
+
* runs after a turn resolves (post-message); `sessionTools` contributes
|
|
30
|
+
* model-invocable recall for the calling session. Both optional — a
|
|
31
|
+
* provider may only capture, or only recall.
|
|
32
|
+
*/
|
|
33
|
+
export type MemoryProvider = {
|
|
34
|
+
captureExchange?(exchange: VerbatimExchange): Promise<void>;
|
|
35
|
+
sessionTools?(ctx: MemoryProviderContext): Record<string, Tool>;
|
|
36
|
+
};
|
|
37
|
+
export type VerbatimArchiveProviderDeps = {
|
|
38
|
+
client: QdrantClientLike;
|
|
39
|
+
collectionName: string;
|
|
40
|
+
embed: (text: string) => Promise<number[]>;
|
|
41
|
+
/** Test seam; defaults to `() => new Date()`. The provider owns the durable ordering timestamp. */
|
|
42
|
+
now?: () => Date;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Builds the verbatim-archive provider: captures every user/assistant
|
|
46
|
+
* message verbatim to its Qdrant collection, and exposes a
|
|
47
|
+
* `recall_verbatim` tool that similarity-searches that archive scoped to
|
|
48
|
+
* the calling user — letting a later session resurface what was actually
|
|
49
|
+
* said before, beyond the live history window.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createVerbatimArchiveProvider(deps: VerbatimArchiveProviderDeps): MemoryProvider;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3 (Qdrant) semantic facts storage — one collection, separate from
|
|
3
|
+
* `episodic-store.ts`. Episodic points are a raw dated account of a whole
|
|
4
|
+
* session; semantic facts are `{topic, value}` pairs extracted from a
|
|
5
|
+
* session, clustered per user+topic, and deterministically promoted to a
|
|
6
|
+
* standing wiki note. This module only owns the collection lifecycle;
|
|
7
|
+
* extraction, clustering, and promotion are separate concerns built on
|
|
8
|
+
* top of it.
|
|
9
|
+
*/
|
|
10
|
+
import type { QdrantClientLike } from "./episodic-store.ts";
|
|
11
|
+
/** Creates `collectionName` (cosine distance, `vectorSize`-dim) if it doesn't already exist — safe to call on every startup. */
|
|
12
|
+
export declare function ensureSemanticFactsCollection(client: QdrantClientLike, collectionName: string, vectorSize: number): Promise<void>;
|
|
13
|
+
export type SemanticFactEntry = {
|
|
14
|
+
userId: string;
|
|
15
|
+
topic: string;
|
|
16
|
+
value: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Embeds `entry.topic` alone (never `topic + value`) and upserts it as a
|
|
21
|
+
* new point in `collectionName`, payload carrying the full entry. One
|
|
22
|
+
* point per extracted fact, never an update-in-place — consolidation
|
|
23
|
+
* reads the whole history back via `searchSemanticFactsByTopic` and
|
|
24
|
+
* decides what to promote.
|
|
25
|
+
*/
|
|
26
|
+
export declare function storeSemanticFact(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, entry: SemanticFactEntry): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Past facts for a specific user, clustered by topic similarity — the
|
|
29
|
+
* read side consolidation uses to gather "every occurrence of roughly
|
|
30
|
+
* this topic" before counting a dominant value. Filters by userId so
|
|
31
|
+
* one user's facts never leak into another's cluster.
|
|
32
|
+
*/
|
|
33
|
+
export declare function searchSemanticFactsByTopic(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, query: {
|
|
34
|
+
userId: string;
|
|
35
|
+
topic: string;
|
|
36
|
+
limit?: number;
|
|
37
|
+
}): Promise<SemanticFactEntry[]>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3 (Qdrant) procedural-correction staging — separate collection
|
|
3
|
+
* from `semantic-facts-store.ts`: that one is keyed by userId (a fact
|
|
4
|
+
* about a specific person), this one is keyed by tool (a fact about a
|
|
5
|
+
* CLI, true for whoever uses it next). Kept as its own collection rather
|
|
6
|
+
* than overloading `semantic-facts-store.ts`'s `userId` field with a tool
|
|
7
|
+
* name — a tool name in a field called `userId` would be a standing
|
|
8
|
+
* source of confusion for anyone reading this collection later.
|
|
9
|
+
*/
|
|
10
|
+
import type { QdrantClientLike } from "./episodic-store.ts";
|
|
11
|
+
/** Creates `collectionName` (cosine distance, `vectorSize`-dim) if it doesn't already exist — safe to call on every startup. */
|
|
12
|
+
export declare function ensureToolCorrectionsCollection(client: QdrantClientLike, collectionName: string, vectorSize: number): Promise<void>;
|
|
13
|
+
export type ToolCorrectionEntry = {
|
|
14
|
+
tool: string;
|
|
15
|
+
topic: string;
|
|
16
|
+
value: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
};
|
|
19
|
+
/** Embeds `entry.topic` alone (never `topic + value`), same reasoning as `storeSemanticFact` — upserts a new point, never an update-in-place. */
|
|
20
|
+
export declare function storeToolCorrection(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, entry: ToolCorrectionEntry): Promise<void>;
|
|
21
|
+
/** Past corrections for a specific tool, clustered by topic similarity — filters by `tool` so one tool's corrections never leak into another's cluster. */
|
|
22
|
+
export declare function searchToolCorrectionsByTopic(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, query: {
|
|
23
|
+
tool: string;
|
|
24
|
+
topic: string;
|
|
25
|
+
limit?: number;
|
|
26
|
+
}): Promise<ToolCorrectionEntry[]>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbatim conversation archive (issue #4): a durable, lossless
|
|
3
|
+
* Qdrant collection holding the complete user↔model exchange, one point
|
|
4
|
+
* per message, distinct from every other memory layer.
|
|
5
|
+
*
|
|
6
|
+
* How it differs from the neighbours in this directory:
|
|
7
|
+
* - Layer-1 history (`../session/history.ts`) is a sliding window that
|
|
8
|
+
* summarizes itself away — lossy by design.
|
|
9
|
+
* - The episodic store (`./episodic-store.ts`) captures a *summary* of a
|
|
10
|
+
* closed session — derived, not raw.
|
|
11
|
+
* This archive instead keeps what was actually said, verbatim, so a later
|
|
12
|
+
* session can resurface it and re-read it in light of new facts. It only
|
|
13
|
+
* ever appends and reads; nothing here evicts (retention bounding is a
|
|
14
|
+
* separate, later concern).
|
|
15
|
+
*
|
|
16
|
+
* Reuses `episodic-store.ts`'s structural `QdrantClientLike` — the same
|
|
17
|
+
* subset of the real client every store in this directory shares.
|
|
18
|
+
*/
|
|
19
|
+
import type { QdrantClientLike } from "./episodic-store.ts";
|
|
20
|
+
/**
|
|
21
|
+
* Creates `collectionName` (cosine distance, `vectorSize`-dim) if it
|
|
22
|
+
* doesn't already exist, then ensures the `userId` keyword payload index
|
|
23
|
+
* regardless — `searchVerbatim` filters by `userId` server-side, and the
|
|
24
|
+
* index keeps that filter usable as the archive grows. Idempotent
|
|
25
|
+
* (Qdrant no-ops re-creating an existing index), so safe to call on every
|
|
26
|
+
* startup, including against a collection created before this existed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureVerbatimCollection(client: QdrantClientLike, collectionName: string, vectorSize: number): Promise<void>;
|
|
29
|
+
/** A single verbatim message as it was emitted in chat. `timestamp` (ISO 8601, ms precision) is the durable ordering key. */
|
|
30
|
+
export type VerbatimMessage = {
|
|
31
|
+
userId: string;
|
|
32
|
+
sessionKey: string;
|
|
33
|
+
role: "user" | "assistant";
|
|
34
|
+
content: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
};
|
|
37
|
+
/** Embeds `entry.content` and upserts it as a new point in `collectionName`, payload carrying the full verbatim message. */
|
|
38
|
+
export declare function appendVerbatimMessage(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, entry: VerbatimMessage): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* The verbatim messages for a specific user most relevant to `queryText`
|
|
41
|
+
* — e.g. "what did we say about KAN-1 last week". Filtered by `userId` so
|
|
42
|
+
* one user's transcript never leaks into another's. Only reads; returns
|
|
43
|
+
* the raw messages, ordering left to the caller (each carries its own
|
|
44
|
+
* `timestamp`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function searchVerbatim(client: QdrantClientLike, collectionName: string, embed: (text: string) => Promise<number[]>, query: {
|
|
47
|
+
userId: string;
|
|
48
|
+
queryText: string;
|
|
49
|
+
limit?: number;
|
|
50
|
+
}): Promise<VerbatimMessage[]>;
|
|
51
|
+
/** One conversation in the list view: its key, the timestamp of its most recent message, and a short preview of it. */
|
|
52
|
+
export type VerbatimSession = {
|
|
53
|
+
sessionKey: string;
|
|
54
|
+
lastTimestamp: string;
|
|
55
|
+
preview: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The known conversations, most-recently-active first — the sidebar a UI shows
|
|
59
|
+
* to switch between conversations. Scrolls the newest `SESSION_SCAN_LIMIT`
|
|
60
|
+
* messages (timestamp desc) and dedups by `sessionKey`, keeping each
|
|
61
|
+
* conversation's most recent message for its timestamp and preview, then caps
|
|
62
|
+
* the result at `limit`. Returns empty if the client can't scroll.
|
|
63
|
+
*/
|
|
64
|
+
export declare function listVerbatimSessions(client: QdrantClientLike, collectionName: string, query: {
|
|
65
|
+
limit: number;
|
|
66
|
+
}): Promise<{
|
|
67
|
+
conversations: VerbatimSession[];
|
|
68
|
+
}>;
|
|
69
|
+
/** A page of a conversation's verbatim messages, plus the opaque cursor for the next page (null when exhausted). */
|
|
70
|
+
export type VerbatimPage = {
|
|
71
|
+
messages: VerbatimMessage[];
|
|
72
|
+
nextOffset: string | number | Record<string, unknown> | null;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The verbatim messages of one conversation (`sessionKey`) in chronological
|
|
76
|
+
* order — the durable transcript a UI renders when it (re)loads a conversation.
|
|
77
|
+
* Unlike `searchVerbatim` this is a plain scroll (no similarity), filtered by
|
|
78
|
+
* `sessionKey` (the true per-conversation key; in the HTTP surface it equals
|
|
79
|
+
* the client's `conversationId`) and ordered by `timestamp` ascending, paged
|
|
80
|
+
* via the opaque `offset` cursor. Returns empty if the client can't scroll.
|
|
81
|
+
*/
|
|
82
|
+
export declare function listVerbatimBySession(client: QdrantClientLike, collectionName: string, query: {
|
|
83
|
+
sessionKey: string;
|
|
84
|
+
limit: number;
|
|
85
|
+
offset?: string | number | Record<string, unknown> | null;
|
|
86
|
+
}): Promise<VerbatimPage>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constructs the Ollama provider Mercury uses for every LLM call (main
|
|
3
|
+
* agent turns and Layer 1 summarization alike).
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: the Ollama endpoint Mercury talks to varies by
|
|
6
|
+
* deployment (a local GPU box in development, a different host in
|
|
7
|
+
* production) and must always come from configuration, never be
|
|
8
|
+
* hardcoded or silently defaulted to `localhost` — this is the single
|
|
9
|
+
* place that reads that configuration, so the fail-fast behavior only
|
|
10
|
+
* has to be correct once.
|
|
11
|
+
*
|
|
12
|
+
* Used by: `src/index.ts` (wiring), which passes the resulting
|
|
13
|
+
* provider's model instance into `createSummarizer` (src/session/summarizer.ts)
|
|
14
|
+
* and `runTurn` (src/session/agent-turn.ts).
|
|
15
|
+
*/
|
|
16
|
+
import { type OllamaProvider } from "ai-sdk-ollama";
|
|
17
|
+
/**
|
|
18
|
+
* Returns an Ollama provider bound to `OLLAMA_HOST`.
|
|
19
|
+
*
|
|
20
|
+
* Throws synchronously if `OLLAMA_HOST` is unset or empty — there is no
|
|
21
|
+
* fallback to `localhost`, since which Ollama endpoint to use is a
|
|
22
|
+
* deployment decision, not something Mercury's code should guess.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getOllamaProvider(): OllamaProvider;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queries Ollama directly (its native HTTP API, not the AI SDK — which
|
|
3
|
+
* has no concept of this) for the real context window size the running
|
|
4
|
+
* model is currently loaded with.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists, instead of just reading the model's architectural max
|
|
7
|
+
* context length (available via `/api/show`'s `model_info`): Ollama can
|
|
8
|
+
* load a model with a smaller effective context window than what the
|
|
9
|
+
* model architecture supports (e.g. capped by `OLLAMA_CONTEXT_LENGTH` on
|
|
10
|
+
* the server, or a per-request `num_ctx`), so the architectural max can
|
|
11
|
+
* overstate what's actually usable. `/api/ps` reports what's actually
|
|
12
|
+
* loaded right now — verified live against a real Ollama server (GB10,
|
|
13
|
+
* qwen3.5:35b): it loaded with the full architectural context (262144),
|
|
14
|
+
* but that's specific to this deployment's configuration, not something
|
|
15
|
+
* to assume in general.
|
|
16
|
+
*
|
|
17
|
+
* Used by: `src/index.ts`, to show a real (not estimated) context-usage
|
|
18
|
+
* indicator next to the terminal prompt — see `src/router/tool-log.ts`'s
|
|
19
|
+
* `formatContextUsage`.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Returns the context length Ollama has the given model loaded with
|
|
23
|
+
* right now, or `null` if that model isn't currently loaded (e.g. before
|
|
24
|
+
* its first use this process) — `/api/ps` only lists loaded models, so
|
|
25
|
+
* there's nothing to report yet. Querying again after the model has
|
|
26
|
+
* been used at least once will find it.
|
|
27
|
+
*
|
|
28
|
+
* @param fetchFn - Test seam; defaults to the real global `fetch`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getLoadedContextLength(host: string, model: string, fetchFn?: typeof fetch): Promise<number | null>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the read-only installation manifest the HTTP surface exposes (4b):
|
|
3
|
+
* what this instance actually loaded — the core's plugin apiVersion, each
|
|
4
|
+
* plugin (name, declared apiVersion, whether it activated, the skills it
|
|
5
|
+
* contributes, whether it has a `build()`), the active CLI binaries, and the
|
|
6
|
+
* skill descriptors. Pure and side-effect-free so it's unit-tested directly.
|
|
7
|
+
*
|
|
8
|
+
* A plugin no longer exposes a central config, so activation is reported from
|
|
9
|
+
* the loader's `activated` set rather than inferred from a config map. The
|
|
10
|
+
* active CLI binaries are the activated plugins plus the file-based CLIs (the
|
|
11
|
+
* residual with no owning plugin).
|
|
12
|
+
*/
|
|
13
|
+
import { type Plugin, type Skill } from "@mercury-fw/plugin-types";
|
|
14
|
+
export type PluginManifest = {
|
|
15
|
+
coreApiVersion: number;
|
|
16
|
+
plugins: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
apiVersion: number;
|
|
19
|
+
active: boolean;
|
|
20
|
+
skills: string[];
|
|
21
|
+
hasBuild: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
activeClis: string[];
|
|
24
|
+
skills: Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
export declare function buildPluginManifest(plugins: Plugin[], activatedPluginNames: string[], fileCliBinaries: string[], skills: Skill[]): PluginManifest;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generic, fail-soft plugin loader. Turns a hand-listed set of plugin
|
|
3
|
+
* modules into what the composition root wires into a running Mercury: the
|
|
4
|
+
* per-plugin tool bundles the model-facing tools are built from, their status
|
|
5
|
+
* describers, the system-prompt fragments, and the post-turn guards. It knows
|
|
6
|
+
* nothing about any specific plugin — or about CLIs — the composition root
|
|
7
|
+
* names them, this loop processes them identically.
|
|
8
|
+
*
|
|
9
|
+
* Two properties it guarantees, both required by the plan's fail-soft step:
|
|
10
|
+
* - a plugin contributes only when it is enabled on this instance (its name is
|
|
11
|
+
* in MERCURY_CLIS);
|
|
12
|
+
* - a plugin that fails — its `build()` throws — degrades as a single unit:
|
|
13
|
+
* none of its contributions land, the failure is
|
|
14
|
+
* logged with detail, and every other plugin and the process itself carry
|
|
15
|
+
* on. This is the CLAUDE.md lesson made structural: one plugin's bad tick
|
|
16
|
+
* never takes down the rest.
|
|
17
|
+
*
|
|
18
|
+
* The contract types (`Plugin`, `SessionToolContext`, `CliPostProcessor`,
|
|
19
|
+
* `PostTurnGuard`, and the `build()` context) live in `@mercury-fw/plugin-types`,
|
|
20
|
+
* the shared package both the core and the plugins import, so neither mirrors
|
|
21
|
+
* the other. This module keeps only the core-runtime pieces: how a hand-listed
|
|
22
|
+
* set of plugins is loaded and what the load produces.
|
|
23
|
+
*/
|
|
24
|
+
import type { LanguageModel } from "ai";
|
|
25
|
+
import type { Plugin, CliPostProcessor, PostTurnGuard, Skill, SessionToolContext } from "@mercury-fw/plugin-types";
|
|
26
|
+
import type { Tool } from "ai";
|
|
27
|
+
/** One plugin's tool contribution, kept paired so the composition root can build
|
|
28
|
+
* its tools from its own post-processor: the plugin authored both, but a
|
|
29
|
+
* formatter decorator wraps the post-processor after `build()` returns, so the
|
|
30
|
+
* factory receives the final one at invocation rather than closing over it. */
|
|
31
|
+
export interface SessionToolBundle {
|
|
32
|
+
build: (ctx: SessionToolContext, postProcess?: CliPostProcessor) => Record<string, Tool>;
|
|
33
|
+
postProcess?: CliPostProcessor;
|
|
34
|
+
}
|
|
35
|
+
/** What the loader hands back to the composition root, aggregated across every
|
|
36
|
+
* plugin that loaded. The core knows nothing about CLIs: a plugin's tool is an
|
|
37
|
+
* opaque `SessionToolBundle` it contributed, not a config the core assembles. */
|
|
38
|
+
export interface LoadedPlugins {
|
|
39
|
+
promptFragments: string[];
|
|
40
|
+
skills: Skill[];
|
|
41
|
+
/** Per-plugin tool factories + their post-processors; the composition root
|
|
42
|
+
* invokes each per turn with the session context (see `SessionToolBundle`). */
|
|
43
|
+
sessionToolBundles: SessionToolBundle[];
|
|
44
|
+
/** Merged across plugins, keyed by the tool name each contributes, turning a
|
|
45
|
+
* tool call's input into its status label. */
|
|
46
|
+
toolStatusDescribers: Record<string, (input: unknown) => string>;
|
|
47
|
+
postTurnGuards: PostTurnGuard[];
|
|
48
|
+
/** Names of the plugins that fully activated — for read-only introspection
|
|
49
|
+
* (the manifest), since a plugin's tool is opaque and there's no central
|
|
50
|
+
* config map to infer activation from anymore. */
|
|
51
|
+
activated: string[];
|
|
52
|
+
}
|
|
53
|
+
/** Everything the loader needs from the composition root: which plugins are
|
|
54
|
+
* enabled on this instance, and the runtime context every `build()` gets. */
|
|
55
|
+
export interface PluginLoadContext {
|
|
56
|
+
enabledClis: string[];
|
|
57
|
+
model: LanguageModel;
|
|
58
|
+
env: Record<string, string | undefined>;
|
|
59
|
+
log: (msg: string) => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Orders `plugins` so every plugin comes after each of its declared `dependsOn`
|
|
63
|
+
* (a stable topological sort: among plugins with no ordering constraint between
|
|
64
|
+
* them, listing order is preserved). Only edges to a plugin actually present in
|
|
65
|
+
* the set are honored — an unknown dependency name creates no edge and is
|
|
66
|
+
* caught later, at activation, as an absent dependency. Any plugin left over
|
|
67
|
+
* after the sort is part of, or downstream of, a dependency **cycle**: it comes
|
|
68
|
+
* back in `cyclic`, never in `ordered`, so a cycle degrades fail-soft (the
|
|
69
|
+
* caller skips those plugins) instead of dropping the whole load.
|
|
70
|
+
*/
|
|
71
|
+
export declare function orderByDependencies(plugins: Plugin[]): {
|
|
72
|
+
ordered: Plugin[];
|
|
73
|
+
cyclic: Plugin[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Loads every plugin in `plugins`, returning their combined contributions.
|
|
77
|
+
* Plugins load in dependency-first order (see `orderByDependencies`), not
|
|
78
|
+
* listing order — though listing order is preserved among plugins with no
|
|
79
|
+
* dependency between them. Never throws — a plugin that fails is logged and
|
|
80
|
+
* skipped, its contributions staged and merged only once the whole plugin
|
|
81
|
+
* succeeds so a later failure can't leave it half-wired. A plugin whose
|
|
82
|
+
* declared `dependsOn` isn't fully activated (a dependency disabled, failed,
|
|
83
|
+
* unknown, or itself skipped) is skipped fail-soft too, transitively.
|
|
84
|
+
*/
|
|
85
|
+
export declare function loadPlugins(plugins: Plugin[], ctx: PluginLoadContext): Promise<LoadedPlugins>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the hand-listed channel plugins into the providers the composition root
|
|
3
|
+
* starts — the channel-side mirror of `plugins/plugin-loader.ts`. It knows
|
|
4
|
+
* nothing about any specific channel: the composition root names them, this
|
|
5
|
+
* loop builds them identically.
|
|
6
|
+
*
|
|
7
|
+
* A channel is active when it's declared in `mercury.config.ts`'s `channels`
|
|
8
|
+
* (there is no separate env gate): declared = active. Each channel self-gates on
|
|
9
|
+
* its real config via `build()` returning `undefined` (Google Chat with no
|
|
10
|
+
* subscription; HTTP always builds).
|
|
11
|
+
*
|
|
12
|
+
* Fail-soft, same guarantees as the tool-plugin loader:
|
|
13
|
+
* - an `apiVersion` mismatch is refused before `build()` runs;
|
|
14
|
+
* - a `build()` that throws degrades as a single unit (logged, skipped) while
|
|
15
|
+
* every other channel and the process carry on;
|
|
16
|
+
* - a `build()` that returns `undefined` means "present but inert" (the
|
|
17
|
+
* instance isn't configured for it) — not started, not an error.
|
|
18
|
+
*/
|
|
19
|
+
import { type ChannelPlugin, type ChannelRuntimeContext, type Provider } from "@mercury-fw/channel-types";
|
|
20
|
+
/** Everything the loader needs from the composition root: the runtime context every `build()` gets. */
|
|
21
|
+
export type LoadChannelsContext = {
|
|
22
|
+
runtime: ChannelRuntimeContext;
|
|
23
|
+
};
|
|
24
|
+
/** One built channel, kept with its plugin name so the composition root can start it and look it up (e.g. the cron `Notifier`). */
|
|
25
|
+
export type LoadedChannel = {
|
|
26
|
+
name: string;
|
|
27
|
+
provider: Provider;
|
|
28
|
+
};
|
|
29
|
+
/** Builds every declared channel in `channels`, returning the providers that actually constructed (skipping incompatible, inert, or failed ones). */
|
|
30
|
+
export declare function loadChannels(channels: ChannelPlugin[], ctx: LoadChannelsContext): LoadedChannel[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel contract, re-exported from `@mercury-fw/channel-types`. A thin shim so
|
|
3
|
+
* the core modules importing from `router/provider.ts` don't change; the
|
|
4
|
+
* definitions moved to the shared package so channel plugins can import them
|
|
5
|
+
* without depending on the app.
|
|
6
|
+
*/
|
|
7
|
+
export type { Provider, InboundTurn, TurnSink, HandleTurn, Notifier } from "@mercury-fw/channel-types";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal channel's `Provider` implementation — wraps
|
|
3
|
+
* `startTerminalRepl` (`src/router/terminal.ts`, unmodified) and owns
|
|
4
|
+
* everything specific to running Mercury from a terminal: the `/dump`
|
|
5
|
+
* command, bare-token confirmation interception, the dim/italic
|
|
6
|
+
* tool-status rendering, and the live context-usage prompt suffix.
|
|
7
|
+
*
|
|
8
|
+
* A single operator, one conversation at a time — there is no real
|
|
9
|
+
* per-user identity here, so `notify` just writes to stderr rather than
|
|
10
|
+
* actually reaching anyone; implemented (not left optional) so a caller
|
|
11
|
+
* that expects a `Notifier` never silently loses a message.
|
|
12
|
+
*/
|
|
13
|
+
import { startTerminalRepl } from "./terminal.ts";
|
|
14
|
+
import { tryConfirm, type ConfirmationStore } from "@mercury-fw/confirm-engine";
|
|
15
|
+
import { getLoadedContextLength } from "../model/context-size.ts";
|
|
16
|
+
import type { Provider } from "./provider.ts";
|
|
17
|
+
import type { writeConfirmationNote } from "../wiki/wiki-note.ts";
|
|
18
|
+
export type TerminalProviderDeps = {
|
|
19
|
+
confirmDeps: {
|
|
20
|
+
store: ConfirmationStore;
|
|
21
|
+
vaultPath: string;
|
|
22
|
+
writeConfirmationNoteFn: typeof writeConfirmationNote;
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
};
|
|
25
|
+
ollamaHost: string;
|
|
26
|
+
ollamaModel: string;
|
|
27
|
+
/** Test seam; defaults to the real `getLoadedContextLength`. */
|
|
28
|
+
getLoadedContextLengthFn?: typeof getLoadedContextLength;
|
|
29
|
+
/** Test seam; defaults to the real `startTerminalRepl`. */
|
|
30
|
+
startTerminalReplFn?: typeof startTerminalRepl;
|
|
31
|
+
/** Test seam; defaults to the real `tryConfirm`. */
|
|
32
|
+
tryConfirmFn?: typeof tryConfirm;
|
|
33
|
+
/** Test seam; defaults to `console.error`. */
|
|
34
|
+
stderrWrite?: (s: string) => void;
|
|
35
|
+
};
|
|
36
|
+
/** Builds the terminal's `Provider`. */
|
|
37
|
+
export declare function createTerminalProvider(deps: TerminalProviderDeps): Provider;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Written before the first input and again after every result, so a
|
|
3
|
+
* human at the terminal can tell an answer has finished and the next
|
|
4
|
+
* question can be typed — without this, a multi-line answer and the
|
|
5
|
+
* start of a new question were visually indistinguishable.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PROMPT = "> ";
|
|
8
|
+
/**
|
|
9
|
+
* Runs the REPL loop: read a line, pass it to `handleInput`, write the
|
|
10
|
+
* result, repeat until the input source is exhausted (EOF).
|
|
11
|
+
*
|
|
12
|
+
* If `handleInput` rejects, the error is written as a line (so a long-
|
|
13
|
+
* running debug session can see what went wrong) and the loop continues
|
|
14
|
+
* with the next line — one bad turn shouldn't kill the whole session.
|
|
15
|
+
*
|
|
16
|
+
* @param handleInput - Turns one line of input into one line of output.
|
|
17
|
+
* Knows nothing about this being a terminal. Receives `onChunk`, which
|
|
18
|
+
* it may call zero or more times with pieces of the answer as they
|
|
19
|
+
* become available (e.g. while streaming a model response) — each
|
|
20
|
+
* call is written immediately, without waiting for `handleInput` to
|
|
21
|
+
* resolve. If `onChunk` is never called, the function's returned
|
|
22
|
+
* string is written instead once `handleInput` resolves, exactly as
|
|
23
|
+
* before streaming existed — this is what keeps the `/dump` command
|
|
24
|
+
* and any other non-streaming reply working unchanged.
|
|
25
|
+
* @param io - Test seam. Defaults to real stdin/stdout when omitted.
|
|
26
|
+
* @param opts.promptSuffix - Optional, called fresh right before every
|
|
27
|
+
* prompt (including the very first one) and written ahead of it — e.g.
|
|
28
|
+
* a live context-usage indicator (see `src/router/tool-log.ts`'s
|
|
29
|
+
* `formatContextUsage`), recomputed each time since the value changes
|
|
30
|
+
* turn to turn.
|
|
31
|
+
*/
|
|
32
|
+
export declare function startTerminalRepl(handleInput: (input: string, onChunk: (chunk: string) => void) => Promise<string>, io?: {
|
|
33
|
+
input?: AsyncIterable<string>;
|
|
34
|
+
output?: {
|
|
35
|
+
write(s: string, opts?: {
|
|
36
|
+
newline?: boolean;
|
|
37
|
+
}): void;
|
|
38
|
+
};
|
|
39
|
+
}, opts?: {
|
|
40
|
+
promptSuffix?: () => string;
|
|
41
|
+
}): Promise<void>;
|