@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @mercury-fw/core
|
|
2
|
+
|
|
3
|
+
## 0.25.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c114e34: - Mercury is published on npm as `@mercury-fw/*`: the framework packages move together under one version, plugins and channels have versions of their own.
|
|
8
|
+
- `bun create mercury-agent my-agent` scaffolds an app, and the CLI command is now `mfw` (`@mercury-fw/cli`).
|
|
9
|
+
- Packages ship type declarations, so a new app type-checks against the framework without re-checking its source.
|
|
10
|
+
- External dependencies use version ranges instead of exact pins, so an app shares them with the framework.
|
|
11
|
+
- Every package has its own README, and the repo README describes the framework.
|
|
12
|
+
- MIT license.
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- @mercury-fw/plugin-types@0.25.0
|
|
17
|
+
- @mercury-fw/channel-types@0.25.0
|
|
18
|
+
- @mercury-fw/cli-engine@0.25.0
|
|
19
|
+
- @mercury-fw/confirm-engine@0.25.0
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @mercury-fw/core
|
|
2
|
+
|
|
3
|
+
The runtime of [Mercury](https://github.com/lucabro81/mercury-fw): it builds an agent from the config an app gives it and runs it, from the conversation loop to the channels. An app depends on it; a plugin doesn't (plugins build on [`@mercury-fw/kit`](https://www.npmjs.com/package/@mercury-fw/kit)).
|
|
4
|
+
|
|
5
|
+
The usual way to get it is scaffolding an app, which wires it up for you:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create mercury-agent my-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it exports
|
|
12
|
+
|
|
13
|
+
- `defineMercuryConfig(config)` types the app's `mercury.config.ts`: `plugins`, `channels`, `persona`.
|
|
14
|
+
- `composeMercury(config)` builds the agent from that config and the environment, and returns what the entrypoints start: `handleTurn`, the declared channels with their runtime, and the background jobs (`startCrons`, `startAdmin`).
|
|
15
|
+
- `loadChannels(channels, { runtime })` turns the declared channels into started providers, and `createTerminalProvider(...)` opens the dev REPL on `handleTurn`.
|
|
16
|
+
- `DEFAULT_PERSONA_IDENTITY`, `DEFAULT_PERSONA_TONE` are the persona an app gets when its config sets none.
|
|
17
|
+
|
|
18
|
+
A scaffolded app's `src/index.ts` (the service) and `src/repl.ts` (the REPL) are the reference for using them.
|
|
19
|
+
|
|
20
|
+
## Environment
|
|
21
|
+
|
|
22
|
+
| Variable | |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `OLLAMA_HOST` | The Ollama-compatible endpoint. Required. |
|
|
25
|
+
| `OLLAMA_MODEL` | The chat model. Required. |
|
|
26
|
+
| `OLLAMA_EMBEDDING_MODEL` | The embedding model for the episodic memory (default `nomic-embed-text`). |
|
|
27
|
+
| `OLLAMA_THINK` | `false` for a model that doesn't support thinking. |
|
|
28
|
+
| `QDRANT_URL` | Qdrant, for the episodic memory (default `http://qdrant:6333`). |
|
|
29
|
+
| `WIKI_VAULT_PATH` | Where the wiki lives. Required. |
|
|
30
|
+
| `MERCURY_CLIS` | The tool plugins this deployment turns on, by name, comma-separated. |
|
|
31
|
+
|
|
32
|
+
Qdrant being unreachable degrades memory, it doesn't stop the agent.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
Bun: the package ships its TypeScript source, which Bun runs as is, plus type declarations for your editor and `tsc`.
|
|
37
|
+
|
|
38
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of `@mercury-fw/core` — the framework runtime a Mercury
|
|
3
|
+
* instance consumes. Everything else under `src/` is internal: an app depends on
|
|
4
|
+
* this barrel, never on a deep path.
|
|
5
|
+
*
|
|
6
|
+
* What's exported, and who uses it:
|
|
7
|
+
* - `composeMercury` builds the instance from a config it is *given* (the
|
|
8
|
+
* config is never imported by the core — that's what keeps it app-agnostic).
|
|
9
|
+
* `ComposedApp`/`ConfirmDeps` are its result and confirm-binding types.
|
|
10
|
+
* - `loadChannels`/`LoadedChannel` — the service entrypoint starts the declared
|
|
11
|
+
* channels with these.
|
|
12
|
+
* - `createTerminalProvider` — the dev REPL entrypoint opens the terminal with it.
|
|
13
|
+
* - `defineMercuryConfig`/`MercuryConfig` — the app's `mercury.config.ts` declares
|
|
14
|
+
* its composition through these; `Persona` types its `persona` field.
|
|
15
|
+
* - `DEFAULT_PERSONA_IDENTITY`/`DEFAULT_PERSONA_TONE` — the persona an instance
|
|
16
|
+
* gets when its config sets none; the scaffolder writes them as a new app's
|
|
17
|
+
* starting persona.
|
|
18
|
+
*/
|
|
19
|
+
export { composeMercury, type ComposedApp, type ConfirmDeps } from "./src/compose.ts";
|
|
20
|
+
export { loadChannels, type LoadedChannel } from "./src/router/channel-loader.ts";
|
|
21
|
+
export { createTerminalProvider } from "./src/router/terminal-provider.ts";
|
|
22
|
+
export { defineMercuryConfig, type MercuryConfig } from "./src/config/define-config.ts";
|
|
23
|
+
export { DEFAULT_PERSONA_IDENTITY, DEFAULT_PERSONA_TONE, type Persona } from "./src/session/system-prompt.ts";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin panel's CLI status tab: lists each active CLI's allowlisted
|
|
3
|
+
* commands straight from the same `CliConfig` `src/index.ts` already
|
|
4
|
+
* built at startup (`activeCliConfigs`), and — where a CLI declares a
|
|
5
|
+
* plain, non-mutating `auth whoami`-shaped prefix — runs it through the
|
|
6
|
+
* exact same `runCli` used for real tool calls. Never constructs or runs
|
|
7
|
+
* anything else; this is read-only status, not a command console.
|
|
8
|
+
*/
|
|
9
|
+
import type { CliConfig } from "@mercury-fw/cli-engine";
|
|
10
|
+
import type { runCli } from "@mercury-fw/cli-engine";
|
|
11
|
+
export type CliStatusEntry = {
|
|
12
|
+
binary: string;
|
|
13
|
+
allowedPrefixes: CliConfig["allowedPrefixes"];
|
|
14
|
+
whoami: {
|
|
15
|
+
available: false;
|
|
16
|
+
} | {
|
|
17
|
+
available: true;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
output: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare function getCliStatus(configs: Record<string, CliConfig>, runCliFn: typeof runCli): Promise<CliStatusEntry[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setEnvValue(content: string, key: string, value: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Live model names Ollama actually has pulled, or `null` if the host isn't reachable. */
|
|
2
|
+
export declare function getAvailableModels(host: string, fetchFn?: typeof fetch): Promise<string[] | null>;
|
|
3
|
+
export type ModelStatus = {
|
|
4
|
+
host: string;
|
|
5
|
+
model: string;
|
|
6
|
+
contextLength: number | null;
|
|
7
|
+
availableModels: string[] | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function getModelStatus(host: string, model: string, fetchFn?: typeof fetch): Promise<ModelStatus>;
|
|
10
|
+
export type SelfHealth = {
|
|
11
|
+
uptimeSeconds: number;
|
|
12
|
+
memory: {
|
|
13
|
+
rss: number;
|
|
14
|
+
heapUsed: number;
|
|
15
|
+
heapTotal: number;
|
|
16
|
+
};
|
|
17
|
+
qdrantReachable: boolean;
|
|
18
|
+
ollamaReachable: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function getSelfHealth(deps: {
|
|
21
|
+
qdrant: {
|
|
22
|
+
getCollections(): Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
ollamaHost: string;
|
|
25
|
+
fetchFn?: typeof fetch;
|
|
26
|
+
}): Promise<SelfHealth>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only point enumeration for the admin panel's Qdrant inspection tab —
|
|
3
|
+
* `episodic-store.ts`/`semantic-facts-store.ts` only wrap similarity
|
|
4
|
+
* `search`, there's no existing way to just list what's in a collection.
|
|
5
|
+
* One page per call (not an auto-paging fetch-everything loop): a real
|
|
6
|
+
* collection can be arbitrarily large, so the caller decides whether to
|
|
7
|
+
* request another page via the returned `nextOffset`.
|
|
8
|
+
*/
|
|
9
|
+
type ScrollOffset = string | number | Record<string, unknown> | null;
|
|
10
|
+
export type ScrollableQdrantClient = {
|
|
11
|
+
scroll(collection: string, params: {
|
|
12
|
+
limit: number;
|
|
13
|
+
offset?: ScrollOffset;
|
|
14
|
+
with_payload: boolean;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
points: Array<{
|
|
17
|
+
id: string | number;
|
|
18
|
+
payload?: Record<string, unknown> | null;
|
|
19
|
+
}>;
|
|
20
|
+
next_page_offset?: ScrollOffset;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export type ScrollPage = {
|
|
24
|
+
points: Array<{
|
|
25
|
+
id: string | number;
|
|
26
|
+
payload: Record<string, unknown> | null;
|
|
27
|
+
}>;
|
|
28
|
+
nextOffset: ScrollOffset;
|
|
29
|
+
};
|
|
30
|
+
export declare function scrollCollection(client: ScrollableQdrantClient, collectionName: string, opts: {
|
|
31
|
+
limit: number;
|
|
32
|
+
offset?: ScrollOffset;
|
|
33
|
+
}): Promise<ScrollPage>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin panel POC — a separate `Bun.serve()` instance running inside the
|
|
3
|
+
* same Mercury process (see plan: reuses the already-constructed `qdrant`
|
|
4
|
+
* client, `activeCliConfigs`, model, and vault path directly, no IPC).
|
|
5
|
+
* Dev-only, unauthenticated by design (see docs/DECISIONS.md-adjacent
|
|
6
|
+
* scoping conversation) — never started unless `ADMIN_PANEL_ENABLED` is
|
|
7
|
+
* set (see `src/index.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import type { LanguageModel } from "ai";
|
|
10
|
+
import type { CliConfig } from "@mercury-fw/cli-engine";
|
|
11
|
+
import type { runCli } from "@mercury-fw/cli-engine";
|
|
12
|
+
import { type ScrollableQdrantClient } from "./qdrant-scroll.ts";
|
|
13
|
+
type QdrantDeps = ScrollableQdrantClient & {
|
|
14
|
+
getCollections(): Promise<{
|
|
15
|
+
collections: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
export type AdminServerDeps = {
|
|
21
|
+
port: number;
|
|
22
|
+
vaultPath: string;
|
|
23
|
+
model: LanguageModel;
|
|
24
|
+
qdrant: QdrantDeps;
|
|
25
|
+
qdrantCollections: {
|
|
26
|
+
episodic: string;
|
|
27
|
+
semanticFacts: string;
|
|
28
|
+
};
|
|
29
|
+
activeCliConfigs: Record<string, CliConfig>;
|
|
30
|
+
runCliFn: typeof runCli;
|
|
31
|
+
ollamaHost: string;
|
|
32
|
+
ollamaModel: string;
|
|
33
|
+
systemPrompts: {
|
|
34
|
+
terminal: string;
|
|
35
|
+
googleChat: string;
|
|
36
|
+
};
|
|
37
|
+
envFilePath: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function startAdminServer(deps: AdminServerDeps): ReturnType<typeof Bun.serve>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin panel's Wiki tab. `list`/`read`/`grep` walk the whole vault
|
|
3
|
+
* directly (same trusted-admin-context choice `vault-cli.ts` already
|
|
4
|
+
* makes — bypassing `wiki-read.ts`'s per-user `allowedRoots` scoping,
|
|
5
|
+
* which exists to isolate what the MODEL can see per caller, not what an
|
|
6
|
+
* operator running this panel can see).
|
|
7
|
+
*
|
|
8
|
+
* Curated edits go through a real, short-lived agent turn instead of
|
|
9
|
+
* calling `writeCuratedNote` directly — see `editWikiViaModel`. Raw
|
|
10
|
+
* writes and deletes have no model-facing tool today, so they stay
|
|
11
|
+
* direct calls into `wiki-note.ts`, same as `vault-cli.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import type { LanguageModel } from "ai";
|
|
14
|
+
import type { WikiGrepMatch } from "../wiki/wiki-read.ts";
|
|
15
|
+
export declare function listWikiVault(vaultPath: string): Promise<string[]>;
|
|
16
|
+
export declare function readWikiVaultFile(vaultPath: string, relativePath: string): Promise<string>;
|
|
17
|
+
export declare function grepWikiVault(vaultPath: string, pattern: string): Promise<WikiGrepMatch[]>;
|
|
18
|
+
/** `vaultRelativePath` must start with `raw/`, matching `vault-cli.ts`'s `write-raw`. */
|
|
19
|
+
export declare function writeRawWikiEntry(vaultPath: string, vaultRelativePath: string, body: string): Promise<void>;
|
|
20
|
+
/** `vaultRelativePath` must start with `curated/` or `raw/` — nothing else is deletable from here. */
|
|
21
|
+
export declare function deleteWikiEntry(vaultPath: string, vaultRelativePath: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Sends `instruction` through a real, short-lived agent turn scoped
|
|
24
|
+
* ONLY to the four wiki tools (`createWikiTools`) — never the full
|
|
25
|
+
* toolset a normal channel gets, so this box can't touch Jira/Bitbucket.
|
|
26
|
+
* Reuses the exact commit path a real conversation would take
|
|
27
|
+
* (`write_file` -> `writeCuratedNote` -> git commit, D-16); this
|
|
28
|
+
* function has no write logic of its own. History is fresh per call,
|
|
29
|
+
* never persisted — this isn't a real session.
|
|
30
|
+
*/
|
|
31
|
+
export declare function editWikiViaModel(model: LanguageModel, vaultPath: string, instruction: string): Promise<string>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ConfirmationStore } from "@mercury-fw/confirm-engine";
|
|
2
|
+
import type { MercuryConfig } from "./config/define-config.ts";
|
|
3
|
+
import type { HandleTurn, ChannelRuntimeContext, ChannelPlugin } from "@mercury-fw/channel-types";
|
|
4
|
+
import { writeConfirmationNote } from "./wiki/wiki-note.ts";
|
|
5
|
+
/** A stoppable subsystem (cron, server). */
|
|
6
|
+
type Stoppable = {
|
|
7
|
+
stop: () => void;
|
|
8
|
+
};
|
|
9
|
+
/** The confirm capability's binding to this instance's store/vault/note-writer, shared by the terminal and the channel runtime. */
|
|
10
|
+
export type ConfirmDeps = {
|
|
11
|
+
store: ConfirmationStore;
|
|
12
|
+
vaultPath: string;
|
|
13
|
+
writeConfirmationNoteFn: typeof writeConfirmationNote;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The built Mercury instance. `build`, not `start`: the channels, crons and
|
|
17
|
+
* admin are not running until an entrypoint starts them.
|
|
18
|
+
*/
|
|
19
|
+
export type ComposedApp = {
|
|
20
|
+
/** The turn driver every channel funnels through (see `turn-runner.ts`). */
|
|
21
|
+
handleTurn: HandleTurn;
|
|
22
|
+
/** The declared channel plugins (from `mercury.config.ts`). */
|
|
23
|
+
channels: ChannelPlugin[];
|
|
24
|
+
/** The runtime context each channel's `build()` gets — confirm + in-process reads injected by the core. */
|
|
25
|
+
channelRuntime: ChannelRuntimeContext;
|
|
26
|
+
/** The confirm binding, for a caller (the terminal) that intercepts tokens directly. */
|
|
27
|
+
confirmDeps: ConfirmDeps;
|
|
28
|
+
ollamaHost: string;
|
|
29
|
+
ollamaModel: string;
|
|
30
|
+
/** Starts the Layer-3 idle-capture and self-review crons; returns a single stopper for shutdown. */
|
|
31
|
+
startCrons: () => Stoppable;
|
|
32
|
+
/** Starts the POC admin panel if `ADMIN_PANEL_ENABLED`; returns it (to stop on shutdown), or undefined. */
|
|
33
|
+
startAdmin: () => Stoppable | undefined;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Builds a Mercury instance from the composition `config` it is given + env. The
|
|
37
|
+
* config is a parameter, never imported here: that's what makes the core
|
|
38
|
+
* app-agnostic — an entrypoint (the service, the dev REPL, a future scaffolded
|
|
39
|
+
* app) reads its own `mercury.config.ts` and passes it in. See {@link ComposedApp}.
|
|
40
|
+
*/
|
|
41
|
+
export declare function composeMercury(config: MercuryConfig): Promise<ComposedApp>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instance composition config contract and its `defineMercuryConfig`
|
|
3
|
+
* helper. A Mercury instance is composed by declaring, in one place, which
|
|
4
|
+
* plugins it runs — the way a Nuxt/Vite project declares its config through a
|
|
5
|
+
* `defineConfig` call in a `*.config.ts` file. `defineMercuryConfig` adds no
|
|
6
|
+
* runtime behavior; it exists purely so the app-root `mercury.config.ts` gets
|
|
7
|
+
* editor/compiler support (the argument is checked against `MercuryConfig`) and
|
|
8
|
+
* so there is a single, stable seam the composition root reads from.
|
|
9
|
+
*
|
|
10
|
+
* This is the minimal foundational slice of a broader composition rethink: for
|
|
11
|
+
* now the config carries only the plugin list. Per-plugin configuration and the
|
|
12
|
+
* eventual retirement of the file-based cli-configs are deliberately not here.
|
|
13
|
+
*/
|
|
14
|
+
import type { Plugin } from "@mercury-fw/plugin-types";
|
|
15
|
+
import type { ChannelPlugin } from "@mercury-fw/channel-types";
|
|
16
|
+
import type { Persona } from "../session/system-prompt.ts";
|
|
17
|
+
/** The shape of a Mercury instance's composition config: the tool plugins
|
|
18
|
+
* (gated by MERCURY_CLIS) and the channel plugins (enabled by being declared
|
|
19
|
+
* here — declared = active), each loaded by its own loader, plus the
|
|
20
|
+
* assistant's persona (its identity and tone; the defaults when left out). */
|
|
21
|
+
export type MercuryConfig = {
|
|
22
|
+
plugins: Plugin[];
|
|
23
|
+
channels?: ChannelPlugin[];
|
|
24
|
+
persona?: Persona;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Identity + typing helper for `mercury.config.ts`. Returns its argument
|
|
28
|
+
* unchanged; its only job is to type the config literal against `MercuryConfig`
|
|
29
|
+
* at the call site, exactly like `defineConfig` in the Vite/Nuxt ecosystem.
|
|
30
|
+
*/
|
|
31
|
+
export declare function defineMercuryConfig(config: MercuryConfig): MercuryConfig;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-persistence cron loop: periodically sweeps for sessions idle past the
|
|
3
|
+
* configured timeout, summarizes each one (episodic-summarizer.ts),
|
|
4
|
+
* writes the result to Qdrant (episodic-store.ts), and discards the raw
|
|
5
|
+
* transcript (`deps.closeSession`). Every dependency is injected — this
|
|
6
|
+
* file owns only the sweep/interval mechanics, not session storage, the
|
|
7
|
+
* LLM call, or Qdrant itself.
|
|
8
|
+
*/
|
|
9
|
+
import type { IdleSessionScanner } from "./idle-session-scanner.ts";
|
|
10
|
+
import type { Message } from "../session/history.ts";
|
|
11
|
+
import type { EpisodicSummary } from "../memory/episodic-store.ts";
|
|
12
|
+
import type { SemanticFact } from "../session/semantic-fact-extractor.ts";
|
|
13
|
+
import type { SemanticFactEntry } from "../memory/semantic-facts-store.ts";
|
|
14
|
+
export type IdleSession = {
|
|
15
|
+
key: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
messages: Message[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Dependencies for `captureSessionToMemory` — a subset of
|
|
21
|
+
* `IdleSessionSweepDeps`, without `getSession`/`closeSession`: this
|
|
22
|
+
* function never decides *whether* a session should be captured or
|
|
23
|
+
* whether it should be closed afterwards, only *how* to capture a given
|
|
24
|
+
* slice of messages. Reused by the idle sweep below (final capture +
|
|
25
|
+
* close) and, without touching this file's own tests, by the two
|
|
26
|
+
* mid-conversation triggers wired in `index.ts` (message-count threshold,
|
|
27
|
+
* Layer 1 compression) — neither of which closes the session.
|
|
28
|
+
*/
|
|
29
|
+
export type CaptureDeps = {
|
|
30
|
+
summarize: (messages: Message[]) => Promise<string>;
|
|
31
|
+
store: (entry: EpisodicSummary) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Semantic fact extraction/consolidation (D-22/D-34) — an enrichment on
|
|
34
|
+
* top of the episodic summary above, not a required part of it: omit
|
|
35
|
+
* all three and capture behaves exactly as before. When present, a
|
|
36
|
+
* failure here is logged and never propagates — the episodic write
|
|
37
|
+
* already succeeded and is the source of truth being preserved, same
|
|
38
|
+
* "system must work when this enrichment is absent" boundary as every
|
|
39
|
+
* other Layer 2/3 store in Mercury.
|
|
40
|
+
*/
|
|
41
|
+
extractFacts?: (messages: Message[]) => Promise<SemanticFact[]>;
|
|
42
|
+
storeFact?: (entry: SemanticFactEntry) => Promise<void>;
|
|
43
|
+
consolidateFact?: (userId: string, topic: string) => Promise<void>;
|
|
44
|
+
log?: (msg: string) => void;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Summarizes+stores an episodic entry for `messages`, then (when the
|
|
48
|
+
* semantic deps are provided) extracts and consolidates semantic facts —
|
|
49
|
+
* the one place this logic lives, shared by every capture trigger. A
|
|
50
|
+
* failure summarizing/storing propagates to the caller (it decides what
|
|
51
|
+
* "capture failed" means for its own trigger — e.g. the idle sweep below
|
|
52
|
+
* leaves the session tracked for retry and skips `closeSession`); a
|
|
53
|
+
* failure in the semantic enrichment layer is caught and logged here,
|
|
54
|
+
* never propagated, since it must never undo an episodic write that
|
|
55
|
+
* already succeeded.
|
|
56
|
+
*/
|
|
57
|
+
export declare function captureSessionToMemory(userId: string, sessionKey: string, messages: Message[], now: number, deps: CaptureDeps): Promise<void>;
|
|
58
|
+
export type IdleSessionSweepDeps = CaptureDeps & {
|
|
59
|
+
/** Looks up a session's current content by key; `undefined` if it's already gone (e.g. closed by something else in the meantime). */
|
|
60
|
+
getSession: (key: string) => IdleSession | undefined;
|
|
61
|
+
/** Discards the session's raw transcript — called only after a successful summarize+store. */
|
|
62
|
+
closeSession: (key: string) => void;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Runs one sweep at `now`: every session `scanner` reports idle (past
|
|
66
|
+
* `idleTimeoutMs`) gets captured (`captureSessionToMemory`) and closed,
|
|
67
|
+
* then cleared from `scanner`. A failure capturing is logged and leaves
|
|
68
|
+
* that session's tracking untouched (retried on the next sweep) — it must
|
|
69
|
+
* never stop the sweep from processing the others (hard-won convention:
|
|
70
|
+
* one bad tick can't take down the rest of Mercury).
|
|
71
|
+
*/
|
|
72
|
+
export declare function runIdleSessionSweep(scanner: IdleSessionScanner, now: number, idleTimeoutMs: number, deps: IdleSessionSweepDeps): Promise<void>;
|
|
73
|
+
export type IdleSessionCron = {
|
|
74
|
+
stop: () => void;
|
|
75
|
+
};
|
|
76
|
+
/** Starts the periodic sweep on `opts.checkIntervalMs`, gated on `opts.idleTimeoutMs`. `stop()` halts it. */
|
|
77
|
+
export declare function startIdleSessionCron(scanner: IdleSessionScanner, deps: IdleSessionSweepDeps, opts: {
|
|
78
|
+
idleTimeoutMs: number;
|
|
79
|
+
checkIntervalMs: number;
|
|
80
|
+
}): IdleSessionCron;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure idle-tracking for session persistence: remembers the last
|
|
3
|
+
* activity time per session key and reports which are idle at a given
|
|
4
|
+
* moment. Time is always passed in, never read internally (`Date.now()`
|
|
5
|
+
* lives in the caller, e.g. `src/index.ts`/`idle-session-cron.ts`) — this
|
|
6
|
+
* is what makes `scanIdle`'s threshold behavior exactly testable.
|
|
7
|
+
*/
|
|
8
|
+
export type IdleSessionScanner = {
|
|
9
|
+
/** Records activity for `key` at `now`, resetting its idle clock. */
|
|
10
|
+
touch(key: string, now: number): void;
|
|
11
|
+
/** Returns every tracked key whose last activity is at least `idleTimeoutMs` before `now`. */
|
|
12
|
+
scanIdle(now: number, idleTimeoutMs: number): string[];
|
|
13
|
+
/** Stops tracking `key` — call after a session has been consolidated and its raw transcript discarded. */
|
|
14
|
+
clear(key: string): void;
|
|
15
|
+
};
|
|
16
|
+
export declare function createIdleSessionScanner(): IdleSessionScanner;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wiki self-review cron: runs once nightly (a fixed local hour,
|
|
3
|
+
* hardcoded, no env override — deliberately different from every other
|
|
4
|
+
* interval in this codebase, per an explicit request to keep this one
|
|
5
|
+
* out of runtime configurability), running raw/ triage, index.md/orphan
|
|
6
|
+
* maintenance, and a contradiction check as three independent sub-passes
|
|
7
|
+
* (see `self-review-runner.ts` for why they're independent, not one
|
|
8
|
+
* multi-step call). Raw-triage and index/orphan each skip when their own
|
|
9
|
+
* cheap pre-check finds nothing to do; the contradiction check has no
|
|
10
|
+
* such pre-check and always runs on a triggered tick — running the whole
|
|
11
|
+
* thing at night, when nothing else contends for the shared local model,
|
|
12
|
+
* is what makes that affordable.
|
|
13
|
+
*
|
|
14
|
+
* This file owns only the scheduling/orchestration mechanics — vault
|
|
15
|
+
* access and the actual LLM calls are injected, same separation
|
|
16
|
+
* `idle-session-cron.ts` uses for session storage and the LLM summarizer.
|
|
17
|
+
*/
|
|
18
|
+
export type SelfReviewTickDeps = {
|
|
19
|
+
listRawEntries: () => Promise<string[]>;
|
|
20
|
+
findOrphans: () => Promise<string[]>;
|
|
21
|
+
runRawTriage: (rawEntries: string[]) => Promise<void>;
|
|
22
|
+
runIndexAndOrphan: (orphans: string[]) => Promise<void>;
|
|
23
|
+
runContradictionCheck: () => Promise<void>;
|
|
24
|
+
log?: (msg: string) => void;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Runs one nightly pass: raw-triage and index/orphan each run only if
|
|
28
|
+
* their own cheap signal found something; the contradiction check always
|
|
29
|
+
* runs, since nothing cheap can tell it whether there's anything to find.
|
|
30
|
+
* Each sub-pass is wrapped in its own try/catch — one failing must not
|
|
31
|
+
* stop the other two from running in the same pass (hard-won convention:
|
|
32
|
+
* one bad tick can't take down the rest of Mercury).
|
|
33
|
+
*/
|
|
34
|
+
export declare function runSelfReviewTick(deps: SelfReviewTickDeps): Promise<void>;
|
|
35
|
+
/** 3 AM local time — hardcoded on purpose, see file header. */
|
|
36
|
+
export declare const SELF_REVIEW_HOUR = 3;
|
|
37
|
+
/** How often to check whether it's time to run — mirrors idle-session-cron's
|
|
38
|
+
* check-vs-timeout split, hardcoded for the same reason as the hour above. */
|
|
39
|
+
export declare const SELF_REVIEW_CHECK_INTERVAL_MS: number;
|
|
40
|
+
export type SelfReviewCron = {
|
|
41
|
+
stop: () => void;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Checks every `opts.checkIntervalMs` whether the current local hour
|
|
45
|
+
* matches `opts.hour` and today hasn't run yet; if so, runs one tick.
|
|
46
|
+
* `lastRunDate` is in-memory only — if the process restarts mid-window it
|
|
47
|
+
* just waits for tomorrow's, which is fine, nothing here needs to survive
|
|
48
|
+
* a restart. `opts.now`/`opts.hour`/`opts.checkIntervalMs` exist purely as
|
|
49
|
+
* test seams; production (`index.ts`) never overrides them.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startSelfReviewCron(deps: SelfReviewTickDeps, opts?: {
|
|
52
|
+
hour?: number;
|
|
53
|
+
checkIntervalMs?: number;
|
|
54
|
+
now?: () => Date;
|
|
55
|
+
}): SelfReviewCron;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { readWikiFile } from "../wiki/wiki-read.ts";
|
|
2
|
+
import type { writeInferredNote, writeToolCorrectionNote } from "../wiki/wiki-note.ts";
|
|
3
|
+
import type { SemanticFactEntry } from "../memory/semantic-facts-store.ts";
|
|
4
|
+
import type { ToolCorrectionEntry } from "../memory/tool-corrections-store.ts";
|
|
5
|
+
type ClusterFn = (userId: string, topic: string, limit: number) => Promise<SemanticFactEntry[]>;
|
|
6
|
+
type Confidence = "low" | "medium" | "high";
|
|
7
|
+
export type ConsolidationDeps = {
|
|
8
|
+
vaultPath: string;
|
|
9
|
+
clusterFn: ClusterFn;
|
|
10
|
+
readWikiFileFn: typeof readWikiFile;
|
|
11
|
+
writeInferredNoteFn: typeof writeInferredNote;
|
|
12
|
+
k?: number;
|
|
13
|
+
confidenceForCount?: (dominantCount: number, k: number) => Confidence;
|
|
14
|
+
now?: () => string;
|
|
15
|
+
};
|
|
16
|
+
type ToolCorrectionClusterFn = (tool: string, topic: string, limit: number) => Promise<ToolCorrectionEntry[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Same shape as `ConsolidationDeps`, keyed by `tool` instead of `userId` —
|
|
19
|
+
* `readNoteFn`/`writeNoteFn` deliberately don't take a userId at all
|
|
20
|
+
* (unlike `readWikiFileFn`/`writeInferredNoteFn` above): a procedural
|
|
21
|
+
* correction lives under `curated/standards/`, visible to every session
|
|
22
|
+
* regardless of who asks, never scoped to one user's own
|
|
23
|
+
* `inferred/users/<userId>/`.
|
|
24
|
+
*/
|
|
25
|
+
export type ToolCorrectionConsolidationDeps = {
|
|
26
|
+
vaultPath: string;
|
|
27
|
+
clusterFn: ToolCorrectionClusterFn;
|
|
28
|
+
readNoteFn: (vaultPath: string, relativePath: string) => Promise<string>;
|
|
29
|
+
writeNoteFn: typeof writeToolCorrectionNote;
|
|
30
|
+
k?: number;
|
|
31
|
+
confidenceForCount?: (dominantCount: number, k: number) => Confidence;
|
|
32
|
+
now?: () => string;
|
|
33
|
+
};
|
|
34
|
+
/** Window size for consolidation — how many recent occurrences of a topic to consider. Uncalibrated: chosen without real usage data, to revisit once there's actual traffic to tune against. */
|
|
35
|
+
export declare const DEFAULT_CONSOLIDATION_K = 3;
|
|
36
|
+
/**
|
|
37
|
+
* Uncalibrated confidence bands, count relative to `k`: a single
|
|
38
|
+
* occurrence is unconfirmed (low); repeated but not unanimous within the
|
|
39
|
+
* tracked window is medium; the dominant value filling the whole window
|
|
40
|
+
* is high. Same "revisit with real usage" caveat as `DEFAULT_CONSOLIDATION_K`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function defaultConfidenceForCount(dominantCount: number, k: number): Confidence;
|
|
43
|
+
/**
|
|
44
|
+
* Re-clusters `topic` for `userId`, and promotes the dominant value to a
|
|
45
|
+
* wiki note if it beats the current incumbent's count. No-op if the
|
|
46
|
+
* cluster is empty or has no single dominant value.
|
|
47
|
+
*
|
|
48
|
+
* `userId` here is Qdrant's own storage form (e.g. `"users/42"`, a raw
|
|
49
|
+
* Google Chat resource name) — `clusterFn` above searches with it as-is,
|
|
50
|
+
* matching how `storeSemanticFact` wrote it. The wiki's
|
|
51
|
+
* `inferred/users/<userId>/` convention expects a different,
|
|
52
|
+
* `encodeURIComponent`-encoded form instead (the same one the model's
|
|
53
|
+
* own wiki tools already use) — a raw userId containing "/" would
|
|
54
|
+
* otherwise be rejected outright by
|
|
55
|
+
* `writeInferredNote`'s own path-separator guard, and even without that
|
|
56
|
+
* guard would land in a directory the model's wiki tools never look at.
|
|
57
|
+
* Two different representations of the same identity, for two different
|
|
58
|
+
* purposes — `wikiUserId` below is used only for the wiki-facing calls,
|
|
59
|
+
* never for `clusterFn`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function consolidateSemanticFact(userId: string, topic: string, deps: ConsolidationDeps): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Same promotion logic as `consolidateSemanticFact` — re-clusters `topic`
|
|
64
|
+
* for `tool`, promotes the dominant value if it beats the incumbent's
|
|
65
|
+
* count — but keyed by `tool` (a CLI, not a person) and writing to
|
|
66
|
+
* `curated/standards/<tool>-<topic>.md` (global) instead of
|
|
67
|
+
* `inferred/users/<userId>/<topic>.md` (per-user). No userId encoding
|
|
68
|
+
* needed here: a tool name (e.g. "jira") never contains a path separator.
|
|
69
|
+
*/
|
|
70
|
+
export declare function consolidateToolCorrection(tool: string, topic: string, deps: ToolCorrectionConsolidationDeps): Promise<void>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin glue turning an embedding model into the `(text) => Promise<number[]>`
|
|
3
|
+
* shape `episodic-store.ts`'s `storeEpisodicSummary` expects. Same "not
|
|
4
|
+
* worth mocking deeply" reasoning as `session/summarizer.ts` — one line
|
|
5
|
+
* of glue around the AI SDK's `embed`, no dedicated test file.
|
|
6
|
+
*/
|
|
7
|
+
import { type EmbeddingModel } from "ai";
|
|
8
|
+
/** Returns a function that embeds a string using `model`. */
|
|
9
|
+
export declare function createEmbedder(model: EmbeddingModel): (text: string) => Promise<number[]>;
|