Mercury Framework
import { QdrantClient } from "@qdrant/js-client-rest"; import { getOllamaProvider } from "./model/client.ts"; import { createConfirmationStore, createStageChoices, createStageConfirmation, type ConfirmationStore, type ResumedAction } from "@mercury-fw/confirm-engine"; import { createDisplayStore } from "./tools/display-store.ts"; import { createPresentTool } from "./tools/present-tool.ts"; import { loadPlugins } from "./plugins/plugin-loader.ts"; import { materializeCliCreden
tials } from "./credentials/materialize.ts"; import type { MercuryConfig } from "./config/define-config.ts"; import { createResumeSlots } from "./session/resume-slots.ts"; import { createSessionHistory, type SessionHistory, type Message, type ResumedConversation } from "./session/history.ts"; import { createSummarizer } from "./session/summarizer.ts"; import { createEpisodicSummarizer } from "./session/episodic-summarizer.ts"; import { createSemanticFactExtractor } from "./se
ssion/semantic-fact-extractor.ts"; import { buildContextPrimer, primerOptions, type ContextPrimer } from "./session/context-primer.ts"; import { resumeConversation, resumeBudget,·parseResumeMaxChars } from "./session/resume.ts"; import { buildSystemPrompts } from "./session/system-prompt.ts"; import { assistantNameOf, skillsWithAssistantName, toolsWithAssistantName } from "./session/assistant-name.ts"; import { createSessionLock } from "./router/session-lock.ts"; import { cre
ateSessionCapture } from "./session/session-capture.ts"; import { createTurnRunner, parseStallTimeoutMs } from "./router/turn-runner.ts"; import { loadAuth } from "./router/auth-loader.ts";·import { loadDirectory } from "./router/directory-loader.ts"; import type { TurnSink } from "./router/provider.ts"; import type { HandleTurn, ChannelRuntimeContext, ChannelPlugin } from "@mercury-fw/channel-types"; import { truncateForDisplay, describeToolOutcome, } from "./router/tool-log
.ts"; import type { StepInfo } from "./session/step-info.ts"; import { withToolStartHook } from "./session/tool-start-hook.ts"; import { writeInferredNote, writeToolCorrectionNote,·writeConfirmationNote, } from "./wiki/wiki-note.ts"; import { createWikiTools } from "./wiki/wiki-tools.ts"; import { createToolLogRecallTool } from "./session/tool-log-recall-tool.ts"; import { createReadSkillTool } from "./session/read-skill-tool.ts"; import { createIdleSessionScanner } from "./c
ron/idle-session-scanner.ts"; import { startIdleSessionCron, captureSessionToMemory, type CaptureDeps } from "./cron/idle-session-cron.ts"; import { ensureEpisodicCollection,·storeEpisodicSummary,·getLastSessionEpisodicSummaries, getEpisodicSummariesBySession, }·from "./memory/episodic-store.ts"; import·{ ensureVerbatimCollection, listVerbatimTail } from "./memory/verbatim-archive-store.ts"; import { createVerbatimArchiveProvider } from "./memory/memory-provider.ts"; import {
ensureSemanticFactsCollection, storeSemanticFact, searchSemanticFactsByTopic } from "./memory/semantic-facts-store.ts"; import { ensureToolCorrectionsCollection, storeToolCorrection,·searchToolCorrectionsByTopic }·from·"./memory/tool-corrections-store.ts"; import·{·setUpWhenReachable·} from·"./memory/collection-setup.ts"; import { consolidateSemanticFact, consolidateToolCorrection, type ToolCorrectionConsolidationDeps } from "./cron/semantic-consolidation.ts"; import { creat
eToolCorrectionExtractor } from "./session/tool-correction-extractor.ts"; import { createEmbedder } from "./memory/embedder.ts"; import { initVault } from "./wiki/vault-init.ts"; import {·findOrphanCuratedDocs } from "./wiki/orphan-detector.ts"; import·{·listWikiFilesInRoots,·readWikiFileInRoots, readIndexFile } from "./wiki/wiki-read.ts"; import { createHostReads } from "./identity/host-reads.ts"; import { createChannelSettings } from "./identity/channel-settings.ts"; import
{ resolveTimeZone, stripTurnStamp } from "./session/turn-stamp.ts"; import { bindConfirm } from "./identity/confirm-binding.ts"; import { createPersonLogins } from "./identity/person-logins.ts";·import·{·createProfileStore, profilesPath } from "./identity/profiles.ts"; import·{·createTexts,·instanceI18n, requestTexts, type Texts, type Translate } from "./i18n/i18n.ts"; import { channelMessages } from "./router/channel-loader.ts"; import { createProfileTools } from "./identit
y/profile-tools.ts"; import { createPeople, type TurnWho } from "./identity/people.ts"; import { createLinkStore, linksPath } from "./identity/links.ts"; import { createLinking·} from "./identity/linking.ts";·import {·offeringFor, type·Offering } from "./plugins/offering.ts";·import { AS_SELF_SUFFIX·}·from "./plugins/act-as-self.ts"; import { buildOfferedTools } from "./plugins/offered-tools.ts"; import { migrateMemoryToUserKeys, migrateVaultToUserAreas } from "./identity/mig
rate-layout.ts"; import { runRawTriagePass, runIndexAndOrphanPass, runContradictionCheckPass } from "./wiki/self-review-runner.ts"; import { startSelfReviewCron } from "./cron/self-review-cron.ts";·import {·resolve as·resolvePath } from·"node:path"; import { homedir·} from "node:os"; import type·{·Tool } from "ai"; import { getSelfHealth } from "./health.ts"; import { frameworkVersion } from "./version.ts"; import { buildPluginManifest } from "./plugins/manifest.ts"; type Sto
ppable = { stop: () => void }; export type ConfirmDeps = { store: ConfirmationStore; vaultPath: string; writeConfirmationNoteFn: typeof writeConfirmationNote; onResume: (sessionKey: string,·action:·ResumedAction) =>·void;·};·export type·ComposedApp = {·handleTurn:·HandleTurn;·channels: ChannelPlugin[]; channelRuntime: ChannelRuntimeContext; confirmDeps: ConfirmDeps; texts: Texts; ollamaHost: string; ollamaModel: string; startCrons: () => Stoppable; }; function requireEnv(name
: string): string { const value = process.env[name]; if (!value) { throw new Error(`${name} is not set`); } return value; } export async function composeMercury(config: MercuryConfig): Promise<ComposedApp>·{·// What·every·text·calls the·assistant: checked·first,·so·a persona·name·that // can't stand in a prompt stops the app before anything starts. const assistantName = assistantNameOf(config.persona); // The model is constructed up front, before the plugins load and the syst
em // prompt is built: a plugin's post-turn guard can be model-backed (Jira's // issue-list corrector is), and the system prompt is assembled from the // fragments the plugin loader returns — both need the model in hand first. const provider = getOllamaProvider(); const ollamaHost = requireEnv("OLLAMA_HOST"); // already validated by getOllamaProvider(); read again here for the terminal provider's getLoadedContextLength call const ollamaModel = requireEnv("OLLAMA_MODEL"); // t
hink: true enables Ollama's native extended-thinking tokens — see // agent-turn.ts's reasoning-delta handling. OLLAMA_THINK lets the deployer // turn it off for a model that doesn't support it (Ollama rejects the request // outright otherwise, 400 "does not support thinking"), same reasoning·as·//·never guessing·OLLAMA_HOST. Model-construction-time setting only, set once // here for the one shared model instance used by every channel. const ollamaThink = process.env.OLLAMA_TH
INK !== "false"; const model = provider(ollamaModel, { think: ollamaThink }); const summarize = createSummarizer(model); // Plugins are declared in the instance's·config·(its `mercury.config.ts`) and // handed in — this file no longer names them. The loader processes an opaque // list (see·plugins/plugin-loader.ts): each supplies its allowlist as data, a // system-prompt fragment, and a `build()` that turns the runtime context into // post-processors and post-turn guards. Eve
ry declared plugin loads; one // that fails (an invalid allowlist, missing configuration) degrades only itself. const plugins = config.plugins; // A plugin's·CLI·that·keeps its login in a folder finds it in place before // the plugin loads: on the credentials volume, linked when it's kept elsewhere. await materializeCliCredentials({ appDir: process.cwd(), homeDir: homedir(), log: (msg) => console.error(msg), }); const loadedPlugins = await loadPlugins(plugins, { model, assist
antName, env: process.env, log: (msg) => console.error(msg), pluginDataRoot: resolvePath(requireEnv("WIKI_VAULT_PATH"), ".mercury", "plugins"), }); // Status·labels for the tool-start hook, keyed by tool name: each plugin's // describer for its own tool (jiraCommand, …), the same for its variant // acting as·Mercury. const toolStatusDescribers: Record<string, (input: unknown, t: Translate) => string> = { ...loadedPlugins.toolStatusDescribers }; for (const [name, describe] of
Object.entries(loadedPlugins.toolStatusDescribers)) { toolStatusDescribers[`${name}${AS_SELF_SUFFIX}`] = describe; } // Who a principal is to Mercury: an account linked to another·is·its·//·owner,·then·the·declared·directory·decides, closed·to·anyone·it·doesn't // know unless·the·config opens·it.·The·links live·on the·vault's volume, // where no model tool or HTTP read reaches. const accountLinks = createLinkStore({ path: linksPath(requireEnv("WIKI_VAULT_PATH")) }); // How ea
ch person wants to be called and the language they want, kept next to the links. const profiles = createProfileStore({ path: profilesPath(requireEnv("WIKI_VAULT_PATH")) }); //·The·texts·a·person sees, in·their language·(see·i18n/i18n.ts):·the·//·core's·and the·loaded·plugins'·English, the instance's·language·files over·it. A default // language the instance doesn't ship stops the start; a file's gaps are // logged, and show as their keys. // The declared channels' texts too,
those of the channels the loader will load. const i18n = instanceI18n({ ...loadedPlugins.messages, ...channelMessages(config.channels ?? []) }, config.i18n);·for·(const·line of i18n.report()) console.error(line);·const texts =·createTexts(i18n,·(key)·=>·profiles.languageOf(key)); const people =·createPeople({·links:·accountLinks, directory: loadDirectory(config.directory, { env: process.env, log: (msg) => console.error(msg) }), ...(config.access?.unknown === undefined ? {} :
{ unknown: config.access.unknown }), texts, log: (msg) => console.error(msg), }); // Linking a second account to a person, by a code (see identity/linking.ts).·const linking = createLinking({ people,·links:·accountLinks,·assistantName,·texts·});·// What·a person is·offered·of the loaded plugins·(see plugins/offering.ts). const offeringOf = (who: TurnWho): Offering => offeringFor(loadedPlugins.plugins, { roles: who.person.roles, operator: who.operator }, config.access?.plugins
); // The two system prompts (1:1 and shared-space) for an offering, built from // the descriptions, fragments and skills of the plugins it holds and the instance's // persona, once·per distinct offering.·const promptsByOffering·= new·Map<string,·{ singleUser:·string; multiUser: string·}>(); function systemPromptsFor(who: TurnWho): { singleUser: string; multiUser: string } { const offering = offeringOf(who); let prompts = promptsByOffering.get(offering.signature); if (!prompt
s) { const built = buildSystemPrompts({ pluginFragments: offering.promptFragments, plugins: offering.plugins, skills: offering.skills, persona: config.persona,·}); prompts = { singleUser: built.system, multiUser:·built.chatSystem };·promptsByOffering.set(offering.signature,·prompts);·} return prompts; } // People's pending logins to the plugins' services, finished by the channel // that receives the provider's redirect. const personLogins = createPersonLogins({ log: (msg) =>
console.error(msg), assistantName, texts }); const histories = new Map<string, SessionHistory>(); function getOrCreateHistory(key: string, trackForCapture =·false, primer?: ContextPrimer,·resumed?:·ResumedConversation):·SessionHistory·{·let history = histories.get(key); if·(!history)·{ history·=·createSessionHistory(·summarize, trackForCapture ? (messages) => sessionCapture.onCompress(key, messages) : undefined, primer, resumed, ); histories.set(key, history); } return histor
y; } // Session persistence, Layer 3: a tracked session idle past // SESSION_IDLE_TIMEOUT_MS is summarized and written to Qdrant as a dated // episodic record,·then discarded.·Identity-less sessions·(the terminal)·are //·never·tracked here·—·per-user isolation needs·a real·sender·identity.·const·idleScanner =·createIdleSessionScanner(); const episodicSummarize = createEpisodicSummarizer(model); const embeddingModel = provider.textEmbeddingModel(process.env.OLLAMA_EMBEDDING_MO
DEL ?? "nomic-embed-text"); const embed = createEmbedder(embeddingModel); const qdrant = new QdrantClient({ url: process.env.QDRANT_URL ?? "http://qdrant:6333" }); const episodicCollection = process.env.QDRANT_EPISODIC_COLLECTION ?? "episodic_memory"; const episodicVectorSize = Number(process.env.QDRANT_EPISODIC_VECTOR_SIZE ?? "768"); // Verbatim conversation archive (#4): a distinct collection holding the raw // user<->model exchange, lossless and durable — separate from the
lossy // Layer-1 window and the derived episodic summaries above. const verbatimCollection = process.env.QDRANT_VERBATIM_COLLECTION ?? "verbatim_archive"; const verbatimVectorSize = Number(process.env.QDRANT_VERBATIM_VECTOR_SIZE ?? "768"); const verbatimProvider = createVerbatimArchiveProvider({ client: qdrant, collectionName: verbatimCollection, embed }); // How much of a resumed conversation comes back, when its channel sets no // budget of its own (see session/resume.ts).
const resumeMaxChars = parseResumeMaxChars(process.env.SESSION_RESUME_MAX_CHARS); // Semantic consolidation (D-22/D-34): a separate Qdrant collection from // episodic memory above — one point per extracted {topic, value} candidate, // vector embedded on the topic alone (see semantic-facts-store.ts for why). const semanticFactsCollection = process.env.QDRANT_SEMANTIC_FACTS_COLLECTION ?? "semantic_facts"; const semanticFactsVectorSize = Number(process.env.QDRANT_SEMANTIC_FACTS
_VECTOR_SIZE ? "768"); onst extractFacts = cre teSem nticFactExtractor(model); // Idempotent self-heal: the v ult lives on a named Docker volume, empty on // first boot and not pre-populatable t build time — re-running this every // tartup is che p and means a wiped/fresh volume never needs a separate // manual provisi ning step. const wikiVaultPath = equireEnv("WIKI_VAULT_PATH"); await initVault(wikiVaultPath); // A vault from before per-person are s ge s its notes move
d into them. await igrateVaultT U erAreas(wikiVaultPath, (msg) => console.e ror(`[wiki-v ult] ${msg}`) .c tch((err: unknown) => console.error(`[wi i-vault] layout migration failed, Mercury s ar s anyway: ${String(err) `), ); // hared by the idl sweep (fin l capture + clo ) and by sessionCapture // (the two mid-co ver ation trigger , neith r of whi closes the sess on) — ne d fi ition of "how to capture", reused everywhere. const captureDeps: CaptureDeps = { summarize
: episodicSummarize, store: (ent y) > t reEpi di Summa y(qdran , episodicCollection, embed, entry , xtra t acts, s oreFact: ( ntr ) => storeSemanticFact(q rant, sem nticFactsCo lection, embed, entry), cons l dateFact: ( serId opic => c nsolidate mantic act(user d, topic, { v u tPath: wikiVaul Path, cl sterFn: u, t, l mi ) => e r hSemanticFa tsByTo ic(qdrant, s m nticFactsCollection, embed, { userI : u, topic t, l mit }), wri eInfer e oteFn: writeIn erred ote, }),
log: msg) => con ole.error(`[cron] ${msg}`), }; // H w many new messages (since the last apture) a ive racke session needs // before it's irrored to Qd ant, instead of nly ever capturing on // idle t m out. const MESSAGE_COUNT_CAPTURE_ H ESHOLD Num r(process.env.SESSION_CAPTURE_ ESSA E_THRESHOLD ?? 6 ); // How uch of t e pen ing mes ages actual text shows up i apture status // card's d tail — enou h t reco nize which exchange is being sa ed const MESSAGE_PR
EVIEW_CHA S 200; function pre i Messages( essag s: essage[] maxChars: number) string { const jo ned = m ssages.map((m) => stripTurnStamp(m.content)).joi (" ); return jo ed.l n th <= m xChars ? joined : `$ ined. l ce(0, maxChars)}…`; } // Who each t ked sessi n belongs t and ow much of i is al eady in // memory; mirrors the res at the two mid c nversatio triggers. const sess onCaptur createSe ionCapture({ ca ture: (us Id sessionK y, messages) => c ptur S
e sio ToMemory us rId, sess nKey, essag , Date. ow( , aptureD ps), hr h ld: MESS G _C UN _ P URE_THRE HOLD, texts, de cr be: pending, ) = ("c re.s a us.c p ur -de ail", { co nt: pen ing.le gth, p eview: re ie M sage (pen ing E SAG _PREVIEW_ HAR ) lle tion: episodicColle io , } } ; // Procedu al cor ections ( unt 2/Fa e D : per-turn, no per- d e- e o — // the t ol-call tr ce on y xists in emory for the d ra ion f its rn. onst t olC rrect ons o
t n = proce .env.QDRAN _TOOL_CORRECT NS COL CT ON ?? tool orrection ; ons t ol orre ti nsV ctorSize = N mber(p e s.env. D AN TO _ ORR CTIO _V CTOR_S ZE ?? "768" ; // Ever Layer-3 c llection, et up in th background: Mer ry t rts ev n / while Q r n isn t ans er ng yet an memory swit hes nce it oe . co st memo Rea y tUpWh n e cha e( as n () => { aw t ensureEpi od cC llection q rant, e isodi Collection, pisodicVect S ze); ait ens reVer at
mC le tion(qdr nt, e imCollec ion, verbatimV to Size); a it ensu eSe an ic ac s lle t on(q r , se a tic c sC llect n, em n icF cts e torSi ); ai n reTo lCor ctionsCo l c ion q r nt, ool orr i nsColl c ion, toolCorr c onsV ct rSi e); }, lo (msg) = console error(`[m mory] msg `) }, ); // Memory writ e efore the ser key gets it, o e Qdrant an wers. void mem ryReady.d ne.t en( ) => mi ateMemo ToU er e s dr t, [episo i oll cti n, semanti Fa
c C lecti , verba mCollectio ], (msg => consol .err r( e ory ${m g `), ) ); con t extr ctT olC rr tions = r ateT lC r ctio E tra tor model, und ed log: (m g) = le r o (`[ ron] ${msg} ), }) const tool o r ct o ons li a ionDeps: oolCo rectionCons ida ionDeps = ultPa h wi iVau tPath, lus er n: (to l to c l mi > sea c Too Co rectio sB pic(q r tool orrec ions oll tion embed tool, t p , l mi }) writ eF : riteTo lCorre ti
Note // A i gle confir ed co rec i is alrea strong igna — unl ke // identi / refe ce fa ts (DE A LT_ O SO IDATION 3 . k: ires // efaultCo fid nceF Count' o i t ount >= k bra ch o the f rs // can idat ("high" co f de ce m i el ). 1, y c fun tion p o e ToolC rrec ions( step StepI fo ] on ool tar : TurnSink["onToo Start"] | defined, on o Fin s Turn ink[ o T ol ini h" | und f ned, k : r ng, ): P omise<v id { const cor ecti s
awa extra To lCo rectio s(s eps) o t te s forPer (ke fo ( o t o re t o of rrec on ) con t or n d = rypt do UI ( T lS a t? ( ("core. at s remember g" , t(" or . atus rection-de ail { o c r e tion.t l, opic o ct on pic, ll ctio : t Cor ecti nsCo ecti n } co rect nId ); t { onst ti es p new D t ).toISO ring( wait store oo Correc i n( rant t o C r ct ons oll ct n m d, . .correct o
n t m amp }) wa c n ol at T l or ec n(co ec .to , or e t o t i , to Cor ect onConsol ati De s ; T ol n h?. c rrec io Id, " u ce "); } catch ( ) o e.erro `[c tu e] pr ced r l c rrec ion fai d f r ${ o r i . l} { or e t o op c : ${St ng(err)}`) nTo l ini ? (corre i d, "failed ); } / ey` th pe son' ke i sep rat r ` ess o K y`: a pe n's i // ar a, emory d c irmatio s are he s ross paces co
v rs tion , it m st t inc de e s ac r runn r d n fies t e r o from t ' prin ipal. // ` ho de ide lu i are f e e nd a om their tool act: / t e p s n, or er ury its lf (s lug off r t ). ction ld o s( s ion ey str ng, k y: stri g w : TurnW o onToo t ? T rnSi "onToo Start"] n olFin h u in o T ol i h ] lang g H t : s in ) ecor < T ol> { nst ring = of i gO (w o);
h text th erso t e u n f , o he nn l' hi t. o s { = t xt . orP n k , l a t ; on t session ool ecord<str n Too > = {}; The ssio -s o ed ca abili ie e ery LI to l e s, bou o his t / sta ing conf rm-r ui d ct n ( oke end ng o e), on as / veral c oic , st shin a d a i a t th model c `p se t` c ns taging eps = ore: c nfi io o , s s io e , w r key, va l at : w ul
Pat ri eC n r a i n o Fn r t onfirm onNot o t es on o lC t t = s s on ey, s ge f m tion cr t St eC n ma i s i e ge oic : reat age h ices(stag n Deps h i p y rt fa t n , b : tring > splayS o st (s sion ey, ar i ct, la e , }; a h - a e p ugin w s its tool iraComm , … , bu l r ts // al ow st a post-pr c s or T e cor st vok s w at th y co trib e , // s the p n
o as e r pe h f ri s e p u ins/ f ere -too .ts) co o s plug n o h Cli oo } = bu dOff r To s(of erin who, c n xt: s ss nTo l nt , req reLog ser e, gin, per o Key) => p so og .req r ( rv ce, i , pe sonKey , log: sg => con l .e r r(m }); Objec .assign( e s Too pl oo s) / `p es n ` onl k s se o g de CL o l : the r ha p du e t // d ay rt s it s rf ce . tu th n I oo
e e s i . i ( a Cl ol) { bje as i ( ssionT o , e t Presen To l s io K y, st e displ y ore ) O jec sign se s n oo s, t ik To s({ au Path iVa tPa h, k a e o f a io : s To l t xt e i tion }), ) O c a si ss o To , eat Pr il s({ tore p o iles, y, an ua 18 u port d ; ct. i n s s nTools, rea lL g ecal o { sion y owner: k y } ; // e batim hi e r ca l, c d
o s on le s e f e / w as ctu l y sa i a lier o r a n , be n iv wi do j ct.a s ( si ols, e ati r.s s n o s se n , u erId ke )) // ll o ex s a l gin o f ed c n d le t . i (o fe i g k s l > O j c ss n(s s teRead k llTo l(skillsWi Assi a t me ff ing. ls, a s n N m ) ); // E ry cri ti the l lu na e
t e s a as t e d es con amed olsW h ss s an a ( s nT l , s s a Na ); re ur on lStar t o S tHoo n d o T o S , Stat sD scr bers nTo F ni a ed ns n irm t t re c t C irm o S ); / A c n ir i wh s lt g es ba k t e e , k n u t e ch nel es mes t ses io 's tur ` n o dT r .r u e f e onfirm`) c n s me reate es eS t ); / S r os
tu (l k o fi m re isp a r f t a e / hi an w a su fa by p e ` a ate ur so i 's s ss o -s ope ot rebu er u s d pla o e = r at Disp a re( ; t t t n K o g r in f a st / a . M _ LINE ARS o n ' h per ll/ es . co st M X_IN E C AR 60 ; nc i n l gS e pr fi : r g, ep t I o : d r ( on t a l of t o l s)
o ole r or( `${p ix ] to e (${t c ay c t A _I N _C RS)}) ) on e e r ef } e i o l u co e te , ca t lId, A I E S) ; } / r rn- ng el ne see u tu -ru er ts) P m ter ze e re y ovi / b n r Si — t oe ' h v d r a g v n t n r . ge O r a e s o se d con ext i n f a u y r d r- s r
de i se i ; an e ti - s / u rId f n n g r i . / t im er se ion, he i e i i on th / me o ef re it a t s nd clo es a s ion. o ess on o k = r t o L () / A tr ing rn hose m e g sil n toppe # 0 ; hec e h a d v ue s o s he p a t up t t l Tim ut r l utM p c s n .T R S L I EO T_M o h n Tu n = a e u R
un a e e d se i n > ume e( essi e , on k o el, y r p s e mp F ( o { e : " l : [ } ope a r: se } , te ro t , p fe e ame: ( e p fi . am y , pr e ngu ge ( > t s.s v gu e k im n ( e , i t => v Ti Z n ( le t Z n Of( e ) int i 8n.ti Zo e) tr l tor o k , h nt xts rPe o ( y i t, al tMs s w Lis ses ion y
p S e ( es on y iden i p e. e i y, uil o , / tu u r s c u e b t v r gin loa d — s he i u i at y oe see o r ard s G ds o P . u r , etO Cr t t s ( c r e d ume t o ( C d & h o e key) / c n s io s l t r o en es a f om t ar i # . ns r s m d
wai e u e on a ion { u Id o e h um dg ( esu e, e u Ma a r ad l: q) > l s ba T v b tim c i , q) a u m ri ( = E is d m r sB ssio ( dr , i C l e t n, q), o : ( ) > on e er ( ), } m , ); / A c e io lie pe s t wi l / n 2 : he p so p e n w ne on o . s pr r = u d nt ser v
u th Vaul P th g a tSe o En : uid) > L t i p s d um r e ( d n is di olle t erI u } l W F l I Fn: st i i nR ts k F nR n e ki l n o r n e l n I : s ) n l r emor g } p i ion ( ) ; re r et r te i ry , a p , p m e ed r u t rC t , r ; , ck e si n s a
) i u k us ca ne . i u eCa l k (k n lS a o l h > s n .re l c , { T t } W s o o ro k s d i m m r , h r s io u e b e e , ry g C t r l ) ca V rb e b im v der c t E h n r o o l t p / A p th l s a
p e s u n t e e t in fac s: (se ionK ) d St . urf ce s s o ) // f m pa y, o s a st r t i c t on e s: C n D st nf r tio a l h i V u t a h r teC mat onNot : w o f N t o e e s o , t => e s s o a T r m c n e e c n l' s
he fl r f p / T s ona ab li ie s l e o f ati n re ds e r u id an s l , c n t f e ; a t s / n e t m n m c n me C n un i t n r . n ( s co s . r g) i C f r i m y k .r de t , dmi peo le. it, c a l o e t son l ag
n s t i n , t e au i e e tT .i t f h n dAu (c i u s . n , log: ( ) => n e r / r b e h s o e o t e d t s.t e os R d ( e : o l e u ik Pa h, d co l t ons { e at er e io , d epis Col on m c e c c l c i n
t nS e e i o e , th g m t o s er g o) st e w (o . ap(( > i et n u d l g n est( ug te ( > f .h s(p n ) l i ac [ o i g.s i s h el H al q a pr r d => f e n m e r e n a > ext v u ge ref d eZ n k
r f ti O ( e c a eC a n n i y: p i i y et m i Z e . T , m ) n a t: e o n a e , o l t : rs in . o , l k l i t } i a o w V ( n t t r ) o ab e n = a I s nCr d e a n { e S sio ) = c s s
; con C p u e ( ; r d) { r r e ; , m s y u } S i e > stor e k y ; io pt . o ) i . e ) , io : y n a s . c De , eT o N m . . S I E _M t 0 0) , c I e lM : b s n I N
C R _ S i g 0 ), } ); n f vie s S l R v r ( R i i i F le V h ve ki l " ) ) n n ph n D s i iV h , T : = T wi V a , n ) In e A h n = e O ph ss va l Pa l h m de r h n u t ad n
t P h h o ), m g c n o $ , r o : o v w ; e u n n n l ha l n r , Ho t o m rt n } r m " q s cl -r m rt ll m v m l e { c e n m nS r r s a
C i t i a n y u e f / o m- n "; t c a s or m / a -s o . r e T s pr o t i n t e z l t al } d t s a a i c C r . i ne c g { R s i
e t es y R a i . tor t p e t u i } r e s /su a m r s s o e c a e t t cr e a o s o m i t ex r o . im i t r m n y e f / o . s s e a
a me " s o r s t i S o } . s m p o t s s , t tN . e on i - am t o t c e t " u e /s " t s n on s r { T p S ; dA ". r au
t -l e t l y } f r c d { S r p im p d n e gin r a e s" im t a o sp c b m o } m . se i fo p r th o S r m ol oo s o o c i
ma e t " c W i r t l . e o s o s o { ea l } - i o io r r o l s n a T , p i t r e
or o g t i n m , od esBy s om m o s mp r i Co ti b o r e- or m t ro r m o /m y p p s a on e y m e y t a " { s r o
r c r y t s s i W a s ; o da o t e e n . p / c c s t e m d . ; t /
a t . o or i n I w k - ; " i t d ; p t a r / i p ur s am ; / f b
e - t / i t t e s i s o u e m t i s i r P op n o it . ks ro . "; n
k m o f i ts p ; r . s s l r d n C P e - el e o .
/ e r ; o ; o i e m o f " o u n r = { o : e N m
m o d or a l i n e ; l m o t g l r s: S i l n h p s
o s / W h t , o m a p t y a t h s t u f gi ' e e i c r i d s i
. t A T r e e t l " a n e u h
r r / i o a c Th " m c
n o l i g
n u k e o ) o = o _ u
t o e a r u . U
/ t c t P
s e t e n
a n e
: i i s o .
t i
Mercury Framework (Mercury, for short) is a framework for building your own agent: a program that talks with people in a chat, understands what they ask with a language model, and does it with the tools you give it, such as searching the web or opening a ticket.
You describe the agent in one file: what it's called, where people can talk to it, what it's allowed to do. Mercury takes care of the rest, from the conversation to the memory of what was said, and it runs on your own machine or server, in Docker.
What you get
- Channels
- Where people talk to the agent: Google Chat, or an HTTP interface for a chat of your own. A terminal is always there for you.
- Plugins
- What the agent can do. Each plugin adds tools (Jira, Bitbucket, web search…) and the agent can use nothing it wasn't given.
- Confirmations
- An action that can't be undone never runs because the model decided so: the person gets a one-time code and the action runs only when they send it back.
- Memory
- The agent remembers the conversation, keeps notes in a wiki, and recalls earlier conversations. Each person has their own.
What it isn't
It isn't a service you sign up for, and it doesn't come with a language model. The model is yours: Mercury talks to any server that speaks Ollama's protocol, on your laptop or on a machine in your network, so the conversations never leave your hands.
What you need
- Bun, the JavaScript runtime Mercury is written for.
- Docker, where the agent runs.
- An Ollama server with a model that can call tools, reachable from Docker.
Build your first agentfrom an empty folder to the first answer