@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/src/compose.ts
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a Mercury instance from its composition config: the model, the tools
|
|
3
|
+
* this instance has enabled, the memory layers, and the turn pipeline. This is
|
|
4
|
+
* the one place that decides which tools and channels exist on this instance —
|
|
5
|
+
* every other module takes them as inputs.
|
|
6
|
+
*
|
|
7
|
+
* It *builds*, it does not *start*: `composeMercury()` returns `handleTurn`, the
|
|
8
|
+
* channel runtime context, and deferred `startCrons`/`startAdmin` closures, so
|
|
9
|
+
* each entrypoint decides what to run. The long-running service (`index.ts`)
|
|
10
|
+
* starts the network channels + crons + admin; the dev REPL (`repl.ts`) starts
|
|
11
|
+
* only the terminal against the same `handleTurn`.
|
|
12
|
+
*
|
|
13
|
+
* Note: this factory (and `repl.ts`) are written to be lifted into the future
|
|
14
|
+
* Mercury CLI (a separate monorepo app that scaffolds an instance) — that CLI
|
|
15
|
+
* would reuse `composeMercury` to boot the app it built. Not built yet.
|
|
16
|
+
*/
|
|
17
|
+
import { QdrantClient } from "@qdrant/js-client-rest";
|
|
18
|
+
import { getOllamaProvider } from "./model/client.ts";
|
|
19
|
+
import { runCli } from "@mercury-fw/cli-engine";
|
|
20
|
+
import { createConfirmationStore, createStageConfirmation, tryConfirm, resolveConfirmation, type ConfirmationStore } from "@mercury-fw/confirm-engine";
|
|
21
|
+
import { createDisplayStore } from "./tools/display-store.ts";
|
|
22
|
+
import { createPresentTool } from "./tools/present-tool.ts";
|
|
23
|
+
import { loadPlugins } from "./plugins/plugin-loader.ts";
|
|
24
|
+
import type { MercuryConfig } from "./config/define-config.ts";
|
|
25
|
+
import { createSessionHistory, type SessionHistory, type Message } from "./session/history.ts";
|
|
26
|
+
import { createSummarizer } from "./session/summarizer.ts";
|
|
27
|
+
import { createEpisodicSummarizer } from "./session/episodic-summarizer.ts";
|
|
28
|
+
import { createSemanticFactExtractor } from "./session/semantic-fact-extractor.ts";
|
|
29
|
+
import { buildContextPrimer } from "./session/context-primer.ts";
|
|
30
|
+
import { buildSystemPrompts } from "./session/system-prompt.ts";
|
|
31
|
+
import { createTurnRunner } from "./router/turn-runner.ts";
|
|
32
|
+
import type { TurnSink } from "./router/provider.ts";
|
|
33
|
+
import type { HandleTurn, ChannelRuntimeContext, ChannelPlugin } from "@mercury-fw/channel-types";
|
|
34
|
+
import {
|
|
35
|
+
truncateForDisplay,
|
|
36
|
+
describeToolOutcome,
|
|
37
|
+
} from "./router/tool-log.ts";
|
|
38
|
+
import type { StepInfo } from "./session/step-info.ts";
|
|
39
|
+
import { withToolStartHook } from "./session/tool-start-hook.ts";
|
|
40
|
+
import {
|
|
41
|
+
writeInferredNote,
|
|
42
|
+
writeToolCorrectionNote,
|
|
43
|
+
writeConfirmationNote,
|
|
44
|
+
} from "./wiki/wiki-note.ts";
|
|
45
|
+
import { createWikiTools } from "./wiki/wiki-tools.ts";
|
|
46
|
+
import { createToolLogRecallTool } from "./session/tool-log-recall-tool.ts";
|
|
47
|
+
import { createReadSkillTool } from "./session/read-skill-tool.ts";
|
|
48
|
+
import { createIdleSessionScanner } from "./cron/idle-session-scanner.ts";
|
|
49
|
+
import { startIdleSessionCron, captureSessionToMemory, type CaptureDeps } from "./cron/idle-session-cron.ts";
|
|
50
|
+
import {
|
|
51
|
+
ensureEpisodicCollection,
|
|
52
|
+
storeEpisodicSummary,
|
|
53
|
+
getLastSessionEpisodicSummaries,
|
|
54
|
+
} from "./memory/episodic-store.ts";
|
|
55
|
+
import { ensureVerbatimCollection, listVerbatimBySession, listVerbatimSessions } from "./memory/verbatim-archive-store.ts";
|
|
56
|
+
import { createVerbatimArchiveProvider } from "./memory/memory-provider.ts";
|
|
57
|
+
import { ensureSemanticFactsCollection, storeSemanticFact, searchSemanticFactsByTopic } from "./memory/semantic-facts-store.ts";
|
|
58
|
+
import { ensureToolCorrectionsCollection, storeToolCorrection, searchToolCorrectionsByTopic } from "./memory/tool-corrections-store.ts";
|
|
59
|
+
import { consolidateSemanticFact, consolidateToolCorrection, type ToolCorrectionConsolidationDeps } from "./cron/semantic-consolidation.ts";
|
|
60
|
+
import { createToolCorrectionExtractor } from "./session/tool-correction-extractor.ts";
|
|
61
|
+
import { createEmbedder } from "./memory/embedder.ts";
|
|
62
|
+
import { initVault } from "./wiki/vault-init.ts";
|
|
63
|
+
import { findOrphanCuratedDocs } from "./wiki/orphan-detector.ts";
|
|
64
|
+
import { listWikiFilesInRoots, readWikiFile, readWikiFileInRoots, readIndexFile } from "./wiki/wiki-read.ts";
|
|
65
|
+
import { runRawTriagePass, runIndexAndOrphanPass, runContradictionCheckPass } from "./wiki/self-review-runner.ts";
|
|
66
|
+
import { startSelfReviewCron } from "./cron/self-review-cron.ts";
|
|
67
|
+
import { resolve as resolvePath } from "node:path";
|
|
68
|
+
import type { Tool } from "ai";
|
|
69
|
+
import { startAdminServer } from "./admin/server.ts";
|
|
70
|
+
// The HTTP surface's read routes (4b) reuse the admin panel's per-domain
|
|
71
|
+
// functions — the admin is a POC to be retired later; these reads outlive it.
|
|
72
|
+
import { listWikiVault, readWikiVaultFile, grepWikiVault } from "./admin/wiki-routes.ts";
|
|
73
|
+
import { scrollCollection } from "./admin/qdrant-scroll.ts";
|
|
74
|
+
import { getSelfHealth } from "./admin/model-routes.ts";
|
|
75
|
+
import { getToolLog } from "./session/tool-log-buffer.ts";
|
|
76
|
+
import { buildPluginManifest } from "./plugins/manifest.ts";
|
|
77
|
+
|
|
78
|
+
/** A stoppable subsystem (cron, server). */
|
|
79
|
+
type Stoppable = { stop: () => void };
|
|
80
|
+
|
|
81
|
+
/** The confirm capability's binding to this instance's store/vault/note-writer, shared by the terminal and the channel runtime. */
|
|
82
|
+
export type ConfirmDeps = {
|
|
83
|
+
store: ConfirmationStore;
|
|
84
|
+
vaultPath: string;
|
|
85
|
+
writeConfirmationNoteFn: typeof writeConfirmationNote;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The built Mercury instance. `build`, not `start`: the channels, crons and
|
|
90
|
+
* admin are not running until an entrypoint starts them.
|
|
91
|
+
*/
|
|
92
|
+
export type ComposedApp = {
|
|
93
|
+
/** The turn driver every channel funnels through (see `turn-runner.ts`). */
|
|
94
|
+
handleTurn: HandleTurn;
|
|
95
|
+
/** The declared channel plugins (from `mercury.config.ts`). */
|
|
96
|
+
channels: ChannelPlugin[];
|
|
97
|
+
/** The runtime context each channel's `build()` gets — confirm + in-process reads injected by the core. */
|
|
98
|
+
channelRuntime: ChannelRuntimeContext;
|
|
99
|
+
/** The confirm binding, for a caller (the terminal) that intercepts tokens directly. */
|
|
100
|
+
confirmDeps: ConfirmDeps;
|
|
101
|
+
ollamaHost: string;
|
|
102
|
+
ollamaModel: string;
|
|
103
|
+
/** Starts the Layer-3 idle-capture and self-review crons; returns a single stopper for shutdown. */
|
|
104
|
+
startCrons: () => Stoppable;
|
|
105
|
+
/** Starts the POC admin panel if `ADMIN_PANEL_ENABLED`; returns it (to stop on shutdown), or undefined. */
|
|
106
|
+
startAdmin: () => Stoppable | undefined;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Reads a required env var, failing fast instead of silently defaulting. */
|
|
110
|
+
function requireEnv(name: string): string {
|
|
111
|
+
const value = process.env[name];
|
|
112
|
+
if (!value) {
|
|
113
|
+
throw new Error(`${name} is not set`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Builds a Mercury instance from the composition `config` it is given + env. The
|
|
120
|
+
* config is a parameter, never imported here: that's what makes the core
|
|
121
|
+
* app-agnostic — an entrypoint (the service, the dev REPL, a future scaffolded
|
|
122
|
+
* app) reads its own `mercury.config.ts` and passes it in. See {@link ComposedApp}.
|
|
123
|
+
*/
|
|
124
|
+
export async function composeMercury(config: MercuryConfig): Promise<ComposedApp> {
|
|
125
|
+
const enabledClis = (process.env.MERCURY_CLIS ?? "")
|
|
126
|
+
.split(",")
|
|
127
|
+
.map((s) => s.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
|
|
130
|
+
// The model is constructed up front, before the plugins load and the system
|
|
131
|
+
// prompt is built: a plugin's post-turn guard can be model-backed (Jira's
|
|
132
|
+
// issue-list corrector is), and the system prompt is assembled from the
|
|
133
|
+
// fragments the plugin loader returns — both need the model in hand first.
|
|
134
|
+
const provider = getOllamaProvider();
|
|
135
|
+
const ollamaHost = requireEnv("OLLAMA_HOST"); // already validated by getOllamaProvider(); read again here for the terminal provider's getLoadedContextLength call
|
|
136
|
+
const ollamaModel = requireEnv("OLLAMA_MODEL");
|
|
137
|
+
// think: true enables Ollama's native extended-thinking tokens — see
|
|
138
|
+
// agent-turn.ts's reasoning-delta handling. OLLAMA_THINK lets the deployer
|
|
139
|
+
// turn it off for a model that doesn't support it (Ollama rejects the request
|
|
140
|
+
// outright otherwise, 400 "does not support thinking"), same reasoning as
|
|
141
|
+
// never guessing OLLAMA_HOST. Model-construction-time setting only, set once
|
|
142
|
+
// here for the one shared model instance used by every channel.
|
|
143
|
+
const ollamaThink = process.env.OLLAMA_THINK !== "false";
|
|
144
|
+
const model = provider(ollamaModel, { think: ollamaThink });
|
|
145
|
+
const summarize = createSummarizer(model);
|
|
146
|
+
|
|
147
|
+
// Plugins are declared in the instance's config (its `mercury.config.ts`) and
|
|
148
|
+
// handed in — this file no longer names them. The loader processes an opaque
|
|
149
|
+
// list (see plugins/plugin-loader.ts): each supplies its allowlist as data, a
|
|
150
|
+
// system-prompt fragment, and a `build()` that turns the runtime context into
|
|
151
|
+
// post-processors and post-turn guards. A plugin contributes only when it's
|
|
152
|
+
// both listed in MERCURY_CLIS and its allowlist validates; one that fails
|
|
153
|
+
// degrades only itself.
|
|
154
|
+
const plugins = config.plugins;
|
|
155
|
+
|
|
156
|
+
const loadedPlugins = await loadPlugins(plugins, {
|
|
157
|
+
enabledClis,
|
|
158
|
+
model,
|
|
159
|
+
env: process.env,
|
|
160
|
+
log: (msg) => console.error(msg),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Status labels for the tool-start hook, keyed by tool name: each plugin's
|
|
164
|
+
// describer for its own tool (jiraCommand, …).
|
|
165
|
+
const toolStatusDescribers: Record<string, (input: unknown) => string> = { ...loadedPlugins.toolStatusDescribers };
|
|
166
|
+
|
|
167
|
+
// Two system prompts (1:1 and shared-space), both built from the fragments of
|
|
168
|
+
// whatever plugins actually loaded and from the instance's persona.
|
|
169
|
+
const { system, chatSystem } = buildSystemPrompts({
|
|
170
|
+
pluginFragments: loadedPlugins.promptFragments,
|
|
171
|
+
skills: loadedPlugins.skills,
|
|
172
|
+
persona: config.persona,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const histories = new Map<string, SessionHistory>();
|
|
176
|
+
/**
|
|
177
|
+
* `trackForCapture` wires `onBeforeCompress` so a Layer 1 compression also
|
|
178
|
+
* mirrors the compressed batch to Qdrant (see `captureIncrement`) — only
|
|
179
|
+
* meaningful for sessions tracked in `sessionUsers`/`sessionCaptureMarkers`
|
|
180
|
+
* (a real per-user identity, i.e. Google Chat); an identity-less channel
|
|
181
|
+
* omits it.
|
|
182
|
+
*/
|
|
183
|
+
function getOrCreateHistory(key: string, trackForCapture = false, primer?: string): SessionHistory {
|
|
184
|
+
let history = histories.get(key);
|
|
185
|
+
if (!history) {
|
|
186
|
+
history = createSessionHistory(
|
|
187
|
+
summarize,
|
|
188
|
+
trackForCapture
|
|
189
|
+
? (messages) => {
|
|
190
|
+
void captureIncrement(key, messages).finally(() => {
|
|
191
|
+
// The new getMessages() view after compression starts fresh
|
|
192
|
+
// (just the new synthetic summary message) — the old marker's
|
|
193
|
+
// index has no meaning against it regardless of whether the
|
|
194
|
+
// capture above succeeded.
|
|
195
|
+
sessionCaptureMarkers.set(key, 0);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
: undefined,
|
|
199
|
+
primer,
|
|
200
|
+
);
|
|
201
|
+
histories.set(key, history);
|
|
202
|
+
}
|
|
203
|
+
return history;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Session persistence, Layer 3: a tracked session idle past
|
|
207
|
+
// SESSION_IDLE_TIMEOUT_MS is summarized and written to Qdrant as a dated
|
|
208
|
+
// episodic record, then discarded. Identity-less sessions (the terminal) are
|
|
209
|
+
// never tracked here — per-user isolation needs a real sender identity.
|
|
210
|
+
const sessionUsers = new Map<string, string>(); // session key -> sender (userId)
|
|
211
|
+
// How many of a session's current getMessages() entries have already been
|
|
212
|
+
// mirrored to Qdrant by captureIncrement — advanced only after a successful
|
|
213
|
+
// capture, so a failure retries the same (or a larger) slice next time.
|
|
214
|
+
// Reset to 0 whenever Layer 1 compresses the session. Discarded on
|
|
215
|
+
// idle-timeout close, same as sessionUsers.
|
|
216
|
+
const sessionCaptureMarkers = new Map<string, number>();
|
|
217
|
+
// The current turn's tool-status callbacks, refreshed each turn — looked up
|
|
218
|
+
// lazily by captureIncrement/onBeforeCompress rather than captured once.
|
|
219
|
+
const sessionOnCaptureCallbacks = new Map<
|
|
220
|
+
string,
|
|
221
|
+
{ onToolStart: TurnSink["onToolStart"]; onToolFinish: TurnSink["onToolFinish"] }
|
|
222
|
+
>();
|
|
223
|
+
const idleScanner = createIdleSessionScanner();
|
|
224
|
+
const episodicSummarize = createEpisodicSummarizer(model);
|
|
225
|
+
const embeddingModel = provider.textEmbeddingModel(process.env.OLLAMA_EMBEDDING_MODEL ?? "nomic-embed-text");
|
|
226
|
+
const embed = createEmbedder(embeddingModel);
|
|
227
|
+
const qdrant = new QdrantClient({ url: process.env.QDRANT_URL ?? "http://qdrant:6333" });
|
|
228
|
+
const episodicCollection = process.env.QDRANT_EPISODIC_COLLECTION ?? "episodic_memory";
|
|
229
|
+
const episodicVectorSize = Number(process.env.QDRANT_EPISODIC_VECTOR_SIZE ?? "768");
|
|
230
|
+
await ensureEpisodicCollection(qdrant, episodicCollection, episodicVectorSize);
|
|
231
|
+
|
|
232
|
+
// Verbatim conversation archive (#4): a distinct collection holding the raw
|
|
233
|
+
// user<->model exchange, lossless and durable — separate from the lossy
|
|
234
|
+
// Layer-1 window and the derived episodic summaries above.
|
|
235
|
+
const verbatimCollection = process.env.QDRANT_VERBATIM_COLLECTION ?? "verbatim_archive";
|
|
236
|
+
const verbatimVectorSize = Number(process.env.QDRANT_VERBATIM_VECTOR_SIZE ?? "768");
|
|
237
|
+
await ensureVerbatimCollection(qdrant, verbatimCollection, verbatimVectorSize);
|
|
238
|
+
const verbatimProvider = createVerbatimArchiveProvider({ client: qdrant, collectionName: verbatimCollection, embed });
|
|
239
|
+
|
|
240
|
+
// Semantic consolidation (D-22/D-34): a separate Qdrant collection from
|
|
241
|
+
// episodic memory above — one point per extracted {topic, value} candidate,
|
|
242
|
+
// vector embedded on the topic alone (see semantic-facts-store.ts for why).
|
|
243
|
+
const semanticFactsCollection = process.env.QDRANT_SEMANTIC_FACTS_COLLECTION ?? "semantic_facts";
|
|
244
|
+
const semanticFactsVectorSize = Number(process.env.QDRANT_SEMANTIC_FACTS_VECTOR_SIZE ?? "768");
|
|
245
|
+
await ensureSemanticFactsCollection(qdrant, semanticFactsCollection, semanticFactsVectorSize);
|
|
246
|
+
const extractFacts = createSemanticFactExtractor(model);
|
|
247
|
+
|
|
248
|
+
// Idempotent self-heal: the vault lives on a named Docker volume, empty on
|
|
249
|
+
// first boot and not pre-populatable at build time — re-running this every
|
|
250
|
+
// startup is cheap and means a wiped/fresh volume never needs a separate
|
|
251
|
+
// manual provisioning step.
|
|
252
|
+
const wikiVaultPath = requireEnv("WIKI_VAULT_PATH");
|
|
253
|
+
await initVault(wikiVaultPath);
|
|
254
|
+
|
|
255
|
+
// Shared by the idle sweep (final capture + close) and by captureIncrement
|
|
256
|
+
// (the two mid-conversation triggers, neither of which closes the session) —
|
|
257
|
+
// one definition of "how to capture", reused everywhere.
|
|
258
|
+
const captureDeps: CaptureDeps = {
|
|
259
|
+
summarize: episodicSummarize,
|
|
260
|
+
store: (entry) => storeEpisodicSummary(qdrant, episodicCollection, embed, entry),
|
|
261
|
+
extractFacts,
|
|
262
|
+
storeFact: (entry) => storeSemanticFact(qdrant, semanticFactsCollection, embed, entry),
|
|
263
|
+
consolidateFact: (userId, topic) =>
|
|
264
|
+
consolidateSemanticFact(userId, topic, {
|
|
265
|
+
vaultPath: wikiVaultPath,
|
|
266
|
+
clusterFn: (u, t, limit) => searchSemanticFactsByTopic(qdrant, semanticFactsCollection, embed, { userId: u, topic: t, limit }),
|
|
267
|
+
readWikiFileFn: readWikiFile,
|
|
268
|
+
writeInferredNoteFn: writeInferredNote,
|
|
269
|
+
}),
|
|
270
|
+
log: (msg) => console.error(`[cron] ${msg}`),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// How many new messages (since the last capture) a live tracked session needs
|
|
274
|
+
// before captureIncrement mirrors them to Qdrant, instead of only ever
|
|
275
|
+
// capturing on idle-timeout.
|
|
276
|
+
const MESSAGE_COUNT_CAPTURE_THRESHOLD = Number(process.env.SESSION_CAPTURE_MESSAGE_THRESHOLD ?? "6");
|
|
277
|
+
|
|
278
|
+
// How much of the pending messages' actual text shows up in a capture status
|
|
279
|
+
// card's detail — enough to recognize which exchange is being saved.
|
|
280
|
+
const MESSAGE_PREVIEW_CHARS = 200;
|
|
281
|
+
|
|
282
|
+
/** Joins `messages`' content and head-truncates to `maxChars`, "…"-suffixed when cut. */
|
|
283
|
+
function previewMessages(messages: Message[], maxChars: number): string {
|
|
284
|
+
const joined = messages.map((m) => m.content).join(" ");
|
|
285
|
+
return joined.length <= maxChars ? joined : `${joined.slice(0, maxChars)}…`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Captures whatever's new in `messages` since the last capture for
|
|
290
|
+
* `sessionKey` — a no-op if nothing new. Shared by both mid-conversation
|
|
291
|
+
* triggers (message-count threshold, Layer 1 compression); the idle-timeout
|
|
292
|
+
* trigger uses `captureSessionToMemory` directly since it also closes the
|
|
293
|
+
* session. Drives the turn's tool-status callbacks like a real tool call so a
|
|
294
|
+
* live conversation shows this happening.
|
|
295
|
+
*/
|
|
296
|
+
async function captureIncrement(sessionKey: string, messages: Message[]): Promise<void> {
|
|
297
|
+
const userId = sessionUsers.get(sessionKey);
|
|
298
|
+
if (!userId) return;
|
|
299
|
+
|
|
300
|
+
const alreadyCaptured = sessionCaptureMarkers.get(sessionKey) ?? 0;
|
|
301
|
+
const pending = messages.slice(alreadyCaptured);
|
|
302
|
+
if (pending.length === 0) return;
|
|
303
|
+
|
|
304
|
+
const callbacks = sessionOnCaptureCallbacks.get(sessionKey);
|
|
305
|
+
const captureId = crypto.randomUUID();
|
|
306
|
+
callbacks?.onToolStart(
|
|
307
|
+
"Mi sto segnando un'informazione importante…",
|
|
308
|
+
`Conversazione recente (${pending.length} messaggi: "${previewMessages(pending, MESSAGE_PREVIEW_CHARS)}"), collection "${episodicCollection}"`,
|
|
309
|
+
captureId,
|
|
310
|
+
);
|
|
311
|
+
try {
|
|
312
|
+
await captureSessionToMemory(userId, sessionKey, pending, Date.now(), captureDeps);
|
|
313
|
+
sessionCaptureMarkers.set(sessionKey, messages.length);
|
|
314
|
+
callbacks?.onToolFinish?.(captureId, "success");
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error(`[capture] failed for ${sessionKey}, will retry next trigger: ${String(err)}`);
|
|
317
|
+
callbacks?.onToolFinish?.(captureId, "failed");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Procedural corrections (punto 2/Fase D): per-turn, not per-idle-session —
|
|
322
|
+
// the tool-call trace only exists in memory for the duration of its turn.
|
|
323
|
+
const toolCorrectionsCollection = process.env.QDRANT_TOOL_CORRECTIONS_COLLECTION ?? "tool_corrections";
|
|
324
|
+
const toolCorrectionsVectorSize = Number(process.env.QDRANT_TOOL_CORRECTIONS_VECTOR_SIZE ?? "768");
|
|
325
|
+
await ensureToolCorrectionsCollection(qdrant, toolCorrectionsCollection, toolCorrectionsVectorSize);
|
|
326
|
+
const extractToolCorrections = createToolCorrectionExtractor(model, undefined, {
|
|
327
|
+
log: (msg) => console.error(`[cron] ${msg}`),
|
|
328
|
+
});
|
|
329
|
+
const toolCorrectionConsolidationDeps: ToolCorrectionConsolidationDeps = {
|
|
330
|
+
vaultPath: wikiVaultPath,
|
|
331
|
+
clusterFn: (tool, topic, limit) =>
|
|
332
|
+
searchToolCorrectionsByTopic(qdrant, toolCorrectionsCollection, embed, { tool, topic, limit }),
|
|
333
|
+
readNoteFn: (vp, relativePath) => readWikiFileInRoots(vp, [resolvePath(vp, "curated")], relativePath),
|
|
334
|
+
writeNoteFn: writeToolCorrectionNote,
|
|
335
|
+
// A single confirmed correction is already a strong signal — unlike
|
|
336
|
+
// identity/preference facts (DEFAULT_CONSOLIDATION_K = 3). k: 1 fires
|
|
337
|
+
// defaultConfidenceForCount's dominantCount >= k branch on the first
|
|
338
|
+
// candidate ("high" confidence immediately).
|
|
339
|
+
k: 1,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Extracts and consolidates any procedural corrections found in one turn's
|
|
344
|
+
* `steps` — a no-op if none are found. Drives `onToolStart`/`onToolFinish`
|
|
345
|
+
* once per correction like a real tool call.
|
|
346
|
+
*/
|
|
347
|
+
async function processToolCorrections(
|
|
348
|
+
steps: StepInfo[],
|
|
349
|
+
onToolStart?: TurnSink["onToolStart"],
|
|
350
|
+
onToolFinish?: TurnSink["onToolFinish"],
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
const corrections = await extractToolCorrections(steps);
|
|
353
|
+
for (const correction of corrections) {
|
|
354
|
+
const correctionId = crypto.randomUUID();
|
|
355
|
+
onToolStart?.(
|
|
356
|
+
"Mi sto segnando un'informazione importante…",
|
|
357
|
+
`Correzione per lo strumento "${correction.tool}" (argomento: "${correction.topic}", collection: "${toolCorrectionsCollection}")`,
|
|
358
|
+
correctionId,
|
|
359
|
+
);
|
|
360
|
+
try {
|
|
361
|
+
const timestamp = new Date().toISOString();
|
|
362
|
+
await storeToolCorrection(qdrant, toolCorrectionsCollection, embed, { ...correction, timestamp });
|
|
363
|
+
await consolidateToolCorrection(correction.tool, correction.topic, toolCorrectionConsolidationDeps);
|
|
364
|
+
onToolFinish?.(correctionId, "success");
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(`[capture] procedural correction failed for ${correction.tool}/${correction.topic}: ${String(err)}`);
|
|
367
|
+
onToolFinish?.(correctionId, "failed");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// `wikiUserId` is separate from `sessionKey`: inferred/users/<userId> notes
|
|
373
|
+
// are scoped per-person, not per-(space,person) pair, so it must not include
|
|
374
|
+
// the space. An identity-less channel (the terminal) uses a fixed id.
|
|
375
|
+
function buildTools(
|
|
376
|
+
sessionKey: string,
|
|
377
|
+
wikiUserId: string,
|
|
378
|
+
onToolStart?: TurnSink["onToolStart"],
|
|
379
|
+
onToolFinish?: TurnSink["onToolFinish"],
|
|
380
|
+
): Record<string, Tool> {
|
|
381
|
+
const sessionTools: Record<string, Tool> = {};
|
|
382
|
+
|
|
383
|
+
// The session-scoped capabilities every CLI tool needs, bound to this turn:
|
|
384
|
+
// staging a confirm-required action (token + pending note) and stashing a
|
|
385
|
+
// display artifact the model can `present`.
|
|
386
|
+
const sessionToolContext = {
|
|
387
|
+
sessionKey,
|
|
388
|
+
stageConfirmation: createStageConfirmation({
|
|
389
|
+
store: confirmationStore,
|
|
390
|
+
sessionKey,
|
|
391
|
+
userId: wikiUserId,
|
|
392
|
+
vaultPath: wikiVaultPath,
|
|
393
|
+
writeConfirmationNoteFn: writeConfirmationNote,
|
|
394
|
+
}),
|
|
395
|
+
stashDisplay: (artifact: string) => displayStore.stash(sessionKey, artifact),
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Each CLI-based plugin owns its tool (jiraCommand, …), built from its own
|
|
399
|
+
// allowlist and post-processor. The core just invokes what they contributed.
|
|
400
|
+
for (const bundle of loadedPlugins.sessionToolBundles) {
|
|
401
|
+
Object.assign(sessionTools, bundle.build(sessionToolContext, bundle.postProcess));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// `present` only makes sense alongside CLI tools: they are what produce the
|
|
405
|
+
// display artifacts it surfaces. An instance with no CLI tool never sees it.
|
|
406
|
+
const hasCliTool = loadedPlugins.sessionToolBundles.length > 0;
|
|
407
|
+
if (hasCliTool) {
|
|
408
|
+
Object.assign(sessionTools, createPresentTool({ sessionKey, store: displayStore }));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
Object.assign(sessionTools, createWikiTools({ vaultPath: wikiVaultPath, userId: wikiUserId }));
|
|
412
|
+
Object.assign(sessionTools, createToolLogRecallTool({ sessionKey }));
|
|
413
|
+
// Verbatim archive recall, scoped to this person — lets the model resurface
|
|
414
|
+
// what was actually said in earlier conversations, beyond the live window.
|
|
415
|
+
Object.assign(sessionTools, verbatimProvider.sessionTools!({ sessionKey, userId: wikiUserId }));
|
|
416
|
+
// read_skill only exists when a plugin contributed at least one skill.
|
|
417
|
+
if (loadedPlugins.skills.length > 0) {
|
|
418
|
+
Object.assign(sessionTools, createReadSkillTool(loadedPlugins.skills));
|
|
419
|
+
}
|
|
420
|
+
return onToolStart ? withToolStartHook(sessionTools, onToolStart, toolStatusDescribers, onToolFinish) : sessionTools;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const confirmationStore = createConfirmationStore();
|
|
424
|
+
// Shared across turns (like confirmationStore): a display artifact stashed
|
|
425
|
+
// while answering can be surfaced by `present` in a later turn, so it's
|
|
426
|
+
// session-scoped, not rebuilt per turn.
|
|
427
|
+
const displayStore = createDisplayStore();
|
|
428
|
+
|
|
429
|
+
// Raw tool output can be tens of KB — too long to print in full and stay
|
|
430
|
+
// readable. MAX_INLINE_CHARS bounds what's shown per call/result.
|
|
431
|
+
const MAX_INLINE_CHARS = 600;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Server-side-only tool-call/result visibility for debugging a turn —
|
|
435
|
+
* written to this process's own stderr, never sent back to whoever asked.
|
|
436
|
+
* `prefix` distinguishes which conversation a line belongs to.
|
|
437
|
+
*/
|
|
438
|
+
function logStep(prefix: string, step: StepInfo): void {
|
|
439
|
+
for (const call of step.toolCalls) {
|
|
440
|
+
console.error(
|
|
441
|
+
`${prefix}[tool] ${call.toolName}(${truncateForDisplay(call.input, MAX_INLINE_CHARS)})`,
|
|
442
|
+
);
|
|
443
|
+
console.error(`${prefix}${describeToolOutcome(step, call.toolCallId, MAX_INLINE_CHARS)}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Shared turn-taking pipeline (see src/router/turn-runner.ts). Parameterized
|
|
448
|
+
// entirely by Provider/InboundTurn/TurnSink — it doesn't know which provider
|
|
449
|
+
// a given turn came from. getOrCreateHistory seeds a context primer only for
|
|
450
|
+
// a genuinely new, tracked (real per-user identity) session; an identity-less
|
|
451
|
+
// turn (userId undefined) never triggers it.
|
|
452
|
+
const handleTurn = createTurnRunner({
|
|
453
|
+
model,
|
|
454
|
+
systemPrompts: { singleUser: system, multiUser: chatSystem },
|
|
455
|
+
buildTools,
|
|
456
|
+
// Post-turn guards contributed by whatever plugins loaded — the core runs
|
|
457
|
+
// them without knowing what any does (see PostTurnGuard).
|
|
458
|
+
postTurnGuards: loadedPlugins.postTurnGuards,
|
|
459
|
+
getOrCreateHistory: async (key, trackForCapture, userId) => {
|
|
460
|
+
if (trackForCapture && userId && !histories.has(key)) {
|
|
461
|
+
const primer = await buildContextPrimer(userId, {
|
|
462
|
+
vaultPath: wikiVaultPath,
|
|
463
|
+
getLastSessionEntries: (uid) => getLastSessionEpisodicSummaries(qdrant, episodicCollection, { userId: uid }),
|
|
464
|
+
listWikiFilesInRootsFn: listWikiFilesInRoots,
|
|
465
|
+
readWikiFileInRootsFn: readWikiFileInRoots,
|
|
466
|
+
readIndexFileFn: readIndexFile,
|
|
467
|
+
});
|
|
468
|
+
return getOrCreateHistory(key, trackForCapture, primer);
|
|
469
|
+
}
|
|
470
|
+
return getOrCreateHistory(key, trackForCapture);
|
|
471
|
+
},
|
|
472
|
+
trackSession: (key, userId, at) => {
|
|
473
|
+
sessionUsers.set(key, userId);
|
|
474
|
+
idleScanner.touch(key, at);
|
|
475
|
+
},
|
|
476
|
+
registerCaptureCallback: (key, onToolStart, onToolFinish) => sessionOnCaptureCallbacks.set(key, { onToolStart, onToolFinish }),
|
|
477
|
+
maybeCapture: async (key, history) => {
|
|
478
|
+
const messages = history.getMessages();
|
|
479
|
+
const alreadyCaptured = sessionCaptureMarkers.get(key) ?? 0;
|
|
480
|
+
if (messages.length - alreadyCaptured >= MESSAGE_COUNT_CAPTURE_THRESHOLD) {
|
|
481
|
+
await captureIncrement(key, messages);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
captureVerbatim: verbatimProvider.captureExchange,
|
|
485
|
+
processToolCorrections,
|
|
486
|
+
logStep,
|
|
487
|
+
// Appends what the model surfaced via `present` this turn — nothing if it
|
|
488
|
+
// presented nothing.
|
|
489
|
+
takeSurfacedDisplays: (sessionKey) => displayStore.takeSurfaced(sessionKey),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// The confirm capability, bound to this instance's store/vault/note-writer.
|
|
493
|
+
const confirmDeps: ConfirmDeps = { store: confirmationStore, vaultPath: wikiVaultPath, writeConfirmationNoteFn: writeConfirmationNote };
|
|
494
|
+
|
|
495
|
+
// The runtime context each channel's build() gets. The floor (confirm) plus
|
|
496
|
+
// HTTP's optional in-process capabilities (resolveConfirmation + reads), which
|
|
497
|
+
// can't come from env; a channel that doesn't need them ignores them.
|
|
498
|
+
const channelRuntime: ChannelRuntimeContext = {
|
|
499
|
+
env: process.env,
|
|
500
|
+
log: (msg) => console.error(msg),
|
|
501
|
+
confirm: (token, sessionKey, userId) => tryConfirm(token, sessionKey, { ...confirmDeps, userId }),
|
|
502
|
+
resolveConfirmation: (token, sessionKey, userId) => resolveConfirmation(token, sessionKey, { ...confirmDeps, userId }),
|
|
503
|
+
reads: {
|
|
504
|
+
manifest: () => buildPluginManifest(plugins, loadedPlugins.activated, [], loadedPlugins.skills),
|
|
505
|
+
pendingConfirmations: () => confirmationStore.pending(),
|
|
506
|
+
// Durable per-conversation transcript from the verbatim archive (#4):
|
|
507
|
+
// the client's conversationId is the sessionKey.
|
|
508
|
+
conversation: (sessionKey, limit, offset) =>
|
|
509
|
+
listVerbatimBySession(qdrant, verbatimCollection, { sessionKey, limit, offset }),
|
|
510
|
+
conversations: (limit) => listVerbatimSessions(qdrant, verbatimCollection, { limit }),
|
|
511
|
+
wikiList: () => listWikiVault(wikiVaultPath),
|
|
512
|
+
wikiRead: (path) => readWikiVaultFile(wikiVaultPath, path),
|
|
513
|
+
wikiGrep: (pattern) => grepWikiVault(wikiVaultPath, pattern),
|
|
514
|
+
memoryScroll: (collection, limit, offset) => scrollCollection(qdrant, collection, { limit, offset }),
|
|
515
|
+
toolLog: () => getToolLog(),
|
|
516
|
+
health: () => getSelfHealth({ qdrant, ollamaHost }),
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/** Starts the Layer-3 idle-capture and self-review crons; returns one stopper. */
|
|
521
|
+
function startCrons(): Stoppable {
|
|
522
|
+
const idleCron = startIdleSessionCron(
|
|
523
|
+
idleScanner,
|
|
524
|
+
{
|
|
525
|
+
getSession: (key) => {
|
|
526
|
+
const history = histories.get(key);
|
|
527
|
+
const userId = sessionUsers.get(key);
|
|
528
|
+
if (!history || !userId) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
return { key, userId, messages: history.getMessages() };
|
|
532
|
+
},
|
|
533
|
+
closeSession: (key) => {
|
|
534
|
+
histories.delete(key);
|
|
535
|
+
sessionUsers.delete(key);
|
|
536
|
+
sessionCaptureMarkers.delete(key);
|
|
537
|
+
sessionOnCaptureCallbacks.delete(key);
|
|
538
|
+
},
|
|
539
|
+
...captureDeps,
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
idleTimeoutMs: Number(process.env.SESSION_IDLE_TIMEOUT_MS ?? String(30 * 60_000)),
|
|
543
|
+
checkIntervalMs: Number(process.env.SESSION_IDLE_CHECK_INTERVAL_MS ?? String(60_000)),
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
const selfReviewCron = startSelfReviewCron({
|
|
547
|
+
listRawEntries: () => listWikiFilesInRoots(wikiVaultPath, [resolvePath(wikiVaultPath, "raw")]),
|
|
548
|
+
findOrphans: () => findOrphanCuratedDocs(wikiVaultPath),
|
|
549
|
+
runRawTriage: (rawEntries) => runRawTriagePass({ vaultPath: wikiVaultPath, model, rawEntries }),
|
|
550
|
+
runIndexAndOrphan: (orphans) => runIndexAndOrphanPass({ vaultPath: wikiVaultPath, model, orphans }),
|
|
551
|
+
runContradictionCheck: () => runContradictionCheckPass({ vaultPath: wikiVaultPath, model }),
|
|
552
|
+
log: (msg) => console.error(`[cron] ${msg}`),
|
|
553
|
+
});
|
|
554
|
+
return {
|
|
555
|
+
stop: () => {
|
|
556
|
+
idleCron.stop();
|
|
557
|
+
selfReviewCron.stop();
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Starts the POC admin panel if `ADMIN_PANEL_ENABLED` — opt-in, dev-only,
|
|
564
|
+
* unauthenticated. Reuses the already-constructed qdrant client, model and
|
|
565
|
+
* vault path. Returns it (to stop on shutdown), or undefined when disabled.
|
|
566
|
+
*/
|
|
567
|
+
function startAdmin(): Stoppable | undefined {
|
|
568
|
+
if (process.env.ADMIN_PANEL_ENABLED !== "true") return undefined;
|
|
569
|
+
const adminPort = Number(process.env.ADMIN_PANEL_PORT ?? "4000");
|
|
570
|
+
const adminServer = startAdminServer({
|
|
571
|
+
port: adminPort,
|
|
572
|
+
vaultPath: wikiVaultPath,
|
|
573
|
+
model,
|
|
574
|
+
qdrant,
|
|
575
|
+
qdrantCollections: { episodic: episodicCollection, semanticFacts: semanticFactsCollection },
|
|
576
|
+
// Every CLI is a plugin now (no file-based bucket), so there are no
|
|
577
|
+
// centrally-configured CLIs for the POC admin's CLI status to cover.
|
|
578
|
+
activeCliConfigs: {},
|
|
579
|
+
runCliFn: runCli,
|
|
580
|
+
ollamaHost,
|
|
581
|
+
ollamaModel,
|
|
582
|
+
systemPrompts: { terminal: system, googleChat: chatSystem },
|
|
583
|
+
envFilePath: ".env",
|
|
584
|
+
});
|
|
585
|
+
console.error(`[admin] panel listening on http://localhost:${adminPort}`);
|
|
586
|
+
return adminServer;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
handleTurn,
|
|
591
|
+
channels: config.channels ?? [],
|
|
592
|
+
channelRuntime,
|
|
593
|
+
confirmDeps,
|
|
594
|
+
ollamaHost,
|
|
595
|
+
ollamaModel,
|
|
596
|
+
startCrons,
|
|
597
|
+
startAdmin,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
|
|
18
|
+
/** The shape of a Mercury instance's composition config: the tool plugins
|
|
19
|
+
* (gated by MERCURY_CLIS) and the channel plugins (enabled by being declared
|
|
20
|
+
* here — declared = active), each loaded by its own loader, plus the
|
|
21
|
+
* assistant's persona (its identity and tone; the defaults when left out). */
|
|
22
|
+
export type MercuryConfig = {
|
|
23
|
+
plugins: Plugin[];
|
|
24
|
+
channels?: ChannelPlugin[];
|
|
25
|
+
persona?: Persona;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Identity + typing helper for `mercury.config.ts`. Returns its argument
|
|
30
|
+
* unchanged; its only job is to type the config literal against `MercuryConfig`
|
|
31
|
+
* at the call site, exactly like `defineConfig` in the Vite/Nuxt ecosystem.
|
|
32
|
+
*/
|
|
33
|
+
export function defineMercuryConfig(config: MercuryConfig): MercuryConfig {
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
File without changes
|