@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,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin glue that turns `createSessionHistory`'s injected `summarize`
|
|
3
|
+
* dependency into a real LLM call.
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from `history.ts` on purpose: `history.ts`'s threshold/
|
|
6
|
+
* merge/replace logic is the substantial part and is fully unit-tested
|
|
7
|
+
* with a fake summarizer; this file's only job is producing the prompt
|
|
8
|
+
* and calling `generateText`, which isn't worth mocking deeply for one
|
|
9
|
+
* line of glue.
|
|
10
|
+
*
|
|
11
|
+
* Used by: `src/index.ts` (wiring), which passes the result into
|
|
12
|
+
* `createSessionHistory` (see `src/session/history.ts`).
|
|
13
|
+
*/
|
|
14
|
+
import { type LanguageModel } from "ai";
|
|
15
|
+
import type { Message } from "./history.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Returns a function matching `createSessionHistory`'s `summarize`
|
|
18
|
+
* signature, backed by `model`. The returned function asks the model to
|
|
19
|
+
* condense the given messages into a short summary that preserves
|
|
20
|
+
* names, ticket keys, and decisions — the things a future turn would
|
|
21
|
+
* otherwise lose once the raw messages are cleared.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createSummarizer(model: LanguageModel): (messages: Message[]) => Promise<string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Skill } from "@mercury-fw/plugin-types";
|
|
2
|
+
/** The assistant's persona, set by the instance: `identity` opens the system
|
|
3
|
+
* prompt, `tone` closes it. Either one left out falls back to its default. */
|
|
4
|
+
export type Persona = {
|
|
5
|
+
identity?: string;
|
|
6
|
+
tone?: string;
|
|
7
|
+
};
|
|
8
|
+
/** The identity an instance gets when its config sets none. */
|
|
9
|
+
export declare const DEFAULT_PERSONA_IDENTITY = "You are Mercury, an internal assistant.";
|
|
10
|
+
/** The tone an instance gets when its config sets none. */
|
|
11
|
+
export declare const DEFAULT_PERSONA_TONE: string;
|
|
12
|
+
/**
|
|
13
|
+
* Builds a system prompt that only describes tools actually present in
|
|
14
|
+
* `tools` (see `src/session/agent-turn.ts` for why a prompt mentioning
|
|
15
|
+
* an absent tool is a real bug, not a harmless no-op). The persona fills the
|
|
16
|
+
* first and last slots (see `personaSlot`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildSystemPrompt(opts: {
|
|
19
|
+
pluginFragments: string[];
|
|
20
|
+
skills: Skill[];
|
|
21
|
+
multiUserChannel: boolean;
|
|
22
|
+
persona?: Persona;
|
|
23
|
+
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* Builds the instance's two system prompts from the same plugins and persona:
|
|
26
|
+
* `system` for 1:1 channels (the terminal, the HTTP surface) and `chatSystem`
|
|
27
|
+
* for shared spaces, the only one that carries the multi-user clause — an
|
|
28
|
+
* operator typing normally must never get a NO_REPLY meant for a shared space.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildSystemPrompts(opts: {
|
|
31
|
+
pluginFragments: string[];
|
|
32
|
+
skills: Skill[];
|
|
33
|
+
/** Required even when undefined, so a caller can't silently drop the instance's persona. */
|
|
34
|
+
persona: Persona | undefined;
|
|
35
|
+
}): {
|
|
36
|
+
system: string;
|
|
37
|
+
chatSystem: string;
|
|
38
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects, within a single turn's steps, a CLI command call that failed
|
|
3
|
+
* followed later by one for the same binary that succeeded — a candidate
|
|
4
|
+
* procedural correction, distinct from `semantic-fact-extractor.ts` (which
|
|
5
|
+
* is about the user, from `session.messages`) — this is about a *tool*,
|
|
6
|
+
* from the tool-call trace (`StepInfo`), true for whoever uses that
|
|
7
|
+
* command next, not just this user. Pairing is deterministic (pure
|
|
8
|
+
* string/status matching); describing *what* the correction actually was
|
|
9
|
+
* is delegated to the model, same `generateObject`-with-fixed-schema style
|
|
10
|
+
* as `semantic-fact-extractor.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Wired per-turn (`onStepFinish`, see `index.ts`), not from
|
|
13
|
+
* `idle-session-cron.ts`'s idle sweep: the tool-call trace for a turn only
|
|
14
|
+
* exists in memory for the duration of that turn (`tool-log-buffer.ts` is a
|
|
15
|
+
* 200-entry ring buffer shared across every session — not a reliable place
|
|
16
|
+
* to reconstruct one turn's trace minutes or hours later).
|
|
17
|
+
*/
|
|
18
|
+
import { type LanguageModel } from "ai";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import type { StepInfo } from "./step-info.ts";
|
|
21
|
+
export declare const ProceduralCorrectionCandidateSchema: z.ZodObject<{
|
|
22
|
+
topic: z.ZodString;
|
|
23
|
+
value: z.ZodString;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export type ProceduralCorrection = {
|
|
26
|
+
tool: string;
|
|
27
|
+
topic: string;
|
|
28
|
+
value: string;
|
|
29
|
+
};
|
|
30
|
+
type GenerateObjectFn = (params: {
|
|
31
|
+
model: LanguageModel;
|
|
32
|
+
output: "array";
|
|
33
|
+
schema: typeof ProceduralCorrectionCandidateSchema;
|
|
34
|
+
instructions: string;
|
|
35
|
+
prompt: string;
|
|
36
|
+
}) => Promise<{
|
|
37
|
+
object: z.infer<typeof ProceduralCorrectionCandidateSchema>[];
|
|
38
|
+
}>;
|
|
39
|
+
/** Returns a function that extracts candidate procedural corrections from a single turn's steps. */
|
|
40
|
+
export declare function createToolCorrectionExtractor(model: LanguageModel, generateObjectFn?: GenerateObjectFn, deps?: {
|
|
41
|
+
log?: (msg: string) => void;
|
|
42
|
+
}): (steps: StepInfo[]) => Promise<ProceduralCorrection[]>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StepInfo } from "./step-info.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Whatever the provider that ran the turn calls itself — see
|
|
4
|
+
* `InboundTurn.channel` in `src/router/provider.ts`. Was a closed union
|
|
5
|
+
* ("terminal" | "google-chat") back when the set of providers was fixed in
|
|
6
|
+
* this file; widened so a new provider is a new `Provider` implementation,
|
|
7
|
+
* not an edit here.
|
|
8
|
+
*/
|
|
9
|
+
export type ToolLogChannel = string;
|
|
10
|
+
export type ToolLogEntry = {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
channel: ToolLogChannel;
|
|
13
|
+
sessionKey: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
input: string;
|
|
16
|
+
output: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function recordStep(channel: ToolLogChannel, sessionKey: string, step: StepInfo): void;
|
|
19
|
+
/** Most recent entries first, optionally restricted to one session. */
|
|
20
|
+
export declare function getToolLog(filter?: {
|
|
21
|
+
sessionKey?: string;
|
|
22
|
+
}): ToolLogEntry[];
|
|
23
|
+
/** Test-only: clears the module-level buffer so tests don't leak into each other. */
|
|
24
|
+
export declare function resetToolLogForTest(): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-invocable recall of this conversation's own past tool calls —
|
|
3
|
+
* same split as `wiki-tools.ts` wrapping plain functions in `tool()`.
|
|
4
|
+
* Exists because `SessionHistory` (`history.ts`) only ever persists the
|
|
5
|
+
* final assistant text of a turn, never the tool-call trace: asked to
|
|
6
|
+
* recall exactly what it ran, the model otherwise has no ground truth in
|
|
7
|
+
* its own context and reconstructs a plausible-looking (and sometimes
|
|
8
|
+
* wrong) answer instead of quoting the real one. Backed by
|
|
9
|
+
* `tool-log-buffer.ts`, filtered to the calling session only — never
|
|
10
|
+
* another conversation's history.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExecutableTool } from "@mercury-fw/plugin-types";
|
|
13
|
+
export type ToolLogRecallDeps = {
|
|
14
|
+
sessionKey: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function createToolLogRecallTool(deps: ToolLogRecallDeps): {
|
|
17
|
+
recall_tool_calls: ExecutableTool;
|
|
18
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps every tool's `execute` so `onToolStart` fires the moment a call
|
|
3
|
+
* actually begins (before its result is ready), instead of the existing
|
|
4
|
+
* `onStepFinish` visibility (`tool-log.ts`) which only reports after a step
|
|
5
|
+
* already finished, and `onToolFinish` fires once it settles. Used by
|
|
6
|
+
* `src/index.ts`'s `buildTools` for every channel — both terminal and
|
|
7
|
+
* Google Chat always supply a real `onToolStart` (`TurnSink` in
|
|
8
|
+
* `router/provider.ts`), so both are always wrapped.
|
|
9
|
+
*
|
|
10
|
+
* Every wrapped tool's `execute` calls share one `chain` (below), so at
|
|
11
|
+
* most one tool call runs at a time turn-wide — the AI SDK otherwise runs
|
|
12
|
+
* a step's tool calls concurrently (`Promise.all`), which this codebase
|
|
13
|
+
* has never actually relied on; serializing keeps Google Chat's status
|
|
14
|
+
* card for a given `toolCallId` (see `google-chat-provider.ts`) from ever
|
|
15
|
+
* having to handle an overlapping in-flight card.
|
|
16
|
+
*/
|
|
17
|
+
import type { Tool } from "ai";
|
|
18
|
+
/**
|
|
19
|
+
* Describes, in one short user-facing sentence, what a tool call is about to do
|
|
20
|
+
* — no arguments/queries shown. A tool a plugin contributed carries its own
|
|
21
|
+
* label via `toolStatusDescribers` (keyed by tool name), so the core never
|
|
22
|
+
* decides how a plugin's tool reads — it just looks the describer up. The other
|
|
23
|
+
* tools are core functionality (wiki, grep, memory) and keep their core-coined
|
|
24
|
+
* labels until they too become internal plugins.
|
|
25
|
+
*/
|
|
26
|
+
export declare function describeToolStart(toolName: string, input: unknown, toolStatusDescribers: Record<string, (input: unknown) => string>): string;
|
|
27
|
+
/**
|
|
28
|
+
* The actual command/input behind a tool call, for display in a status
|
|
29
|
+
* card — `describeToolStart`'s label alone doesn't say *what* ran, only
|
|
30
|
+
* which service. No markdown decoration: Google Chat cards don't render
|
|
31
|
+
* backticks as monospace, so a plain string reads better. Named per-tool
|
|
32
|
+
* (path for a file, pattern for a search, ...) rather than dumping the raw
|
|
33
|
+
* input, since that's what a human actually wants to see; only a genuinely
|
|
34
|
+
* unmapped/future tool falls back to a bounded JSON dump.
|
|
35
|
+
*/
|
|
36
|
+
export declare function describeToolDetail(toolName: string, input: unknown): string;
|
|
37
|
+
export type { ToolOutcome } from "@mercury-fw/channel-types";
|
|
38
|
+
import type { ToolOutcome } from "@mercury-fw/channel-types";
|
|
39
|
+
/**
|
|
40
|
+
* Every tool in this codebase returns a uniform `{ok: true, ...}` /
|
|
41
|
+
* `{ok: false, error, ...}` shape (confirmed across `wiki-tools.ts`,
|
|
42
|
+
* `tool-log-recall-tool.ts`, `cli-tool.ts`), with `cli-tool.ts`'s
|
|
43
|
+
* confirm-required staging additionally setting `pendingConfirmation: true`
|
|
44
|
+
* without having actually run the command yet — so this classifier works
|
|
45
|
+
* for any tool without per-tool special-casing. Defaults to `"success"` for
|
|
46
|
+
* an unrecognized result shape; nothing returned by a tool today hits that
|
|
47
|
+
* branch.
|
|
48
|
+
*/
|
|
49
|
+
export declare function classifyToolResult(result: unknown): ToolOutcome;
|
|
50
|
+
/**
|
|
51
|
+
* Wraps every tool's `execute` to call `onToolStart` first, then run the
|
|
52
|
+
* original, then `onToolFinish` once it settles. `chain` serializes every
|
|
53
|
+
* wrapped tool sharing this one `withToolStartHook` call (i.e. one turn's
|
|
54
|
+
* worth of tools, since `buildTools` builds fresh tools per turn) — see
|
|
55
|
+
* this file's header comment for why.
|
|
56
|
+
*/
|
|
57
|
+
export declare function withToolStartHook(tools: Record<string, Tool>, onToolStart: (label: string, detail?: string, toolCallId?: string) => void, toolStatusDescribers: Record<string, (input: unknown) => string>, onToolFinish?: (toolCallId: string, outcome: ToolOutcome) => void): Record<string, Tool>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory staging area for the user-facing `display` artifacts a tool
|
|
3
|
+
* produces (see `ToolDisplay` and the formatter decorator), decoupling
|
|
4
|
+
* *producing* an artifact from *showing* it. A tool `stash`es its already-
|
|
5
|
+
* rendered artifact here and gets back a short `ref`; the model, and only
|
|
6
|
+
* the model, decides whether to actually surface it by calling the
|
|
7
|
+
* `present` tool (see `present-tool.ts`), which marks the ref via
|
|
8
|
+
* `surface`. At finalize the turn runner appends only what
|
|
9
|
+
* `takeSurfaced` returns — the artifacts the model explicitly asked to
|
|
10
|
+
* show — never the whole set unconditionally. The worst case becomes
|
|
11
|
+
* omission (the user says "show me"), never a garbled or force-appended
|
|
12
|
+
* list.
|
|
13
|
+
*
|
|
14
|
+
* Scoped by `sessionKey`, exactly like `confirmation-store.ts`: a ref
|
|
15
|
+
* minted for one session (terminal, or a given Google Chat space+sender)
|
|
16
|
+
* can't be surfaced by another. The store is created once and shared
|
|
17
|
+
* across turns, so a ref stays valid (until its TTL) for a later
|
|
18
|
+
* "show me that list again" follow-up — session-scoped, not turn-scoped.
|
|
19
|
+
*/
|
|
20
|
+
export type DisplayStore = {
|
|
21
|
+
/** Stashes `artifact` for `sessionKey` and returns a fresh ref. */
|
|
22
|
+
stash(sessionKey: string, artifact: string): string;
|
|
23
|
+
/** Marks `ref` to be shown at finalize. Returns `false` if it doesn't
|
|
24
|
+
* exist, belongs to a different session, or has expired. */
|
|
25
|
+
surface(sessionKey: string, ref: string): boolean;
|
|
26
|
+
/** The artifacts surfaced for `sessionKey`, in stash order. Consumes the
|
|
27
|
+
* surfaced flag (so the same artifact isn't re-appended on a later turn
|
|
28
|
+
* unless `surface` is called again) but keeps the entry until its TTL,
|
|
29
|
+
* so a still-valid ref can be surfaced again across turns. */
|
|
30
|
+
takeSurfaced(sessionKey: string): string[];
|
|
31
|
+
};
|
|
32
|
+
export declare function createDisplayStore(opts?: {
|
|
33
|
+
now?: () => number;
|
|
34
|
+
ttlMs?: number;
|
|
35
|
+
refFn?: () => string;
|
|
36
|
+
}): DisplayStore;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `present` tool: the model's explicit "show this artifact to the user"
|
|
3
|
+
* action. A tool that produced a user-facing `display` stashed it in the
|
|
4
|
+
* `DisplayStore` (see `display-store.ts`) and returned a `ref` on the model
|
|
5
|
+
* channel; the model never sees the rendered content, only the ref. Calling
|
|
6
|
+
* `present(ref)` marks that ref to be appended to the reply at finalize.
|
|
7
|
+
*
|
|
8
|
+
* This is what shrinks the model's job from "format this list correctly"
|
|
9
|
+
* (which small models garble) to "show this list? yes/no": the artifact is
|
|
10
|
+
* always the deterministic one the formatter produced, and the model only
|
|
11
|
+
* decides whether it appears. If the model never calls `present`, nothing is
|
|
12
|
+
* shown — the right outcome for a "how many are open?" question answered in
|
|
13
|
+
* prose. Scoped to the calling session, so it can only surface its own refs.
|
|
14
|
+
*/
|
|
15
|
+
import type { ExecutableTool } from "@mercury-fw/plugin-types";
|
|
16
|
+
import type { DisplayStore } from "./display-store.ts";
|
|
17
|
+
export type PresentToolDeps = {
|
|
18
|
+
sessionKey: string;
|
|
19
|
+
store: DisplayStore;
|
|
20
|
+
};
|
|
21
|
+
export declare function createPresentTool(deps: PresentToolDeps): {
|
|
22
|
+
present: ExecutableTool;
|
|
23
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter schema for wiki notes, based on Open Knowledge
|
|
3
|
+
* Format (OKF — https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing):
|
|
4
|
+
* `type` is the only field OKF mandates; `source`/`confidence`/
|
|
5
|
+
* `derived_from`/`last_reviewed` are Mercury-specific extensions, the
|
|
6
|
+
* kind OKF explicitly leaves to the producer. `curated` and `inferred`
|
|
7
|
+
* are validated as separate shapes (discriminated on `type`) because
|
|
8
|
+
* only `inferred` notes carry provenance — a curated doc has no
|
|
9
|
+
* meaningful `confidence` or `derived_from`.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
export declare const CuratedFrontmatterSchema: z.ZodObject<{
|
|
13
|
+
type: z.ZodLiteral<"curated">;
|
|
14
|
+
author: z.ZodOptional<z.ZodString>;
|
|
15
|
+
last_updated: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export declare const InferredFrontmatterSchema: z.ZodObject<{
|
|
18
|
+
type: z.ZodLiteral<"inferred">;
|
|
19
|
+
source: z.ZodLiteral<"agent">;
|
|
20
|
+
confidence: z.ZodEnum<{
|
|
21
|
+
low: "low";
|
|
22
|
+
medium: "medium";
|
|
23
|
+
high: "high";
|
|
24
|
+
}>;
|
|
25
|
+
derived_from: z.ZodArray<z.ZodString>;
|
|
26
|
+
last_reviewed: z.ZodNullable<z.ZodString>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
/**
|
|
29
|
+
* A fourth category: the lifecycle of one confirm-required action, from
|
|
30
|
+
* staging through its eventual resolution — a deterministic instruction
|
|
31
|
+
* the user explicitly approved via the confirmation-token mechanism,
|
|
32
|
+
* never an autonomous LLM judgment call. Tracks a specific CLI action's
|
|
33
|
+
* own token, written once when staged (`status: "pending"`) and
|
|
34
|
+
* overwritten in place once resolved (`"confirmed"`/`"failed"`). Lives
|
|
35
|
+
* outside `inferred/users/<userId>/` (see `wiki-read.ts`'s
|
|
36
|
+
* `allowedRoots`) so it's structurally invisible to the model's own
|
|
37
|
+
* `list_files`/`grep` — reachable only via the narrow `resolve_reference`
|
|
38
|
+
* tool given the exact token (see `wiki-tools.ts`), never by browsing.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ConfirmationFrontmatterSchema: z.ZodObject<{
|
|
41
|
+
type: z.ZodLiteral<"confirmation">;
|
|
42
|
+
status: z.ZodEnum<{
|
|
43
|
+
pending: "pending";
|
|
44
|
+
failed: "failed";
|
|
45
|
+
confirmed: "confirmed";
|
|
46
|
+
}>;
|
|
47
|
+
requested_at: z.ZodString;
|
|
48
|
+
resolved_at: z.ZodNullable<z.ZodString>;
|
|
49
|
+
command: z.ZodString;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
export type CuratedFrontmatter = z.infer<typeof CuratedFrontmatterSchema>;
|
|
52
|
+
export type InferredFrontmatter = z.infer<typeof InferredFrontmatterSchema>;
|
|
53
|
+
export type ConfirmationFrontmatter = z.infer<typeof ConfirmationFrontmatterSchema>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic index.md line management. Turns "this curated doc, with
|
|
3
|
+
* this description" into the exact `[[wikilink]]` line format
|
|
4
|
+
* `orphan-detector.ts`'s wikilink check recognizes — instead of trusting
|
|
5
|
+
* the model to freely author (and correctly reproduce, on every edit) the
|
|
6
|
+
* whole file's syntax by hand, which is what `write_index` used to do and
|
|
7
|
+
* how a doc could end up with an index.md line that still isn't
|
|
8
|
+
* recognized as a reference to it.
|
|
9
|
+
*/
|
|
10
|
+
/** Accepts any of "curated/x/y.md", "x/y.md", or "x/y" and normalizes to "x/y" — the form `[[wikilink]]`s use. */
|
|
11
|
+
export declare function normalizeIndexKey(path: string): string;
|
|
12
|
+
/** Adds a line for `key`, or replaces its existing one in place — never duplicates an entry. */
|
|
13
|
+
export declare function upsertIndexEntry(content: string, key: string, description: string): string;
|
|
14
|
+
/** Removes `key`'s line if present; no-op otherwise. */
|
|
15
|
+
export declare function removeIndexEntry(content: string, key: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findOrphanCuratedDocs(vaultPath: string): Promise<string[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nightly self-review job's three LLM passes. Each is a fresh,
|
|
3
|
+
* stateless `generateText` call — never a persisted conversation history
|
|
4
|
+
* like `agent-turn.ts`'s `runTurn` — so nothing carries over between
|
|
5
|
+
* passes or between nights except what actually landed on disk. Running
|
|
6
|
+
* all four checks as one giant multi-step call would let tool-call
|
|
7
|
+
* context accumulate across unrelated jobs at once; splitting into three
|
|
8
|
+
* independent calls keeps each one's context bounded to its own job.
|
|
9
|
+
*
|
|
10
|
+
* `generateText` comes from `ai-sdk-ollama`, matching `agent-turn.ts` —
|
|
11
|
+
* plain `ai`'s version is documented there to return empty text after a
|
|
12
|
+
* tool call on this codebase's Ollama setup, which applies here too
|
|
13
|
+
* since every pass uses tools.
|
|
14
|
+
*/
|
|
15
|
+
import { type LanguageModel, type Tool } from "ai";
|
|
16
|
+
import type { Message } from "../session/history.ts";
|
|
17
|
+
/** Empirical, tuned later — generous headroom for reading several docs and
|
|
18
|
+
* writing/deleting within one pass, well above a conversational turn's. */
|
|
19
|
+
export declare const SELF_REVIEW_STEP_COUNT = 15;
|
|
20
|
+
type GenerateTextFn = (params: {
|
|
21
|
+
model: LanguageModel;
|
|
22
|
+
messages: Message[];
|
|
23
|
+
tools: Record<string, Tool>;
|
|
24
|
+
instructions: string;
|
|
25
|
+
}) => Promise<{
|
|
26
|
+
text: string;
|
|
27
|
+
}>;
|
|
28
|
+
export type RawTriagePassDeps = {
|
|
29
|
+
vaultPath: string;
|
|
30
|
+
model: LanguageModel;
|
|
31
|
+
rawEntries: string[];
|
|
32
|
+
generateTextFn?: GenerateTextFn;
|
|
33
|
+
};
|
|
34
|
+
export declare function runRawTriagePass(deps: RawTriagePassDeps): Promise<void>;
|
|
35
|
+
export type IndexAndOrphanPassDeps = {
|
|
36
|
+
vaultPath: string;
|
|
37
|
+
model: LanguageModel;
|
|
38
|
+
orphans: string[];
|
|
39
|
+
generateTextFn?: GenerateTextFn;
|
|
40
|
+
};
|
|
41
|
+
export declare function runIndexAndOrphanPass(deps: IndexAndOrphanPassDeps): Promise<void>;
|
|
42
|
+
export type ContradictionCheckPassDeps = {
|
|
43
|
+
vaultPath: string;
|
|
44
|
+
model: LanguageModel;
|
|
45
|
+
generateTextFn?: GenerateTextFn;
|
|
46
|
+
};
|
|
47
|
+
export declare function runContradictionCheckPass(deps: ContradictionCheckPassDeps): Promise<void>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nightly self-review job's own tool set — distinct from
|
|
3
|
+
* `wiki-tools.ts` (the model-invocable tools a normal conversation gets),
|
|
4
|
+
* same reasoning already applied to `writeInferredNote` never being
|
|
5
|
+
* wired into those: this is a separate trust context (a stateless admin
|
|
6
|
+
* batch job, not a live conversation), scoped via `selfReviewRoots`
|
|
7
|
+
* (curated/ + raw/, never inferred/ — reserved for deterministic,
|
|
8
|
+
* mechanically-written notes, not an LLM's own judgment call) and with
|
|
9
|
+
* capabilities no conversational tool
|
|
10
|
+
* has (deleting an entry, updating/removing an index.md entry — the
|
|
11
|
+
* model supplies a doc path and a description, never the file's raw text,
|
|
12
|
+
* so an update can't get the `[[wikilink]]` format wrong).
|
|
13
|
+
*
|
|
14
|
+
* All three nightly sub-passes (`self-review-runner.ts`) share this
|
|
15
|
+
* exact tool set — they differ only in system prompt and pre-computed
|
|
16
|
+
* input data, not in which tools they can call.
|
|
17
|
+
*/
|
|
18
|
+
import type { ExecutableTool } from "@mercury-fw/plugin-types";
|
|
19
|
+
export type SelfReviewToolsDeps = {
|
|
20
|
+
vaultPath: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function createSelfReviewTools(deps: SelfReviewToolsDeps): Record<"list_files" | "read_file" | "grep" | "write_curated" | "update_index_entry" | "remove_index_entry" | "delete_raw" | "delete_curated", ExecutableTool>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates the vault's curated/inferred subdirectories and git-inits the
|
|
3
|
+
* vault if it isn't already a git repo. Safe to call on every startup:
|
|
4
|
+
* pre-existing directories/content are left untouched, and re-running
|
|
5
|
+
* `git init` on an already-initialized repo is a no-op.
|
|
6
|
+
*/
|
|
7
|
+
export declare function initVault(vaultPath: string): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Writes a curated doc at `curated/<relativePath>` (e.g. "standards/jira-fields.md"). */
|
|
2
|
+
export declare function writeCuratedNote(vaultPath: string, relativePath: string, fields: {
|
|
3
|
+
author?: string;
|
|
4
|
+
last_updated?: string;
|
|
5
|
+
}, body: string): Promise<void>;
|
|
6
|
+
/** Writes a semantic note at `inferred/users/<userId>/<topic>.md`. */
|
|
7
|
+
export declare function writeInferredNote(vaultPath: string, userId: string, topic: string, fields: {
|
|
8
|
+
confidence: "low" | "medium" | "high";
|
|
9
|
+
derived_from: string[];
|
|
10
|
+
last_reviewed: string | null;
|
|
11
|
+
}, body: string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Writes a deterministically-promoted procedural correction at
|
|
14
|
+
* `curated/standards/<tool>-<topic>.md` — one file per correction, not
|
|
15
|
+
* merged into a single per-tool doc (that would need safe section-level
|
|
16
|
+
* merging into whatever a human already wrote by hand there, e.g.
|
|
17
|
+
* `curated/standards/jira-cli.md`, deliberately out of scope here).
|
|
18
|
+
* Frontmatter is still `type: inferred, source: agent` (same provenance
|
|
19
|
+
* shape as `writeInferredNote` — probabilistic, consolidation-derived, not
|
|
20
|
+
* human-authored) even though the file lives under `curated/`: the path
|
|
21
|
+
* controls read visibility (every user's wiki tools expose `curated/`,
|
|
22
|
+
* only their own `inferred/users/<userId>/`), not authorship.
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeToolCorrectionNote(vaultPath: string, tool: string, topic: string, fields: {
|
|
25
|
+
confidence: "low" | "medium" | "high";
|
|
26
|
+
derived_from: string[];
|
|
27
|
+
last_reviewed: string | null;
|
|
28
|
+
}, body: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Writes (or overwrites) the deterministic lifecycle record for one
|
|
31
|
+
* confirm-required action at `inferred/confirmations/<encoded userId>/<token>.md`
|
|
32
|
+
* — see `ConfirmationFrontmatterSchema`'s own doc comment for why this
|
|
33
|
+
* subtree, not `inferred/users/<userId>/`. Called twice per action: once
|
|
34
|
+
* at staging (`status: "pending"`, `resolvedAt: null`), once at resolution
|
|
35
|
+
* (`"confirmed"`/`"failed"`, `resolvedAt` set) — the whole-file replace
|
|
36
|
+
* every writer here already does, not a partial update.
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeConfirmationNote(vaultPath: string, userId: string, token: string, fields: {
|
|
39
|
+
status: "pending" | "confirmed" | "failed";
|
|
40
|
+
requestedAt: string;
|
|
41
|
+
resolvedAt: string | null;
|
|
42
|
+
command: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
/** Writes a raw/ inbox entry verbatim at `raw/<relativePath>` — no
|
|
45
|
+
* frontmatter, content is whatever a human pasted as-is (see
|
|
46
|
+
* `vault-cli.ts`'s `write-raw`). Never called during a normal
|
|
47
|
+
* conversation; only the self-review job (`self-review-tools.ts`) reads
|
|
48
|
+
* this back to triage it into `curated/`. */
|
|
49
|
+
export declare function writeRawEntry(vaultPath: string, relativePath: string, body: string): Promise<void>;
|
|
50
|
+
/** Overwrites `index.md` at the vault root with `content` verbatim — no
|
|
51
|
+
* frontmatter, it's a generated Karpathy-pattern index, not a note.
|
|
52
|
+
* Whole-file replace: the caller (self-review) computes the full new
|
|
53
|
+
* text and passes the complete replacement, same as every writer here. */
|
|
54
|
+
export declare function writeIndexFile(vaultPath: string, content: string): Promise<void>;
|
|
55
|
+
/** Deletes a raw/ entry once self-review has resolved it (merged,
|
|
56
|
+
* promoted, or discarded). */
|
|
57
|
+
export declare function deleteRawEntry(vaultPath: string, relativePath: string): Promise<void>;
|
|
58
|
+
/** Deletes a curated/ doc — used only by the self-review job to retire a
|
|
59
|
+
* redundant/superseded doc (never during a normal conversation). Callers
|
|
60
|
+
* should also remove the doc's `index.md` line in the same pass, so a
|
|
61
|
+
* deletion doesn't leave a dangling index reference. */
|
|
62
|
+
export declare function deleteCuratedEntry(vaultPath: string, relativePath: string): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** curated/ + raw/ only — never inferred/, for the nightly self-review job.
|
|
2
|
+
* A distinct trust boundary from a per-user conversation's `allowedRoots`
|
|
3
|
+
* (which trades curated/ for one user's own inferred/ instead of raw/):
|
|
4
|
+
* inferred/ is meant to hold only deterministic, mechanically-written
|
|
5
|
+
* notes (never an LLM's own judgment call about what to remember) —
|
|
6
|
+
* off-limits here for the same reason it's off-limits to regular
|
|
7
|
+
* conversations, not a special exception for self-review. */
|
|
8
|
+
export declare function selfReviewRoots(vaultPath: string): string[];
|
|
9
|
+
/** Lists every `.md` file under `roots`. */
|
|
10
|
+
export declare function listWikiFilesInRoots(vaultPath: string, roots: string[]): Promise<string[]>;
|
|
11
|
+
/** Lists every `.md` file visible to `userId`: all of curated/, plus only their own inferred/users/<userId>/. */
|
|
12
|
+
export declare function listWikiFiles(vaultPath: string, userId: string): Promise<string[]>;
|
|
13
|
+
/** Reads a single wiki file. Throws if `relativePath` falls outside `roots`. */
|
|
14
|
+
export declare function readWikiFileInRoots(vaultPath: string, roots: string[], relativePath: string): Promise<string>;
|
|
15
|
+
/** Reads a single wiki file. Throws if `relativePath` falls outside the caller's allowed scope. */
|
|
16
|
+
export declare function readWikiFile(vaultPath: string, userId: string, relativePath: string): Promise<string>;
|
|
17
|
+
export type WikiGrepMatch = {
|
|
18
|
+
path: string;
|
|
19
|
+
line: number;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
22
|
+
/** Searches every file under `roots` for `pattern` (a regular expression), line by line. */
|
|
23
|
+
export declare function grepWikiInRoots(vaultPath: string, roots: string[], pattern: string): Promise<WikiGrepMatch[]>;
|
|
24
|
+
/** Searches every file visible to `userId` for `pattern` (a regular expression), line by line. */
|
|
25
|
+
export declare function grepWiki(vaultPath: string, userId: string, pattern: string): Promise<WikiGrepMatch[]>;
|
|
26
|
+
/** Reads index.md at the vault root; empty string if it doesn't exist yet (a brand-new vault, or one where self-review hasn't created it). */
|
|
27
|
+
export declare function readIndexFile(vaultPath: string): Promise<string>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExecutableTool } from "@mercury-fw/plugin-types";
|
|
2
|
+
export type WikiToolsDeps = {
|
|
3
|
+
vaultPath: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
};
|
|
6
|
+
/** Builds the four wiki tools scoped to `deps.userId` (curated/ fully, only their own inferred/users/<userId>/). */
|
|
7
|
+
export declare function createWikiTools(deps: WikiToolsDeps): Record<"list_files" | "read_file" | "write_file" | "grep" | "resolve_reference", ExecutableTool>;
|
package/index.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";
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mercury-fw/core",
|
|
3
|
+
"version": "0.25.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lucabro81/mercury-fw.git",
|
|
9
|
+
"directory": "packages/libs/core"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"*.ts",
|
|
13
|
+
"dist",
|
|
14
|
+
"src",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"!**/*.test.ts",
|
|
17
|
+
"!**/__fixtures__"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"mercury-fw-source": "./index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./index.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "bun test"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@mercury-fw/channel-types": "0.25.0",
|
|
35
|
+
"@mercury-fw/cli-engine": "0.25.0",
|
|
36
|
+
"@mercury-fw/confirm-engine": "0.25.0",
|
|
37
|
+
"@mercury-fw/plugin-types": "0.25.0",
|
|
38
|
+
"@qdrant/js-client-rest": "^1.19.0",
|
|
39
|
+
"ai": "^7.0.77",
|
|
40
|
+
"ai-sdk-ollama": "^4.2.0",
|
|
41
|
+
"yaml": "^2.9.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@mercury-fw/typescript-config": "*",
|
|
46
|
+
"@types/bun": "^1.4.0",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|