The stable surface of the engine as of the V1 "Neurosymbolic Reasoning Graph" (formerly "MOE Graph") Phase-0 work. Skynet-graph is a library: a host app embeds the engine, supplies concept definitions (the experts) and provider functions (the effectful work), and drives the graph through mutations.
Mental model: data objects (nodes, segments) carry typed facts. Concepts are declarative
rules that become applicable when their require/assert conditions hold against the facts; when
they cast, they add more facts and/or new child segments, which makes other concepts applicable —
a forward-chaining cascade that runs to a fixpoint (stabilization). Mutations destabilize;
stabilization re-casts/uncasts; repeat until nothing more fires. See CLAUDE.md and doc/original-2016-doc.md
for the concept-schema and the embedded reference/template DSLs.
const Graph = require('skynet-graph'); // native CommonJS — no build step
const graph = new Graph(record, conf, conceptMap);record— the initial graph. Either a serialized snapshot{ graph: "<json string>", lastRev }or a plain{ lastRev, nodes:[…], segments:[…], freeNodes:[…] }(or{ conceptMaps:[…] }, each tagged withNode:true/Segment:true). Nodes/segments are{ _id, … }; segments also needoriginNode/targetNode. Arbitrary typed facts (e.g.Position,Distance) live right on the object record.conf— overrides merged ontoGraph.prototype.cfg. Common fields:field meaning labelname for logs autoMountstart stabilizing immediately (default true)isMastermaster vs client (client forwards mutations via pushToMaster)conceptSetswhich keys of conceptMaptodeepmergeinto the active tree (default["common"])onStabilize(graph, tokens)called once the graph settles (the main hook) bagRefManagersexternal-data ref managers (default caipimatches/^db:(.+)$/)conceptMap— host-supplied, keyed by concept-set name ({ common: <tree> }). The tree is a nestedchildConceptshierarchy.lib/authoring/core/concepts.js#buildConceptTree(dir)assembles one from theconcepts/<set>/directory.
new Graph(seed, {
autoMount: true, isMaster: true, conceptSets: ['common'], bagRefManagers: {},
onStabilize(g) { /* graph has reached a fixpoint */ }
}, { common: tree });onStabilizefires every time the graph reaches a coherent fixpoint — after the initial mount and after every mutation/rollback/patch settles. It is the primary place to read results and to drive the next step.graph.stabilize(cb?)— ensure the task loop is running;cbfires once on the next settle.- A snapshot is captured on every settle (see History).
graph.pushMutation(template, targetId, force?, atomId?, initialRefBag?, cb?);Apply a mutation template that creates/updates objects and marks them unstable. The template uses the
$-prefixed reference/template DSL ($_id, $$_id, $key ref, $$key bagRef, _incoming/_outgoing
nesting). Example (grow the graph):
graph.pushMutation([
{ _id: 'tokyo', Node: true, Position: { lat: 35.6762, lng: 139.6503 } },
{ _id: 'long2', Segment: true, originNode: 'paris', targetNode: 'tokyo' }
]);Providers emit the same templates from their callback (see Providers). After mutating outside an
onStabilize turn, call graph.stabilize() if the loop isn't already running.
| method | returns |
|---|---|
graph.serialize() |
{ lastRev, graph: "<json>" } snapshot of the whole graph |
graph.getCurrentRevision() |
current revision number |
graph.getRevisions() |
ascending list of revisions with captured snapshots |
graph.getSnapshot(rev) / graph.diffRevisions(a, b) |
snapshot at rev / added·removed·changed between revs |
graph.rollbackTo(rev) |
re-mount that snapshot, drop later ones (linear undo), re-stabilize — restores rules too |
graph.exportConcepts() |
the LIVE concept tree as a serializable record (reflects addConcept/patchConcept) — feed to corpus-pack / exportConceptsToDir |
const revA = g.getCurrentRevision();
g.pushMutation(/* … grow … */); // -> settles at revB
// later:
g.rollbackTo(revA); // the growth is undone, onStabilize re-fires at revASnapshots are full serialize() copies captured on each settle (delta-replay is a later optimization).
Spin up an independent child graph (a sandboxed sub-agent, optionally with a different concept set = different capabilities), let it work a sub-problem, and reintegrate its result.
const child = graph.fork(subSeed, {
label: 'child', conceptSets: ['worker'], // child-only capabilities
reintegrateInto: 'root', // target object in the PARENT
project: (child) => ({ $$_id: 'root', mergedWork: child._objById['sub']._etty._.work })
});fork(seed?, conf?)→ a new childGraph.seedomitted ⇒ forks this graph's current snapshot. Reuses this graph's concept library unlessconf.conceptMapoverrides it. Withconf.reintegrateInto(+ optionalconf.project), the child auto-merges back on its own stabilize.merge(child, targetId, project?)— applyproject(child)(a mutation template) ontotargetIdin this graph, thenchild.destroy(). Defaultprojectattaches the child's serialized graph.
Change a live concept (an "expert") and re-evaluate the whole graph against it, both directions, with no restart or rebuild.
graph.patchConcept('Far', { assert: ['$Distance.inKm > 500'] });patchConcept(nameOrId, updates, cb?)— deep-mergesupdatesinto the concept's schema (arrays REPLACE, not concatenate — so{assert:[…]}overrides), recompiles its applicability test, then for every live object: newly-applicable + not cast ⇒ cast; cast + no-longer-applicable ⇒ uncast (cascading to child concepts); then re-stabilizes.getConceptByName(nameOrId)— resolve a concept by its library id, else by_name.
Tightening an assert retracts the concept where it no longer holds and its dependent children; loosening one casts it onto newly-qualifying objects.
| method | meaning |
|---|---|
graph.castConcept(objId, conceptId, cb?) |
force-cast a concept onto an object, re-stabilize |
graph.unCastConcept(objId, conceptId, cb?) |
force-uncast, re-stabilize |
graph.getConcept(id) |
concept instance by library id |
graph.getRef(exp, scope?, follow?, unref?) |
resolve a reference path (a:b:c, $key) from a scope |
graph.getEtty(id) |
the Entity wrapper for an object id |
graph.getPaths(fromId, toId, skip?) / graph.getOpenPathOf(id) |
path discovery / a PathMap |
graph.on(evt, cb) / graph.un(evt, cb) |
subscribe/unsubscribe ("stabilize", "destroy") |
graph.destroy() |
tear down the graph and its objects |
Object internals: graph._objById[id]._etty is the Entity; …._etty._ is the raw typed facts record
(e.g. graph._objById['long']._etty._.Distance.inKm).
A provider does the effectful work a concept needs (geo math, an API/DB call, an LLM call) and emits
facts. The engine looks them up in Graph._providers (host-wired — the engine does not auto-load
them). A concept references one as "Namespace::fn" (optionally ["ns::fn", …args]).
Contract:
Graph._providers = {
Namespace: {
fn(graph, concept, scope, argz, cb) {
// …compute…
cb(err, mutationTemplate); // template applied onto the scope object (`$_id:'_parent'`)
}
}
};scope is the object's Entity; read its context with graph.getRef('originNode:Position', scope).
Return cb(null, null) to no-op (e.g. wait for a dependency).
const Graph = require('skynet-graph');
const { register, CommonGeo, createLLMProvider } = require('skynet-graph/lib/providers');
register(Graph, [ { CommonGeo }, createLLMProvider({ ask: myBackend }) ]);
// register(Graph) with no selection wires the defaults (Geo + a default LLM client)-
CommonGeo→CommonGeo::Distance— haversine great-circle distance between two nodePositions, emitting{ Distance: { inKm } }. Drivesconcepts/common/Edge/Distance.json. (haversineKm(a, b)is also exported as a pure function.) -
createLLMProvider({ ask, parseJSON?, namespace? })→{ LLM: { complete } }, a generic concept↔prompt runner. The concept supplies apromptblock:{ "provider": ["LLM::complete"], "prompt": { "system": "You judge.", "user": "Step: ${label}", "maxTokens": 500, "json": true, "as": "Classification" } }system/userinterpolate${ref}tokens (resolved against the scope viagraph.getRef);json:truesalvages the reply (robust to "thinking" preambles — returns the last balanced JSON); the result is written back as facts (merged if a plain object, or stored underas). Backend errors are captured as anllmErrorfact so the graph still settles. The backend is pluggable — pass anyask({system,user,maxTokens}); the bundledmakeAsk(opts)dispatches onopts.api/ envLLM_API—anthropic(default,/v1/messages) oropenai(/v1/chat/completions, for vLLM / llama.cpp / LM-Studio; readschoices[0].message.contentand falls back toreasoning_contentfor reasoning models).makeOpenAIAsk/makeAnthropicAskare exported too. All configurable viaLLM_BASE/LLM_MODEL/LLM_KEYor{ base, model, key }.
An LLM::complete expert whose output feeds downstream experts must not let raw prose onto a
dependency edge: two semantically-equal replies differ textually, so a require/ensure keyed on prose
re-keys every run and the memo never hits (risk K1, doc/MODELISATION.md §4.2). Declare a facts
schema and the provider writes only those discrete, canonicalized keys as tracked facts; the free
text lands on an untracked prose key; a stable <name>FactsDigest is emitted as an explicit memo key.
{ "provider": ["LLM::complete"],
"prompt": {
"system": "Classify the risk.", "user": "Step: ${label}",
"facts": { // the tracked, discrete spine
"severity": { "enum": ["low","medium","high"] }, // snapped to a closed vocabulary
"priceK": { "grain": 100, "from": "price" }, // numeric rounded to a grain; read raw `price`
"count": { "type": "int" } // int | number | bool | id | string
},
"prose": "summary" // free text -> UNTRACKED key (default: `<name>Prose`)
} }Snapping is deterministic only (never embedding similarity — a fuzzy false-hit graves a wrong fact
that propagates). An out-of-vocabulary enum fails closed: the fact is null and the key is listed
in <name>CanonMiss (visible, never a silent wrong snap). Helpers are exported for direct use:
canonFacts(raw, schema) -> { facts, misses }, canonValue(raw, spec), digest(facts).
With no facts schema declared, the provider keeps its legacy merge/as behavior (back-compatible).
register(Graph, fragments?)merges provider-map fragments ontoGraph._providers, preserving any already set.
The engine maintains coherence, never truth (K3): a hallucinated-but-valid fact propagates and
retracts cleanly. Verification makes unreliability visible and non-propagating by emitting discrete
verdict facts that downstream concepts gate on via ensure — a refuted fact auto-retracts its
dependents (refutation is defeasance; no new engine path). Verdicts are discrete (the typed-fact spine),
never prose, and never overwrite the checked fact. createVerifier() (from skynet-graph/lib/providers)
returns { Verify: { check }, Vote: { tally } }; checks and majority are exported too.
Three patterns (all engine-verified; pick by reactivity need):
- Deterministic verifier = a concept whose
ensureIS the invariant — fullexpr.jsgrammar (uncapped):{ "_name":"BudgetOK", "require":["cost","cap"], "ensure":["$cost <= $cap"] }. Its self-flag is the verdict; a target change re-tests it and auto-retracts. A consumer nested under it (childConcepts) cascade- retracts on refutation. Prefer this — deterministic checkers ≫ LLM-refuters, and it is reactive. - Independent verdict provider
Verify::check— for a check that runs as an effect (external lookup / LLM-refuter):{ "provider":["Verify::check"], "verify":{ "target":"$x", "check":"range", "params":{"min":0,"max":100}, "as":"x" } }writesxVerdict/xVerified/xVerifiedAgainst(provenance), never$x. Downstream gatesensure:["$xVerified == true"]. (A provider verifier is cast-once — it re-runs only on uncast/recast; use pattern 1 for reactive re-checking.) - k-of-n voting
Vote::tally— self-consistency: n strategies{__push}a vote into a grow-only array; aVoteconcept gatedensure:["$votes.length == $expected"]emitsconsensus+confidence = agree/n; downstream gatesensure:["$confidence >= 0.7"]. Treatconfidenceas a heuristic, never proof (a biased model votes confidently wrong). Independence discipline: the refuter must not be the call that produced the fact.
Deterministic checkers: range, oneOf, equals, approx, nonEmpty — (value, params) -> { pass, reason },
extend via createVerifier({ checks: { … } }).
The engine has no internal wall-clock (replay stays hermetic). Time enters as an ordinary fact on a
clock free-node; a time-bound concept gates freshness in an ensure. Advancing the clock re-tests
exactly the concepts that follow it, so a fact that has gone stale auto-retracts (and its dependents
cascade) — the cache-poisoning fix (an LLM/API fact otherwise lives forever). The whole pattern is plain
seed + mutation, no dedicated API:
// seed: { freeNodes: [ { _id:'clock', tick:0 } ], nodes: [ { _id:'n', source:'db', sensedAt:0 } ] }
// concept: { "require":["source"], "ensure":["$$clock:tick - $sensedAt < 2"], "provider":["AI::sense"] }
// ($$clock — DOUBLE-$, a GLOBAL free-node ref; a single $clock is a key on the current scope)
g.pushMutation('clock', { $$_id: 'clock', tick: 3 }); // tick 0 -> 3: the fact is now stale -> retracts + cascades- Invalidation is automatic and reliable. Refetch is host-triggered — a provider is cast-once, so a stale provider-fact re-derives only on uncast→recast (retract the cast flag, restabilize). A provider stamps its fetch time from the clock node.
- Pitfall: an
ensurewith||("$x==null || $$clock:tick-$x<t") short-circuits watcher registration — seed the stamp so the freshness operand always evaluates, or split the fetch from the freshness gate.
A host-side validator enforces the typed-fact discipline before a concept tree reaches the engine — the safety gate for hand- or AI-authored concepts. It validates structure, never the expression grammar.
const { validateConceptTree, validateOrThrow } = require('skynet-graph/lib/authoring/core/validate');
const { errors, warnings } = validateConceptTree(tree, { palette: ['LLM::complete', 'CommonGeo::Distance'] });Checks: every concept has a _name (the self-flag — without it the engine re-fires it forever);
assert/ensure parse under the real evaluator (lib/graph/expr.js) and don't touch constructor/__proto__;
provider ∈ a vetted palette (advisory warning, or an error under { strict: true }); and the valuable
one — ref soundness: a require/ensure/assert that keys on a prose fact (a declared prose
key, or the <name>Prose/<name>CanonMiss defaults) is rejected (it would fragment the memo, K1),
and a bare dependency on a child-set (expandedInto/answeredBy/…) without .length is warned (the
"all-children-answered" footgun — getRef has no quantifier). validateOrThrow throws on the first error.
All are require('skynet-graph/lib/providers') factories; pair each with its ready-made concept-tree
fragment. The deterministic core is untouched.
| factory | wires | concept-tree helper |
|---|---|---|
createSemiring() |
Semiring::reduce — fold {__push}ed contributions under boolean/logodds/maxplus/probor |
semiringConceptTree({ semiring, contribKey, bands? }) |
createSemiring() (pareto family) |
multi-criteria skyline SELECT → selectedId/frontIds/frontSize |
selectConceptTree({ criteria, lex }) — the Candidate→Selected cluster |
createStats() |
Stats::{report,grandMean,shrink} — hierarchical Beta-Binomial shrinkage |
shrinkageConceptTree(...) |
createNogood() |
Nogood::guard — learned sound-skip of dead-ends |
nogoodGuardConcept() / guardTrial(schema) |
createVerifier() |
Verify::check (verdict facts) + Vote::tally (k-of-n) |
— |
createConsistency() |
Merge::combine — sheaf-style agree/borderline/conflict bands |
consistencyConceptTree() |
createSolver({ solve }) |
Solve::run — a C-regime fork that searches; crosses only the snapped model |
solverConceptTree() |
createConstat() |
Constat::record — typed lesson-on-retraction {claim,retractedBecause,certaintyBand,atRev} |
— |
Pure helpers: paretoFront / paretoSelect / makePareto / dominates / reduceSemiring
(lib/providers/semiring.js).
treeDecomposition(tree)/forkPlan(tree)(decompose.js) — derive, off the concept-dependency graph, the separator interface + the candidate forks + each fork's frontier alphabet + the treewidth cost bound (partitionPays). Feedsfork/mergeandvalidateMergeProjection.conceptFactGraph(conceptMap)(grammar-graph.js) — the concept↔fact flux graph: produced / consumed facts with polarity, cross-corpus links, writer-collisions, entry points, tiling overlay..sgccorpus exchange (corpus-pack.js):deriveManifest(produces/consumes alphabet, required providers),packCorpus/unpackCorpus(a portable bundle of the authored grammar). Disk round-trip:Graph.loadConceptMap(dir, { validate })↔exportConceptsToDir(tree, dir)(lib/load.js).
supportConceptTree({ criteria, lex }) + makeSupportProviders({ evalFn, expandFn, proposeFn, escalateFn, escalateBar, rollupFn }) compose the decompose loop with the per-segment
Propose → Pareto-SELECT → Adopt alternative-search trio + escalation on Stuck. Inject the content
functions (deterministic in tests, an LLM in production).
The LLM-driven Use 2 mechanism — forge / crystallize / reuse typed methods on top of the substrate.
Host-side, ZERO-CORE, additive (the base hand-authoring use needs none of it). This is the mechanism the
two preprints measure; their replay artifacts ship under artifact/.
abstract.js— F6 abstractivation:relativize/instantiate(id/frontier holes),antiUnify(Plotkin LGG),methodTransform(the cache{onStore,onReplay}),emitMethodAsSubgraph(re-mountable parameterized method viaGraph#getMutationFromPath). The cross-problem structural-transfer keystone.crystallize.js(+mine.js,abstraction.js,memo-stability.js) —crystallize/adopt/consolidate: mine a producer→consumer chain → compose → MDL/utility gate (abstraction.evaluatescores model calls) → install fail-closed (memo-stability).crystallizeStructuraldistils a recurrent structural cast into a re-mountable defeasible method with a declared frontier (mine.js#declaredCtx) reified asschema.frontier(aFrontierSignature—{params:[{name,sort,field,role}], summaryFacts, appConditions}, serializes with the tree) + alibraryKey(the O(1) dispatch index) +lintFrontier; a soundness gate refuses a method that would leak a learning id at replay (un-holed / base-prefix-phantom / collapsed endpoints).library.js·adapt.js— the method-library index + the adapt-or-forge controller.library.js:makeLibrary/indexMethod/dispatch(lib, target, scopeFacts)— an O(1) bucket lookup onlibraryKey→ refine by application-conditions → ranked candidates (a lookup, never a corpus search).adapt.js:adaptOrForge({lib,target,scopeFacts,forge,verify})— retrieve(hit, 0 calls) / forge-or-adapt(reuse neighbours) / verifier-gate(contract) / index-back(amortise). Structure-mapping (Gentner) over learned methods.method.js— the concept-as-graph host toolkit (the middle spine):applySubgraphArg/mapTemplate/mapSubgraph(a method receives + applies a sub-graph param;Mapfans a body per element with fresh ids),lintMethod(def)(the decidability invariants + the footprint/frame check),selectCluster(case-parameterized selection by mutually-exclusive typed gates).
The defeasible separation-triple checker (Use 2's soundness). Exposed on the facade as deep-path; ZERO-CORE.
checkCompose(m1, m2, opts)→{ verdict:'sound'|'unsound'|'escalate', shared, perKey, reasons, needsOracle }—post(m1) ⊨ pre(m2)overwrite(m1)∩read(m2), by per-key abstract-domain entailment (normalize/entailsKey); never false-accepts (out-of-fragment / under-determined →escalate).opts.oraclefor an effecting m1 (G2).assertPost(contract, factsAfter, touchedKeys, opts)→{ ok, violations, blame }— the runtime monitor: G1 frame-completeness (touched ⊆ declared write), the post must hold, G2 the effect-tag oracle.footprintCycles(methods)→ cycles of retractable methods (Tarjan-SCC; G3, reject before they oscillate).reviseOnBlame(contract, {key,value})→ a NEW contract with the pre specialized (CEGIS — un-learn, not removal).satisfies(atoms, facts)— is a method applicable to a case (selection after a revision).acceptRate(verdicts)— the measured typed-coverage fraction.
The "execute" half of the build/execute separation: a thin durable substrate that runs case records through a
compiled method-net (the belief / durable boundary). ZERO-CORE; on the facade as Graph.durable +
Graph.createCheckpointStore({file}).
checkpoint-store.js—createMemoryCheckpointStore()/createSqliteCheckpointStore({file})(one contract). The durable marking (ensureRun/inject/claim/move/fail/joinArrive/failGroup/track/rollbackInflight/marking/stats), the content-addressed memo (memoGet/memoSet), the createdRefs rollback. Crash-safety = lease-expiry +rollbackInflight+ a fencing token (a monotonic persisted leaseId; a re-claimed lease fences out a zombie worker).xlate.js—compileMethod(spec) → net(a select+task+map+reduce spec → a workflow-net),validateNet(structural lint),indexByFrom.interpreter.js—runFlow(store, runId, net, { runTask, keyOf?, foldKeyOf?, oracle?, assertStep?, lease?, batch?, maxSteps? })→ measured counters. Drains records: typedselect, content-memoizedtask,mapfan-out, the fold-backjoin/fold. A per-stepcontractis asserted before commit (a violation quarantines- blames). Async (real LLM micro-tasks). Resumable (call again after
rollbackInflight).
- blames). Async (real LLM micro-tasks). Resumable (call again after
fold.js—foldSiblings(siblings, reduce)(the JOIN's monoid algebra; commutative = order-independent),monoids(),isCommutative(name).audit.js—auditRun(store, runId)→{ records: { <id>: {status, terminal, result, blame, lineage…} }, totals }(the derivation forest + verdict + blame),auditSummary(audit)(one line per record).
Train concepts as neural-net populations instead of hand-authoring them, then bake the frozen result back into the engine. Host-side, ZERO-CORE, shelved (kept for the curious).
equilibrium.js— gradient through a fixpoint (Deep Equilibrium Models / implicit diff):solveFixpoint(F, z0, {maxIter,tol})(Picard toz*),implicitGrad(Jz, Jtheta, gradL, {mode})(mode:'direct'dense adjoint solve or{neumann:K}),spectralRadius(Jz)(theρregime instrument),numJac(fn, point)(finite-difference Jacobian of one sweep).concept-net.js— a population of concept-units (gate-NN × update-NN):ringPopulation(K)/chainPopulation(K)/widePopulation(K)(builders) →train(pop, {X,T,steps,lr,hard})(learn at the fixpoint, returns{theta,loss0,loss,rho}),grad/loss,evolve({makePop,X,T,maxK,margin})(grow the form by success, utility-gated),bakePopulation(pop, theta)→{conceptTree, providers}(serve a frozen population as real engine concepts),unrollPopulation(pop, N)→{pop, tieTheta, readout}(serve a cyclic population by unrolling its fixpoint to depth N; a direct cyclic bake deadlocks).lifecycle.js— the plasticity ledger:createLifecycle()→register/record(name, ok)/plasticity(name)(the unified knobp∈[0,1]) /regime(name)/reputation(name). ThreadplasticityintocreateLLMProvider({ ask, plasticity })(→ temperature) orcreateNet(net, { plasticity })(→ STE exploration noise) sop=1explores /p=0serves deterministically.
Each combo composes existing bricks with the product posture ON by default (fail-closed, memo/store
ON, validator ON); none is a required path — the bricks stay usable "à nu". Full guide:
doc/usage.md §9; per-capability maturity + numbers: CAPABILITIES.md.
| combo | role |
|---|---|
createAppliance (C1) |
typed-QA appliance: intake → reason loop → typed refusal → memo |
createDurableRunner (C2) |
durable workflow runner (compile / run / resume / audit) |
createLearningLibrary (C3) |
learning method library: cost ladder + crystallize + blame/credit + .sgc |
reactiveKG (C4) |
the engine's original Use-1 preset over fromDirs (builtins ON) |
createSelfMod (C5) |
supervised self-modification (opt-in, guarded; rollbackTo = the guarantee) |
createProxyCache (C6) |
local-first proxy cache / distiller (covered → local at 0 frontier calls) |
createPlanLoop (C7) |
the hierarchical plan loop (the piece-by-piece zoom); decompose/serveLeaf injected |
createMixtureServe (C8) |
orientation menu + optional preRoute over a low-quant target (the runtime cross-agreement trust tier is REFUTED at scale — fail-closed default) |
createCriticalMind (C9) |
the external critical mind: run({topic, statements?, viewpoints?}) → typed ledger + verdict or honest UNDECIDED (mechanical only at the measured margin bound) |
The C7 bricks are standalone lib/authoring/ modules: dag-decompose, context-project (with the
stratComplete stratified rendering), givens (numberGivens / cellGivens / seedOf / labelsOf —
labelsOf implements the measured CELLS rule: label an input iff its provenance is a structured table
cell, and pass it as run(task, { givens, labels })), leaf-io.
const server = Graph.createStudioServer({ Graph, root, ask, logger }) — an http+ws server over a
registry of live Graph sessions, driving a no-build React UI. Wire ops (lib/studio/protocol.js):
grammarGraph · corpusManifest · exportCorpus / importCorpus (.sgc) · providerTrace ·
mergePreview · forkPlan · fork/merge/selectSession · mutate/run/state ·
revisions/snapshot/rollback/diff · validateConcept/patchConcept/addConcept · prompt.
Events include a Session-derived retract (the red-flash signal). Also via bin/sg studio.
One logger per graph (graph._log, exposed as graph.logger). Levels, severity descending:
error > warn > log > info > verbose — a record reaches sinks iff rank(level) <= rank(threshold)
(default warn, or env SG_LOG_LEVEL; the sg run CLI overrides its own session to info). A LogRecord is { level, ts, label, ctx, msg, args } and is
JSON-serializable (Errors in args are reduced to {name,message,stack}).
Configure (in cfg, e.g. via new Graph(seed, conf, conceptMap) or Graph.fromDirs({conf})):
| key | meaning |
|---|---|
cfg.logger |
inject a logger instance (Graph.createLogger(...)); overrides the rest |
cfg.logLevel |
threshold name for the auto-built logger |
cfg.onLog(record) |
convenience sink fn |
graph.logger interface (the debugger/host hook):
| method | returns |
|---|---|
addSink(fn) / removeSink(fn) |
register/unregister fn(record) |
tail(n, filter?) |
last n records; filter = {concept?, target?, applyId?, level?} |
records |
the bounded ring buffer (default 500) |
setLevel(name) / level |
get/set the threshold |
child(ctx) |
a logger that merges ctx into every record |
Provider logging contract (no provider-signature change): inside a provider, reach a context logger via
scope.log (ctx {target,type}) or concept.log(scope) (ctx {concept,target,type,applyId}):
function work ( graph, concept, scope, argz, cb ) {
const log = concept.log(scope); // capture early (freezes applyId for async)
log.verbose('prompt', prompt);
log.warn('input missing on %s', scope._._id);
cb(null, { $_id: '_parent', Worked: true });
}Each concept-apply mints graph._applyId, stamped into both the contextual logger and the
cfg.onConceptApply trace record. So the logs a concept produced while applying are retrievable with
graph.logger.tail(n, { concept }) or { applyId }, and join the trace by applyId — all without
storing anything on the graph objects (logs live in the logger, never as facts). For full history beyond
the bounded buffer, attach a sink (file/studio).
CLI: sg run … [--log-level <lvl>] [--log-mode dashboard|plain] [--log-plain] [--log-file <path>] [--log-file-level <lvl>]. A styled boot banner prints at startup. dashboard = TTY mode where colored
logs scroll normally under a live status bar pinned at the bottom (graph state, unstable node/segment
counts, main-loop queue size, rev, applies, provider time, elapsed); degrades to plain off a TTY. plain =
line output (logs → stderr, run summary → stdout). --log-file writes .jsonl (machine-readable) or
formatted text. Workers: pass logger to createGraphWorker/spawnGraph and a dispatched graph's
log records re-emit into it, tagged {worker:true}.
Graph.createLogger({ label, level, onRecord, capacity, console }) builds a logger standalone.
Two zero-dep, zero-integration fronts over the combos (full guide: doc/usage.md §10):
sg serve— an OpenAI-compatible endpoint (POST /v1/chat/completions,GET /v1/models) over the C6 proxy cache.createServeHandler({proxy, model, onAnswer})is the PURE request handler (stub-testable);startServeServer({handler, port, host})is the node:http wrapper. Provenance on every completion: headersx-sg-served-from|arm|cost|coverage|saved|sgc-version+usage.sg_*mirror;stream:trueis simulated SSE. The v1 wire contract: the query = the LAST user turn (a QA cache, not a dialog engine).sg mcp— an MCP tools server (stdio JSON-RPC).createMcpServer({tools, serverInfo})is the pure dispatcher;defaultTools(wiring)wires the base tools —ask(answer OR a STRUCTURED typed refusal),drift,metrics,lattice_load(growth throughloadLattice— the only registry write path),methods_describe,lattice_rings,trace_tail— plus the ASSISTANT lanes: SOFThint/state_recall/state_note/plan_sync(the typed task delta, JTMSreopenincluded), HARDpropose(gate-tested;force→ recorded-untrusted, never admission), INSTANCESgraph_invoke/graph_instances, and the C9critiquetool (typed ledger + verdict or honest UNDECIDED; OPEN points + UNDECIDED = a typed data request — re-call withstatements).stockWiring(sgc)/--stock <f.sgc>wireshint/proposefrom a forged stock;startMcpStdiois the line-framed transport.sg flow run <module.js>— the C2 durable runner as a CLI; the module exports{ spec, runTask | makeRunTask(), keyOf?, STREAM? }.- Intake depth back-check —
require('lib/providers/intake.js').makeProseBackCheck({ask, proseOf?, onBlame?})→ a ready-made independent verifier forcreateIntake({backCheck})(a 'fail' downgrades the intake tountyped; judged-wrong keys surface viaonBlame).
npm test # unit + integration (node:test, native CJS — no Babel)
node examples/integrated-demo/run.js --replay # the public verifiable: 7 checks, deterministic, no GPU
node examples/run-basic.js # non-LLM end-to-end stabilization over the real `common` set
node examples/run-problem.js # LLM-driven plan decomposition (needs an endpoint; see lib/providers/llm.js)
node bin/sg run --concepts ./concepts --builtins # standalone CLI bootThe engine is native CommonJS and runs directly under Node (no build step; tests/_boot.js just sets
__SERVER__).