AI-native tool service platform (Backend as Toolset). Pure API product, no UI. Any external Agent can integrate via REST API or MCP. Multi-tenant SaaS, designed for high concurrency.
Connecting an agent? Two entry points:
- Agent Bootstrap Prompt — a self-contained block to paste into a new agent's instructions. It asks the human for the deployment address and API key rather than hardcoding them.
- Agent Integration Guide — the full reference, written to be read by the agent itself and verifiable step by step.
This README is the complete endpoint listing.
┌──────────────────────────────────────┐
│ External Agents │
│ (Claude, GPT, Custom, etc.) │
└────┬─────────────────────┬───────────┘
│ REST (JWT/API Key) │ MCP (Streamable HTTP)
┌────▼────┐ ┌─────▼─────┐
│ Gin API │ │ MCP Server│
└────┬────┘ └─────┬─────┘
│ │
┌────▼─────────────────────▼────┐
│ Service Layer │
│ auth / todo / notes │
└────┬──────────────────────────┘
│
┌────▼──────────────────────────┐
│ Repository Layer │
│ pgx v5 + PostgreSQL │
└───────────────────────────────┘
- Go 1.22+, Gin, pgx v5, sqlc, golang-migrate
- Auth: JWT + API Key (dual-track) with scopes
- MCP: mark3labs/mcp-go
Local dev/deployment (git pull + build + run, backed by the shared base
Postgres/Qdrant infrastructure) is managed from infra/agentmate/deploy — see
that directory's README.md for the full setup. Summary:
cd infra/agentmate/deploy
cp agentmate.env.example agentmate.env # fill in AGENTMATE_SRC_DIR, secrets, etc.
nohup bash run.sh >> nohup.log 2>&1 &
# Server runs at http://0.0.0.0:26001# 1. Run migrations against your PostgreSQL instance
migrate -path migrations -database "$DATABASE_URL" up
# 2. Start server
cp .env.example .env
go run ./cmd/serverThe architecture and implementation roadmap for the Git-backed registry are documented in Skill Registry Design v0.1. Skill Registry Phase 4 is implemented as an offline deterministic quality layer: package lint, platform contract checks, same-skill release comparison (including package identity and resource behavior metadata), and strictly version-bound telemetry suggestions. It does not call LLMs, providers, Qdrant, publish/activate/index versions, produce a composite score, or claim semantic evaluation/reinforcement learning. DAG composition remains a later increment.
Skills and knowledge are separate domains. Skill packages carry behavior and execution
assets; domain knowledge corpora belong to a standalone Knowledge Registry that
skills discover at runtime through a Knowledge Discovery Contract instead of fixed bindings.
The target model is specified in
Skill + Knowledge Architecture v0.3.
K1 (knowledge sources, immutable revisions, document snapshots), K2 (K0 catalog cards,
deterministic Markdown chunking, document link graph, account-scoped hybrid retrieval),
and the K3 wiki compiler are implemented. K4 is implemented in three parts: Skills declare a
knowledge: contract in SKILL.md frontmatter (parsed, validated and linted at compile
time, persisted on the compiled artifact, and part of Skill identity via a normalised
contract identity string), POST /api/knowledge/discover resolves that contract
against the account's K0 catalog with classified failure statuses, and
POST /api/knowledge/resolutions freezes what an execution actually selected, retrieved
and cited as an append-only KnowledgeResolutionRun — the anchor for permission audit,
reproduction and attribution.
The official AgentMate Memory skill teaches compatible agents to recall scoped context, journal meaningful events, and preserve evidence-backed durable memory through the Memory MCP server.
Install the complete integrations/skills/agentmate-memory directory in the
agent host's skills directory, then configure its MCP client to use
http://localhost:26001/mcp/memory or the deployment's corresponding URL. Keep
the API key in the host's environment or secret manager.
All REST endpoints are mounted under /api (kept separate from the frontend's
page routes when served from the same origin, see infra/agentmate).
POST /api/auth/register— Register a new userPOST /api/auth/login— Login, returns JWT
GET /api/auth/me— Current user infoPOST /api/auth/apikeys— Create API Key (accepts optionalscopesfield)GET /api/auth/apikeys— List API KeysDELETE /api/auth/apikeys/:id— Delete API Key
curl -X POST http://localhost:26001/api/auth/apikeys \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{"name": "my-agent", "scopes": ["todos:rw", "notes:r"]}'Available scopes:
| Scope | Description |
|---|---|
todos:r |
Read todos |
todos:rw |
Read & write todos (implies todos:r) |
notes:r |
Read notes |
notes:rw |
Read & write notes (implies notes:r) |
reports:r |
Read reports |
reports:rw |
Read & write reports (implies reports:r) |
bookmarks:r |
Read bookmarks |
bookmarks:rw |
Read & write bookmarks (implies bookmarks:r) |
expenses:r |
Read expenses |
expenses:rw |
Read & write expenses (implies expenses:r) |
memory:r |
Read and search durable memories |
memory:rw |
Record events and store durable memories (implies memory:r) |
skills:r |
Read skill logs and versions |
skills:rw |
Read & write skill logs and versions (implies skills:r) |
knowledge:r |
Read knowledge sources, revisions, and documents |
knowledge:rw |
Register/sync knowledge sources and push snapshots (implies knowledge:r) |
manage_keys |
Create/delete API keys |
Empty scopes array [] means full access.
POST /api/todos— Create (scope:todos:rw)GET /api/todos— List (scope:todos:r)GET /api/todos/search?q=— Search (scope:todos:r)GET /api/todos/:id— Get by ID (scope:todos:r)PATCH /api/todos/:id— Update (scope:todos:rw)DELETE /api/todos/:id— Delete (scope:todos:rw)
POST /api/notes— Create (scope:notes:rw)GET /api/notes— List (scope:notes:r)GET /api/notes/search?q=— Search (scope:notes:r)GET /api/notes/:id— Get by ID (scope:notes:r)PATCH /api/notes/:id— Update (scope:notes:rw)DELETE /api/notes/:id— Delete (scope:notes:rw)
POST /api/memory/events— Append an immutable, idempotent memory event (scope:memory:rw)POST /api/memory/entries— Store an evidence-backed durable memory (scope:memory:rw)GET /api/memory/entries— List memories by scope, type, or status (scope:memory:r)GET /api/memory/entries/:id— Get a memory with its evidence (scope:memory:r)POST /api/memory/search— Hybrid PostgreSQL FTS and Qdrant search (scope:memory:r)GET /api/memory/timeline?session_id=&skill_version_id=&limit=— Time-ordered merge of skill executions and memory events. Requiressession_idorskill_version_id; an unfiltered account-wide timeline is a data dump, not attribution. Reportsskill_log_count,memory_event_count,unattributed_countandtruncatedso the coverage of an attribution conclusion is explicit (scope:memory:r)POST /api/memory/entries/:id/supersede— Record that this entry replaces another (bodysuperseded_id). The replaced entry moves tosuperseded, its validity window closes at the supersede time, and its retrieval projection is deleted (scope:memory:rw)POST /api/memory/entries/:id/feedback— Report whether a memory helped (bodysignal=useful|harmful, plusreason,session_id,skill_version_id,metadata) (scope:memory:rw)GET /api/memory/entries/:id/feedback?limit=— The usefulness signal log, newest first (scope:memory:r)POST /api/memory/checkpoints— Save a resumable snapshot of session intent (bodysession_id,goalrequired, plusdone,next,open,notes,label,scope_type,scope_key,skill_version_id) (scope:memory:rw)GET /api/memory/resume?session_id=— Latest checkpoint plus everything recorded after it;resolutionischeckpoint,journal_onlyorempty(scope:memory:r)GET /api/memory/entries/:id/attribution— Resolve which skill execution produced a durable memory. Walks entry → source event → skill version and reports how far the chain got viaresolution:skill_version,session_only,event_only, ornone. Includes the surrounding session timeline when a session is known (scope:memory:r)
Working memory is the raw session layer below the journal and durable memories:
conversation messages, tool results and drafts, organised per session, appended
and replayed in server-assigned seq order. It is the raw material for later
distillation into durable memories — it is never indexed for semantic search
(content worth recalling should be distilled into an entry), never distilled
automatically, and never expired: there is no TTL and no automatic cleanup, so
records live until explicitly deleted.
POST /api/memory/working/sessions— Create a session (bodytitle,agent,metadata, all optional) (scope:memory:rw)GET /api/memory/working/sessions?status=&agent=&limit=&offset=— List sessions, most recently updated first (scope:memory:r)GET /api/memory/working/sessions/:id— Get one session with its metadata andlast_seq(scope:memory:r)PATCH /api/memory/working/sessions/:id— Updatetitle,status(active|archived) ormetadata; at least one required (scope:memory:rw)DELETE /api/memory/working/sessions/:id— Delete a session and all of its items (cascade); reportsitems_deletedfor verification (scope:memory:rw)POST /api/memory/working/sessions/:id/items— Batch append items (bodyitems[], each{item_type, role?, content, metadata?, idempotency_key}); all-or-nothing (scope:memory:rw)GET /api/memory/working/sessions/:id/items?after_seq=&limit=— Incremental replay inseqorder;has_moreis exact even across deletion gaps (scope:memory:r)DELETE /api/memory/working/sessions/:id/items/:seq— Delete one item; itsseqis never reused (scope:memory:rw)
Item types are message (with required role: system|user|assistant|tool),
tool_result and draft; content may be empty (e.g. an assistant turn carrying
only tool calls in metadata) and is stored verbatim, not trimmed. seq is
assigned by the server, monotonic per session — callers do not keep their own
counter. Idempotency follows the memory events convention, scoped to the session:
retrying a batch with the same idempotency_keys and identical content replays
the existing items (created=false, original seq, 200 + X-Idempotent-Replay
when the whole batch replays), while reusing a key with different content returns
409 Conflict and rolls back the entire batch.
Memory events carry an optional skill_version_id attributing them to the skill
execution that produced them. session_id alone is not enough: a session commonly
runs several skills, so session-level correlation cannot tell which execution
produced a given event. The value is verified against the caller's account, and it
participates in the idempotency hash — a replay that adds or changes attribution
returns 409 Conflict rather than silently returning the original unattributed
row. Leave it unset for events with no skill origin, such as a note the user wrote
directly.
Superseding builds a chain, not a graph: C replacing B which replaced A is fine,
but a cycle is rejected with 409 because it makes "which entry is current"
unanswerable. Replaying the same supersede is idempotent; pointing an
already-replaced entry at a different replacement is a 409 conflict. Deleting the
retrieval projection matters as much as the status change — search draws candidates
from the projection and filters by status afterwards, so a replaced entry left
indexed would keep consuming top-k slots and crowd out its replacement.
Feedback signals are the durable record; useful_count and harmful_count on the
entry are a projection of them, kept because ranking cannot afford an aggregate
query per search. One signal of each kind per memory per session, so a retrying
agent cannot inflate a memory's standing. The search score is nudged by a bounded
adjustment (at most ±0.15 on a 0..1 scale) weighted by how much evidence exists:
feedback is a weak, biased signal, so it breaks ties and demotes repeatedly harmful
memories rather than overriding semantic relevance. retrieval_score and
feedback_adjustment are reported separately so a surprising order can be
explained.
Checkpoints are stored as checkpoint events on the journal rather than in their
own table, inheriting immutability, ordering, idempotency and skill attribution.
The default idempotency key is derived from the content, so saving unchanged state
is a no-op instead of appending a near-duplicate. resume returns the snapshot
plus the activity recorded after it: a session is interrupted after its last
checkpoint, so that tail is exactly the state the snapshot is missing.
Event retries must reuse the same idempotency_key. Reusing a key with different
event content returns 409 Conflict. Durable memories require either
source_event_id or at least one evidence item. PostgreSQL is the source of truth:
if embedding or Qdrant indexing fails, creation still succeeds with
indexing.status=failed, and search continues through PostgreSQL FTS.
POST /api/context/pack— Assemble the minimal execution context for a task in one call. Body:task(required),query,skill_name,session_id,knowledge_domain,knowledge_source_ids,memory_scope_type,memory_scope_key,max_chars,top_k,layers,render(route scope:memory:r, plus per-layer scopes below)
Six layers, assembled and rendered in this order: [SKILL] instructions,
[KNOWLEDGE] evidence with citations, [MEMORY] relevant experience, [FACTS]
live todos and notes, [WORKING] the raw items of the named working memory
session, [TASK] the goal plus recent session activity. Every item
carries a source label and a traceable ref so a model can tell authority apart
and a claim can be traced back to its origin.
The value is the budget, not the concatenation. max_chars (default 12000) is
split across the requested layers by fixed shares; omitted layers hand their
share to the rest. Within a pack a layer's budget is never lent to another, so
the result does not depend on assembly order. Oversized content is truncated at a
paragraph or sentence boundary and flagged per item, and each layer reports
char_budget, chars_used, dropped and truncated. Budgets are in characters
rather than tokens deliberately: token cost is model specific, so embedding a
tokenizer would tie the server to one vendor — characters are exact and the
caller can apply its own ratio.
Authorisation is per layer, not per endpoint: one endpoint spanning five domains
must not let a skills:r key read memory. SKILL needs skills:r, KNOWLEDGE
needs knowledge:r, MEMORY and WORKING need memory:r, FACTS needs todos:r and/or
notes:r (each half authorised independently), and TASK needs no scope for the
goal statement but memory:r for the session slice. A layer the caller may not
read comes back empty with an explanatory note, and the call still succeeds —
partial context beats no context, but never silently. The same applies to a
failing or unconfigured layer.
TASK prefers a saved checkpoint over reconstruction, and includes the activity
recorded after it. Sessions with no checkpoint fall back to a journal replay, and
the layer note says which of the two was used. One current limitation remains
reported rather than hidden: skill selection is a pinned skill_name or the top
retrieval hit; dynamic discovery driven by a Skill's knowledge contract is K4.
FACTS is queried live and never embedded: task state changes constantly, so an
indexed copy would serve stale facts with the confidence of retrieved evidence.
WORKING joins the default layer set only when session_id is provided — a
session-less pack has nothing to put there, and an always-empty layer with a
warning would be noise. It renders the session's most recent raw items in
chronological order; when the budget is tight the oldest context is dropped
first, because what happened last is what constrains the next step. Requesting
WORKING explicitly without a session_id returns the layer empty with a note.
POST /api/skills/sources— Register or update a skill source (gitorlocal) (scope:skills:rw)GET /api/skills/sources— List skill sources (scope:skills:r)GET /api/skills/sources/:id/revisions— List source revisions (scope:skills:r)POST /api/skills/sources/:id/snapshots— Push a local skill package snapshot (scope:skills:rw)POST /api/skills/sources/:id/sync— Pull and sync a public GitHub/GitLab skill package (scope:skills:rw)POST /api/skills/compile— Compile/recompile one version, or backfill all active versions (scope:skills:rw)GET /api/skills/catalog?query=&limit=&offset=— List active L0 cards with stable pagination (scope:skills:r)GET /api/skills/versions/:id/instructions— Load L1SKILL.mdinstructions (scope:skills:r)GET /api/skills/versions/:id/resources?limit=&offset=— Load the paginated L2 resource manifest without content (scope:skills:r)GET /api/skills/versions/:id/resources/:file_id— Load one selected text resource (scope:skills:r)POST /api/skills/index— Index compiled active skill cards into retrieval (scope:skills:rw)POST /api/skills/versions/:id/quality-runs— Run offline deterministic quality checks with an optionalbaseline_version_id(scope:skills:rw)GET /api/skills/versions/:id/quality-runs?limit=&offset=— List report-free quality run summaries with stable pagination (scope:skills:r)GET /api/skills/quality-runs/:run_id— Get one account-scoped full quality report (scope:skills:r)POST /api/skills/search— Semantic search across indexed L0 cards;include_contentremains supported (scope:skills:r)GET /api/skills/versions/active?skill_name=— Get active skill version (scope:skills:r)GET /api/skills/versions/:id/files— List internal package file records (compatibility endpoint, scope:skills:r)POST /api/skills/versions/:id/activate— Activate a skill version (scope:skills:rw)
Successful local/Git ingest, direct publish, and activation attempt to refresh the compiled artifact after the package transaction commits. A compiler failure never rolls back package identity. Catalog reads return a basic card when an artifact is missing; call /api/skills/compile to backfill it. Instruction and resource-content responses use Cache-Control: private, no-store.
Migration 000018 replaces any pre-compiler Skill retrieval document that may contain full
instructions with a bounded basic L0 card, marks its stale vector as non-hydratable, and keeps
a safe PostgreSQL lexical fallback. Run POST /api/skills/compile and then
POST /api/skills/index after upgrading to rebuild current artifacts and embeddings.
include_content=true remains compatible by loading the selected L1 instructions from
PostgreSQL after search; instructions are never stored in the retrieval index.
Lexical retrieval matches a bigram projection (retrieval_documents.lexical_text, migration
000023) instead of raw text, because PostgreSQL's simple configuration does not segment CJK
script and would otherwise collapse a whole Chinese sentence into one unmatchable token. CJK runs
become overlapping character bigrams while ASCII runs stay whole lowercased words, so identifiers
keep exact matching. Index side and query side share one Go implementation
(retrieval.LexicalProjection / retrieval.LexicalTSQuery); that shared rule is the correctness
condition of the scheme. Rows written outside the Go write path (for example by an earlier
migration's direct UPDATE) have an empty projection and are invisible to the lexical leg — repair
them with POST /api/admin/retrieval/lexical/rebuild, which recomputes the projection from stored
title and content without re-embedding anything or calling Qdrant.
Phase 4 quality runs are synchronous, offline, and side-effect free except for their own audit row.
Each run reads its version, optional same-skill baseline, compiled artifacts, files, cutoff, and latest
version-bound logs from one read-only repeatable-read snapshot. skill_log_add accepts an optional immutable skill_version_id; when present it must belong to the
same account and skill_name, and the server canonicalizes the legacy version label. Logs without
that ID remain unassigned and are excluded from quality telemetry. Reports use at most the latest
200 logs before a fixed cutoff, require 20 triggered samples for suggestions, and contain counts,
fingerprints, and log IDs rather than instruction, resource, or log bodies. Direct body-only versions
validate sha256(content) == content_hash == package_hash. Each check carries a deterministic
blocker, error, or warning severity. Release comparison reports both package_hash_changed
and resource_manifest_changed; the latter compares static resource behavior metadata such as
kind, MIME type, indexability, and text availability without changing the Phase 1 package hash.
Quality-run list responses contain summaries only; the detail endpoint loads the full report.
Migration 000019 keeps audit targets append-only: deleting a target version referenced by a
version-bound log or quality run is rejected by the default NO ACTION foreign keys. Account
deletion still cascades quality runs, while deleting a baseline version only clears the
baseline_version_id column.
Skill sources keep registry metadata and deterministic file snapshots. Git sources are registered as server-pull sources; local sources are client-push sources where the client sends a package snapshot. SKILL.md remains the compatibility content for skill_versions, while additional files are tracked as revision file metadata and indexable text snapshots.
# Register a local source.
curl -X POST http://localhost:26001/api/skills/sources \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "personal-domain-web",
"type": "local",
"repository_url": "file:///Users/me/.agents/skills",
"package_path": "domain-web"
}'
# Push a local snapshot. Omit sha256/package_hash to let the server derive them
# from supplied text content.
curl -X POST http://localhost:26001/api/skills/sources/<source_id>/snapshots \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"activate": true,
"index": true,
"files": [
{
"path": "SKILL.md",
"mime_type": "text/markdown",
"content": "---\nname: domain-web\ndescription: Build web services\n---\n\n# Instructions\n..."
}
]
}'Knowledge Registry K1: knowledge sources with immutable revisions and document
snapshots. K1 covers source registration, Git/local ingest, canonical package
identity, and account-scoped document reads. K2 adds the K0 catalog,
deterministic Markdown chunking, a document link graph, and account-scoped
hybrid retrieval. K3.1–K3.7 add the wiki compiler: profile versioning, immutable
wiki builds, deterministic checks as the only activation gate, build diff and
rollback, a leased compile queue with bounded retries and cost accounting,
incremental compilation that recompiles only the pages a source change touches, wiki
pages in their own retrieval namespace as the entry layer above raw documents, and
advisory lint over a serving wiki — orphan pages, citations whose source was removed or
rewritten, the pages resting on those, recorded contradictions, and uncited documents.
Lint blocks nothing: a rule that could stop a wiki from serving belongs in the check gate.
K3.8 adds faithfulness review: a model from a different provider than the compiler checks
each page's claims against the raw documents it cites, judging against the source text
rather than the compiler's own excerpts. A same-model reviewer is refused rather than run,
because a model cannot find the mistakes its own priors produced. Review never gates, and
its status distinguishes "clean" from "partial" so a capped review never claims coverage it
did not have. K3.9 adds validation signals and attribution: an agent reports what happened after using an
answer, and the platform places the fault on one of four layers — or records it as
unattributed, which it usually is. A cause it cannot establish is never invented, because a
wrong one sends someone to fix the wrong layer and then the real fix looks like it failed.
Signals record whether they were reported or derived, since an agent that never reports is
indistinguishable from one that had no trouble. Still planned: proposal generation and
disposition, which waits on a process decision rather than a technical one (see
docs/knowledge-wiki-compiler-k3-v0.1.md §13 for exactly what is and is not
implemented).
Skill and knowledge packages can be organised by domain inside a repository
(platform/retrieval, product/faq). The owning domain is derived from the
first package_path segment and stored on the source; a single-segment path has
no domain, since a flat package is not organised by domain. The source name is
derived from every path segment (platform/retrieval → platform-retrieval)
because knowledge sources are unique per (account_id, name), so a
basename-derived name would let same-named packages under different domains
overwrite each other. Domain is never accepted from the client.
Registration upserts by name, so a second registration whose package_path
derives the same name but points at a different package is rejected rather than
silently repointing the existing source. Register such a package under an
explicit distinct name.
POST /api/knowledge/sources— Register or upsert a knowledge source byname(gitorlocal) (scope:knowledge:rw)GET /api/knowledge/sources— List knowledge sources (scope:knowledge:r)GET /api/knowledge/sources/:id/revisions— List immutable source revisions (scope:knowledge:r)POST /api/knowledge/sources/:id/sync— Pull and ingest a public GitHub/GitLab knowledge package (scope:knowledge:rw)POST /api/knowledge/sources/:id/snapshots— Push a local knowledge package snapshot (scope:knowledge:rw)GET /api/knowledge/revisions/:id/documents?limit=&offset=— Paginated document metadata without content bodies (scope:knowledge:r)GET /api/knowledge/revisions/:id/documents/:doc_id— One document including its text content snapshot, servedCache-Control: private, no-store(scope:knowledge:r)GET /api/knowledge/catalog?query=&domain=&limit=&offset=— K0 collection cards for sources with an active revision: manifest metadata (name/description/profile/language/citation_policy), declaredcapabilitiesandlanguages(the match surface Skill knowledge contracts discover against; the older singularlanguagefolds into the plural), owning domain, document count, package hash, and chunk index status; stable pagination with ILIKE-style name/description filtering plus exactdomainfiltering. The response also carriesdomains, the account's domain roster with collection counts, so a domain can be chosen before reading individual cards (scope:knowledge:r)POST /api/knowledge/index— Chunk-index active revisions (bodysource_idoptional; empty indexes every active source) into the account-scopedknowledgeretrieval namespace and rebuild the document link graph (scope:knowledge:rw)POST /api/knowledge/search— Hybrid lexical + semantic search over indexed chunks (bodyquery/top_k/domain/source_ids/include_content;domainresolves to that domain's sources and intersects withsource_ids, so it can only narrow the search); hits carry document/source/revision provenance, heading path, score, snippet, and 1-hop link neighbors (metadata only, capped at 16); the snippet is the first 240 runes of the chunk body (a chunk shorter than that is fully visible in its snippet), and the full chunk body is returned only withinclude_content=true; servedCache-Control: private, no-store(scope:knowledge:r)GET /api/knowledge/documents/:doc_id/links?limit=&offset=— Both directions of one document's package-internal links: outgoing links keep the target path (with a NULL document ID when dangling), incoming links carry the linking document's path (scope:knowledge:r)POST /api/knowledge/discover— Resolve a skill version's compiled knowledge contract against the account's K0 catalog (bodyskill_version_id, optionalrequirement_id). Per requirement it returns ranked candidate collections with the matched capabilities/languages/domain spelled out, the contract's retrieval budgets echoed, and a classified status —matched,ambiguous(more candidates than the contract'smax_knowledge_bases, with the contract's ownon_ambiguousguidance),no_metadata_match(the note distinguishes "nothing fits" from "no manifest declares capabilities at all"),no_authorized_knowledge,pinned_resolved, orpinned_missing. Failure classes are never flattened into an empty list. The response carries a deterministic discoveryfingerprintover the contract identity and the catalog state, the future anchor for KnowledgeResolutionRun.scoped_discovercontracts are refused with501rather than silently widened, because workspaces/tags/approved state do not exist in the knowledge domain yet. Discovery reads the compiled contract from the skills domain, so the route requires bothknowledge:randskills:r; servedCache-Control: private, no-storePOST /api/knowledge/resolutions— Freeze one runtime resolution as an append-only KnowledgeResolutionRun: which discovery it followed (discovery_fingerprint+discovery_status), which bases the execution selected, what it retrieved and cited (references only, never bodies), plus the agent'sselection_reasonandconfidence. The trust boundary is per field:contract_identityis filled by the server from the compiled contract and therequirement_idmust exist in it, everyselectedbase (and its optionalrevision_id/build_id) is verified against the account, whilecandidates/retrieved/citationsare bounded client-reported echoes tied to a served discovery by the fingerprint.selectedmay be empty — "discovery found nothing and the skill proceeded per its fallback" is exactly the run worth recording. Optionalidempotency_key: a byte-identical replay returns the original row with200, a disagreeing replay is409. Deleting a skill version referenced by a run is rejected (audit targets stay append-only); account deletion cascades. Requiresknowledge:rwandskills:rGET /api/knowledge/resolutions?skill_version_id=&session_id=&source_id=&limit=&offset=— Resolution run summaries, newest first, with selected/retrieved/citation counts instead of the evidence arrays.source_idfilters to runs whose selected set contains that base — "which executions rested on this knowledge base" is the audit question this table answers (scope:knowledge:r)GET /api/knowledge/resolutions/:run_id— One run in full: contract identity, discovery anchor, candidates, verified selections, retrieved references and citations; servedCache-Control: private, no-store(scope:knowledge:r)
Wiki pages live in their own retrieval namespace, knowledge_wiki, and are the first level
of a two-level query: find the synthesised page, then follow its citations down to the raw
documents for evidence. The namespace is separate rather than replacing the raw one,
because a synthesis and the documents it was synthesised from must not compete in a single
ranking — the synthesis usually wins and the evidence it rests on disappears.
Search filters on each source's active build, not on whatever the index happens to
hold. Builds are immutable and retained, so a broader index would serve pages from a wiki
that was rolled back while every read API served the restored one. A stale index therefore
returns fewer hits rather than wrong ones, and the gap between active_build_id and
indexed_build_id is reported — a lagging index otherwise looks exactly like a wiki with
nothing to say.
Hits collapse to one page each with a matched_chunks count, since a long page occupies
several top slots once chunked. Citations and typed links travel with every hit, read from
the database rather than the index: the page is model-generated, so a claim is only
checkable by following it. Pages carried over by an incremental build report
derived_from_build_id, so a reader can tell which model run wrote the text.
wiki/log.md is deliberately not indexed — it is a transcript of compiling, not knowledge
about the domain, and indexing it would let an agent cite page_written wiki/x.md as a
fact. wiki/index.md is indexed, since navigating from it is the first move of the
two-level query.
POST /api/knowledge/wiki/search— Hybrid search over compiled wiki pages (bodyquery/top_k/domain/source_ids/include_content); servedCache-Control: private, no-store(scope:knowledge:r)GET /api/knowledge/wiki/index?source_id=— Per source, which build is active and which one the index reflects, plus astalecount (scope:knowledge:r)POST /api/knowledge/wiki/index— Index the active build of one source (bodysource_id) or of every source with an active build; removes rows from earlier builds (scope:knowledge:rw)
The wiki is compiled on the platform, not written back to Git by an agent: the agent runs on the client and is not controllable, so making it the author would make wiki quality depend on whichever client happened to run. Git holds only the raw sources; the wiki is a platform artifact.
A build is not reproducible — the same sources compiled twice yield different text. Everything else follows from that: builds are immutable and retained, provenance is complete (raw revision and package hash, profile version, compiler version, prompt version, model, reviewer independence), reuse keys on input identity rather than a content hash, and content hashes are used only for diffing.
Activation is automatic. The knowledge base belongs to the user while the
quality standard belongs to the platform, so asking a user to approve compiler
output is an identity mismatch that yields either rubber stamping or a wiki that
never updates. What makes that safe is check: deterministic, machine-decidable
invariants (citations resolve to real documents, links close inside the build,
page kinds and link types are allowed by the profile, the index covers every page,
paths are unique, page count has not drifted implausibly from the parent). check
is the only gate — an LLM reviewer is deliberately kept off the blocking path,
because an unreproducible verdict there means a retried build passes or fails at
random. A build failing check writes no pages at all: half a wiki is worse than
none, since an agent cannot tell pages are missing.
wiki/index.md and wiki/log.md are generated by the platform, not the model.
If the model wrote the index, check's coverage rule would be testing the model's
diligence instead of the build's completeness. Those two paths are reserved; a
model page landing there is dropped and recorded as a page_rejected event.
POST /api/knowledge/compile— Queue a compilation of the source's active raw revision. Returns202with aqueuedbuild; nothing has been compiled when it returns. Bodysource_id(required),mode(fullorincremental),force(recompile despite a matching input identity),activate(default true, carried on the job because the caller is long gone when a worker runs). Returns200instead when an existing build already matched the input identity, in which case nothing was queued. Warnings include a standing one when reviewer independence is notcross_provider. Returns 501 when no compiler model is configured, which is an operator gap rather than a failed build (scope:knowledge:rw)GET /api/knowledge/queue— Compile queue for this account: builds waiting, running, waiting on a retry backoff, and the age of the oldest waiting build. Without this, queue wait and a stuck worker look identical from outside (scope:knowledge:r)GET /api/knowledge/builds?source_id=&limit=&offset=— Build history, newest first, withis_activederived from the source pointer (scope:knowledge:r)GET /api/knowledge/builds/:build_id— One build with full provenance, check verdict and failures, and token spend (scope:knowledge:r)GET /api/knowledge/builds/:build_id/pages— Page metadata without bodies (scope:knowledge:r)GET /api/knowledge/builds/:build_id/pages/*path— One page with its body, citations and both inbound and outbound typed links; servedCache-Control: private, no-store(scope:knowledge:r)GET /api/knowledge/builds/:build_id/diff?from=— Compare two builds by page path and content hash;fromdefaults to the previous succeeded build of the same source (scope:knowledge:r)GET /api/knowledge/builds/:build_id/events— The ordered build log, rendered aswiki/log.mdon succeeded builds (scope:knowledge:r)POST /api/knowledge/builds/:build_id/activate— Point the source's wiki at this build. This is also the rollback operation; the response carriesprevious_build_idso a rollback can be undone. Returns 409 for a build that did not succeed or did not pass check (scope:knowledge:rw)
mode=incremental diffs the raw sources against the previous succeeded build,
recompiles only the pages those changes touch, and carries the rest over. It exists
because full compilation emits the whole wiki in one model reply, which caps corpus
size hard — the output budget has already been raised from 4096 to 16384 to 32768 and
there is no further headroom. It is the way past that ceiling, not a cost optimisation.
The impact set is computed from the database, never asked of the model: a page is recompiled when it cites a document that changed or disappeared, plus one hop of pages linking to those. The second hop is not about staleness — the recompile may drop or rename its target, and a carried-over page pointing at a page that no longer exists is a dangling link that check refuses. Closure stops at one hop on purpose, because full transitive closure converges on the whole wiki for any well-connected knowledge base.
Two check rules apply only to incremental builds. incremental_coverage fails a build
that left a scheduled page neither rewritten nor deleted: keeping the old text would
leave a claim its source no longer supports, and no structural rule can see that
because the citation path still resolves. incremental_scope fails a build that
changed a page outside its plan — the compiler is handed every page path so it can
link freely, which also lets it rewrite something nobody asked about.
Incremental is refused, not downgraded, in two cases: when there is no previous build to diff against, and when the previous build came from a different model, prompt, compiler or profile. In the second case the raw diff would be empty while every page still needed rewriting, and carrying pages forward would stamp this build's provenance onto text the recorded model never produced. Repeating the request when the sources have not moved hands back the existing build rather than queueing a no-op, because every incremental build becomes the parent of the next one and identity would otherwise never repeat.
Measured on a four-document knowledge base with one document changed: output tokens fell from 8707 to 4841, with three of five pages carried over. Input tokens did not move — at that size the incremental prompt carries about as much as simply sending everything. Input savings scale with corpus size; the output ceiling is what incremental is for.
Compilation runs in a leased queue, not in the request: a compile takes 200-400 seconds against a reasoning model, past any sane client default timeout, and a caller that gave up used to lose the work. Enqueue returns in milliseconds.
The queue is the build table rather than a broker. The build row already exists and already needs a status, so the lease lives on it — a broker would add a second place that answers "is this build running", and the two can disagree after a crash, which is when the answer matters. Workers are replicas of this same binary; leases keep two of them from compiling the same build.
Recovery keys on lease expiry, not worker liveness: a partitioned worker looks
identical from the database and has the same consequence, which is that nobody is
making progress. attempt counts claims rather than failures, so a build that
silently kills every worker it touches retires instead of cycling forever. Graceful
shutdown yields its builds and refunds the attempt, so a rolling deploy neither
stalls in-flight work for a lease period nor spends its retry budget.
What gets retried is the substance:
| Failure | Retried | Why |
|---|---|---|
| Transport failure, dropped connection, truncated body | yes | A four-minute non-streaming connection does get dropped |
| 408, 429, 5xx | yes | The provider is explicitly saying "later" |
| Other 4xx | no | Would deterministically recur |
Reply truncated by max_tokens |
no | The same budget truncates the same way; a retry only doubles the bill |
| check failure | no | Recompiling until the dice fall right is relaxing the gate by repetition |
| Unknown error | no | A compile is expensive, so the default has to be the cheap answer |
Backoff grows exponentially from 30s and caps at 600s. Cost is recorded per attempt and accumulated, since a build that failed twice before succeeding cost all three calls; prices default to zero because an invented rate would put an authoritative-looking wrong number in the accounting.
| Variable | Default | Notes |
|---|---|---|
WIKI_WORKER_CONCURRENCY |
2 |
The binding constraint is provider rate limits and cost, not local CPU |
WIKI_WORKER_POLL_SECONDS |
5 |
Polling, not LISTEN/NOTIFY: a wakeup would never fire for a build whose worker died |
WIKI_WORKER_HEARTBEAT_SECONDS |
15 |
Must stay well under the lease, or a brief database hiccup costs a live worker its lease |
WIKI_WORKER_RETRY_BACKOFF_SECONDS |
30 |
Base for exponential backoff |
WIKI_WORKER_MAX_RETRY_BACKOFF_SECONDS |
600 |
Without a cap a build disappears for hours over a blip |
COMPILER_INPUT_MICROS_PER_1K_TOKENS |
0 |
Zero means "price unknown", not "free" |
COMPILER_OUTPUT_MICROS_PER_1K_TOKENS |
0 |
The lease duration is derived from COMPILER_TIMEOUT_SECONDS rather than
configured separately: a lease shorter than a compile guarantees that a perfectly
healthy worker gets its build stolen mid-compile, and then two workers write the
same wiki.
Model configuration (both roles fall back to EMBEDDING_BASE_URL /
EMBEDDING_API_KEY, which is how a single-credential deployment starts):
| Variable | Default | Notes |
|---|---|---|
COMPILER_BASE_URL / COMPILER_API_KEY |
embedding endpoint and key | OpenAI-compatible /chat/completions |
COMPILER_MODEL |
qwen3.7-plus |
|
COMPILER_TEMPERATURE |
0.2 |
As reproducible as an LLM allows |
COMPILER_MAX_TOKENS |
32768 |
A reasoning model spends this budget on its own thinking before writing, so it must be a large multiple of the expected wiki size; a truncated reply fails the build rather than yielding a partial wiki |
COMPILER_TIMEOUT_SECONDS |
900 |
|
REVIEWER_BASE_URL / REVIEWER_API_KEY |
embedding endpoint and key | Point at another vendor for real independence |
REVIEWER_MODEL |
qwen-max |
Different from the compiler by default, so review is at least not literal self-review. The local deployment points this at DeepSeek deepseek-v4-pro, which is what makes independence cross_provider |
REVIEWER_TEMPERATURE |
0 |
The same claim and source should get the same verdict |
Every compile also warns, unconditionally, that review is not implemented yet and
review_status stays skipped — check is the only verification a build receives.
That warning is deliberately not conditional on independence: it used to be, and
pointing the reviewer at a second vendor would then have removed the only signal
saying nothing had been reviewed. Better configuration must not reduce what the
caller is told about what was verified.
Reviewer independence is classified per build as cross_provider,
same_provider, same_model or unavailable, and stored on the build. A
heterogeneous reviewer reduces correlated blind spots but does not create
impartiality — mainstream models share training data. The genuinely independent
anchors are check (mechanical) and human validation, not another model.
Every knowledge package must carry a root KNOWLEDGE.yaml manifest
(name required; optional description, profile, language,
capabilities/languages declaration lists (max 16 entries each — they are
what Skill knowledge contracts match against, and an over-broad declaration
stops discriminating), include/exclude glob lists, and
citation_policy: required|optional).
The manifest's include/exclude rules select which files become documents;
KNOWLEDGE.yaml itself always participates in the package identity hash but
is never returned as a document. Text files keep a stored content snapshot;
binary files contribute only hash and size to identity. Ingest is
transactional and idempotent: replaying the same package hash returns the
existing revision and re-targets the source's active_revision_id; a failed
sync marks the source error without leaving a partial revision.
# Register a Git knowledge source.
curl -X POST http://localhost:26001/api/knowledge/sources \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "product-support",
"type": "git",
"repository_url": "https://github.com/acme/knowledge",
"package_path": "product-support"
}'
# Sync a ref to an immutable commit and ingest the package.
curl -X POST http://localhost:26001/api/knowledge/sources/<source_id>/sync \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{"ref": "main"}'# Login to get token
curl -X POST http://localhost:26001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "password123"}'
# Use token
curl -H "Authorization: Bearer <jwt_token>" http://localhost:26001/api/todos# Via x-api-key header
curl -H "x-api-key: ak_xxxx" http://localhost:26001/api/todos
# Via Authorization header
curl -H "Authorization: Bearer ak_xxxx" http://localhost:26001/api/todos# Create a read-only key for todos
curl -X POST http://localhost:26001/api/auth/apikeys \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{"name": "readonly-agent", "scopes": ["todos:r", "notes:r"]}'
# This key can list/get but cannot create/update/delete
curl -H "x-api-key: ak_xxxx" http://localhost:26001/api/todos # ✓ 200
curl -X POST -H "x-api-key: ak_xxxx" http://localhost:26001/api/todos # ✗ 403 insufficient scopeEach business module mounts its own Streamable HTTP MCP server, rather than a single aggregated endpoint. This keeps tool lists focused and lets an agent integration opt into only the modules it needs.
| Endpoint | Tools |
|---|---|
POST /mcp/todos |
todo_create, todo_get, todo_list, todo_update, todo_delete, todo_search |
POST /mcp/notes |
note_create, note_get, note_list, note_update, note_append, note_delete, note_search |
POST /mcp/reports |
report_create, report_get, report_list, report_list_sources, report_update, report_delete |
POST /mcp/bookmarks |
bookmark_create, bookmark_get, bookmark_list, bookmark_update, bookmark_delete |
POST /mcp/expenses |
expense_create, expense_get, expense_list, expense_summary, expense_update, expense_delete |
POST /mcp/memory |
memory_record, memory_store, memory_search, memory_get, memory_timeline, memory_attribution, memory_supersede, memory_feedback, memory_feedback_list, memory_checkpoint_save, memory_resume, memory_working_session_create, memory_working_session_list, memory_working_session_get, memory_working_session_update, memory_working_session_delete, memory_working_append, memory_working_replay, memory_working_item_delete |
POST /mcp/skills |
skill_log_add, skill_logs_list, skill_version_publish, skill_version_get_active, skill_source_sync, skill_stats, skill_signals, skill_search, skill_index_active, skill_catalog_list, skill_compile, skill_version_instructions, skill_version_resources, skill_resource_get, skill_quality_run, skill_quality_get |
POST /mcp/context |
context_pack |
POST /mcp/knowledge |
knowledge_sources_list, knowledge_source_sync, knowledge_documents_list, knowledge_document_get, knowledge_catalog_list, knowledge_search, knowledge_discover, knowledge_resolution_record, knowledge_resolutions_list, knowledge_resolution_get, knowledge_index_active, knowledge_document_links, knowledge_compile, knowledge_builds_list, knowledge_build_get, knowledge_build_pages, knowledge_page_get, knowledge_build_diff, knowledge_build_events, knowledge_build_activate, knowledge_queue_stats, knowledge_wiki_search, knowledge_wiki_index, knowledge_wiki_status |
Authenticate with a valid API key via X-Api-Key header, Authorization: Bearer ak_xxx,
or ?api_key=ak_xxx query param. MCP tool calls enforce the same API key scopes as REST
(e.g. todo_create requires todos:rw, todo_list requires todos:r).
Point the client at the module endpoint it needs, e.g. for todos only:
{
"mcpServers": {
"agentmate-todos": {
"url": "http://localhost:26001/mcp/todos",
"headers": { "X-Api-Key": "ak_xxxx" }
}
}
}To use multiple modules, add one entry per endpoint (each is an independent MCP server with its own tool list):
{
"mcpServers": {
"agentmate-todos": {
"url": "http://localhost:26001/mcp/todos",
"headers": { "X-Api-Key": "ak_xxxx" }
},
"agentmate-notes": {
"url": "http://localhost:26001/mcp/notes",
"headers": { "X-Api-Key": "ak_xxxx" }
},
"agentmate-skills": {
"url": "http://localhost:26001/mcp/skills",
"headers": { "X-Api-Key": "ak_xxxx" }
}
}
}These are Streamable HTTP MCP servers, so a session is established first and its
ID travels on every later request. Calling tools/list cold returns
Invalid session ID — that is the protocol working, not a broken endpoint.
BASE=http://localhost:26001
KEY=ak_xxxx
# 1. initialize — the Mcp-Session-Id response header is the session
SID=$(curl -s -D - -o /dev/null -X POST $BASE/mcp/todos \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-Api-Key: $KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
| grep -i '^mcp-session-id' | tr -d '\r' | cut -d' ' -f2)
# 2. list tools with the session
curl -s -X POST $BASE/mcp/todos \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-Api-Key: $KEY" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'A real MCP client does this handshake itself; the manual form is only for verifying connectivity and scopes. For agent integration start from Agent Integration Guide.
API Key → OAuth Device Flow → Agent DID
- API Key (current) — Simple key-based auth with scopes for agent integration
- OAuth Device Flow — Enable headless agents to authenticate via user-approved device codes
- Agent DID — Decentralized identity for agents, enabling cross-platform trust and verifiable agent credentials