[{"localId": "https://reference-architecture.ai/posts/organisational-roles-as-agent-contracts/", "https://atomicdata.dev/properties/name": "Organisational Roles as Contracts for AI Agents", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/organisational-roles-as-agent-contracts/", "https://atomicdata.dev/properties/description": "

Abstract

\n

An organisational AI agent needs defined responsibilities, relevant knowledge and bounded authority. This paper proposes roles as contracts for all three. A Project Manager plans delivery; a Product Manager decomposes product outcomes into features. Each uses a governed role-based knowledge graph. Agent services realise role capabilities through tools whose actions require contextual permission. Verification and validation require compatible concepts, relationships and operating conditions across the input and evaluation criteria, with explicit mappings where views differ. A cross-check against ISO/IEC/IEEE 42010:2022 and 29148:2018 informs these distinctions without claiming conformity. Terraphim provides graph retrieval and checklists; Terraph connects concept matching to proof-gated verbs; ADF configurations define responsibilities and artefact templates. The proposed integration would allow a Project Manager agent to create a checked plan from an approved template while reserving production deployment for a separately authorised role.

\n

\n

Continuing the argument on evolving AI risk

\n

This paper continues the argument developed by David Hunt and Alexander Mikhalev in AI Risk is evolving, every day., published on 31 October 2026. Their starting concern is that an agent can pursue a legitimate objective through unauthorised methods. They connect oversight of business activity with cybersecurity and knowledge integrity, and argue that changes to agents and their surroundings can invalidate earlier assurance.

\n

Supervision needs an inspectable mandate: the organisational role, the capability being exercised, the service providing it, and the tool action requested against a particular resource. Role-based knowledge graphs provide the relevant concepts and relationships; an independently enforced permission decision determines whether the action may proceed. A Project Manager can prepare a delivery plan using approved templates while production execution remains assigned to a separately authorised role. The sections below distinguish existing components from the integration still required.

\n

A successful check needs criteria that apply to the input's meaning, obligations and operating context. Matching words cannot establish that a plan preserves a prohibition, uses the correct environment, or retains an acceptance threshold. The semantic compatibility contract developed here connects requirements, criteria and evidence through a versioned baseline and explicit mappings. Changes to that baseline trigger reassessment of affected results. The standards cross-check helps define the responsibilities and evaluation claims; it does not establish that the supervisory system has been implemented.

\n

The common concepts connect the earlier article's concerns to the mechanisms examined in this paper:

\n\n\n\n\n\n\n\n\n
Shared conceptEngineering continuation in this paper
Mandate and justified authorityTrace the assigned role through capability, service, tool and action to a contextual permission decision.
Knowledge integrity and contextUse governed concept identities, relationships and requirement baselines to preserve meaning across inputs, outputs and criteria.
Independent supervisionSeparate the agent's proposal from the gateway's permission decision and the evidence used to assess its result.
Continued validity of assuranceBind findings to the evaluated artefact, criteria and context; reassess affected claims when relevant versions change.
Learning and authority to changeGovern changes to role graphs, templates and policies separately from permission to perform the task.
Prediction and operational evidenceKeep illustrative scenarios, expected outcomes and recorded executions distinguishable; validate predictions before relying on them.
Consequences across organisational scalesMake resource and project scope explicit, and identify where wider consequences require escalation beyond the acting role.
\n

This mapping is our architectural synthesis. The examples below illustrate individual mechanisms; connecting them across systems remains engineering work.

\n

The organisational unit of agency

\n

“Planning agent” describes a computational activity. It does not identify who owns the outcome, which planning conventions apply, or what authority accompanies the work. The same ambiguity affects “research agent”, “review agent” and “execution agent”. These labels can be useful implementation names, but they leave organisational meaning unresolved.

\n

A Project Manager plans delivery against agreed scope, dependencies, resources and milestones. A Product Manager decomposes desired outcomes into features, evaluates value and defines acceptance criteria. Both plan, but they plan different objects, draw on different evidence, and answer to different accountabilities. An organisation should therefore delegate to an agent acting in a defined role, with planning as one of that role’s capabilities.

\n

The role contract can remain stable when an agent’s model, prompt, framework or supplier changes. A replacement implementation must demonstrate the required service behaviour and operate within the same authority boundary. Governed role definitions and capability contracts give the organisation a basis for evaluating that replacement.

\n

The central proposal is a traceable relationship:

\n
\n

Organisation → role → capability → service → tool → action → contextual permission.

\n
\n

This sequence is an explanation path, not an assertion that authority flows automatically through every edge. The organisation assigns responsibilities to roles; an agent principal is separately authorised to enact a role. Its implementation supplies services. Tools make actions technically available. Policy determines whether an exact requested action is permitted in its current context.

\n

The organisational semantic layer

\n

The organisational semantic layer describes concepts, relationships, templates, standards and responsibilities using explicit identifiers and versioned definitions. Here, conceptual space means this governed system of organisational meaning. The proposal does not require a fully implemented geometric model of human conceptual spaces.

\n\n\n\n\n\n\n\n\n\n
ElementOrganisational meaningPlanning example
RoleA position with responsibilities and bounded authorityProject Manager
CapabilityAn ability the organisation needs to achieve an outcomeDelivery planning
ServiceAn accessible behaviour provided by an implementationProduce a delivery plan
Agent implementationSoftware that provides the service while enacting an assigned roleProject planning agent instance
ToolA concrete interface through which work is performedTemplate reader or document writer
ActionA specific operation on an identified resourceCreate a draft plan in project workspace
PermissionA policy decision over principal, role, action, resource and contextDraft creation allowed for the assigned project
EvidenceRecords supporting a decision and its observed resultTemplate version, checks and execution receipt
\n

This distinction between capabilities and services is compatible with enterprise architecture’s separation of organisational behaviour from its realisation and exposed services. It is used here as an architectural vocabulary, without claiming formal compliance with an ArchiMate model. Open Group ArchiMate 101

\n

A capability describes what the organisation can accomplish. It is not a permission token. Likewise, an implementation’s demonstrated competence does not grant authority. A model may understand deployment instructions while having no right to invoke a deployment endpoint.

\n

Role capabilities contribute to broader organisational capabilities. For example, an organisation’s Product Delivery capability can depend on Product Discovery performed by the Product Manager, Delivery Planning performed by the Project Manager, and Release Assurance performed by the Release Manager. The graph records these contribution relationships explicitly. Separate agent services realise the role capabilities; their combined performance supports the organisational outcome without granting each service the authority of every contributing role.

\n

Roles should also be sufficiently specific to support ownership. An organisation can assign a person or agent multiple roles, but each action should use an explicit active role and bounded assignment. Combining role knowledge for a discussion must not silently combine execution privileges.

\n\n\n\n\n
RoleDedicated conceptual viewPrincipal serviceExample boundary
Product ManagerOutcomes, user needs, features, value hypotheses and acceptance criteriaProduce an evidence-linked feature breakdownCannot approve production deployment
Project ManagerScope, dependencies, resources, risks and delivery milestonesProduce a delivery plan from an approved templateCan write a project draft; cannot deploy it
Release ManagerRelease readiness, change records, environments and rollback proceduresCoordinate an authorised releaseProduction action requires a separate assignment and satisfied release policy
\n

These are example contracts, not universal job descriptions. The organisation owns the actual definitions and decides where responsibilities overlap.

\n

Dedicated knowledge without isolated vocabularies

\n

Each role receives a governed projection of organisational knowledge. This can select which concepts and relationships are available to the role, as well as how retrieval ranks them. Roles can share an underlying graph while retaining different conceptual views.

\n

Shared concepts retain shared identifiers. For example, a feature’s acceptance criteria connect product intent with delivery planning and release verification. Role-specific relationships express what matters for each responsibility: a Product Manager connects a criterion to user value; a Project Manager connects it to delivery evidence and dependencies; a Release Manager connects it to a release decision. The shared identity supports handoffs without making the roles interchangeable.

\n

Role-owned overlays can define local vocabulary, specialised relationships, approved templates and checklist requirements. Their provenance should identify the owner, source, revision and approval state. Changes to the shared definition of “accepted” require different governance from adding a convenient synonym to a role’s retrieval vocabulary.

\n

Graph projections must also respect access controls. Knowing that another team’s confidential plan exists does not authorise retrieving its contents. A role graph is a semantic view; a separately authenticated assignment and resource policy determine what information the agent may actually read.

\n

The graph is the inspectable, or white-box, component of this architecture. A reviewer can examine its concepts and relationships, the embeddings derived from them, and the definitions that shaped retrieval. This visibility neither makes the language model transparent nor establishes that the graph's contents are correct.

\n

Architecture description and requirements engineering

\n

ISO/IEC/IEEE 42010:2022 concerns architecture descriptions, including viewpoints and model kinds. It distinguishes an architecture from its description and does not prescribe an agent implementation or execution policy. In that standard, ADF means architecture description framework; the ADF agent orchestration system discussed here is a different use of the acronym. ISO/IEC/IEEE 42010:2022

\n

For this architecture, stakeholder concerns, viewpoint conventions and resulting views must remain distinguishable from organisational roles. A viewpoint establishes conventions for constructing and interpreting views that address concerns. ISO 24641:2023 preview

\n

A role can serve several stakeholder concerns and use several viewpoints. A role graph becomes useful architectural material when its concepts, relationships, conventions and intended questions are explicit. It is not automatically a viewpoint; a project plan is not automatically an architecture view. Terraph's language constructs likewise do not establish that it is a conforming architecture description language. The standards do not assign the paper's Project Manager, Product Manager or Release Manager jobs their permissions.

\n

ISO/IEC/IEEE 29148:2018 addresses requirements engineering and its information across the life cycle. Its terminology distinguishes checks on requirements from checks on the realised product or system. The following four questions must not be collapsed into a single “validation passed” label. ISO/IEC/IEEE 29148:2018 official preview

\n\n\n\n\n\n
ActivityQuestion and object
Requirements verificationAre the requirement statements and set well formed?
Requirements validationDo those requirements express the intended stakeholder needs?
Product or system verificationDoes the realised item fulfil its specified requirements?
Product or system validationDoes it support its intended application and use?
\n

Each decision needs the applicable activity and evidence; an individual tool check need not perform all four. A reviewed input baseline is particularly important: an output checklist cannot retrospectively establish what the stakeholder meant.

\n

The following crosswalk is this paper's proposed design, not an ISO clause-compliance matrix:

\n\n\n\n\n\n
Standards-related distinctionApplication to the proposed agent contract
Stakeholder concern and viewpointName the decision owner, concern and representation conventions; bind the agent's organisational assignment separately.
View and modelling conventionsSpecify graph concept and relationship types, interpretation rules and correspondences between role views.
Requirement and traceabilityConnect each obligation to its need, responsible role, input baseline, criteria, output and evidence.
Verification and validationName the checked object, the predicate or intended-use scenario, its applicable context and the decision supported.
\n

This is a bounded cross-check using official public material. It is neither certification nor a full-text normative clause audit. The richer semantic contract below is the paper's engineering proposal, not a claim that either standard mandates knowledge graphs or this permission scheme.

\n

Semantic compatibility between inputs and criteria

\n

A check is meaningful only if its criteria apply to the relevant meaning and context of its input. “Release” can mean software deployment or approval of a payment; the same token need not denote the same concept. A plan for staging cannot be validated against production assumptions merely because both mention “release”. A criterion about elapsed hours cannot silently become one about business days. Evaluation requires compatible conceptual spaces, not identical wording or complete identity between two role graphs.

\n

The proposed trace is:

\n
\n

Requirement ID and revision → stakeholder need → responsible role and relevant viewpoints → input concepts, propositions and context baseline → verification criterion and validation scenario → evidence → decision.

\n
\n

The baseline identifies the entity and project, source and requirement revisions, environment, relevant time and calendar, units and thresholds, assumptions, exceptions and prohibited actions. A proposition preserves who must do what to which object, under which conditions, with its polarity and quantifiers. “Every accepted feature” and “some features” can share concepts while imposing different obligations.

\n

Verification and validation may use different abstractions. A product view can describe a customer outcome while a delivery view describes work packages. Their mapping must explain how the relevant obligation survives decomposition and which evidence supports it. Approved unit conversions and concept mappings may establish compatibility; fuzzy similarity alone cannot. Additional derived criteria can be legitimate when their rationale and relationship to the stakeholder need are recorded. An obligation cannot disappear merely because a role projection omits its vocabulary.

\n

This paper proposes the following contract. These SHALL statements are authored requirements for the proposed integration, not quotations from ISO:

\n
    \n
  1. A check SHALL identify its object, requirement baseline, applicable criteria and relevant context revisions. The requirement owner and authority to resolve ambiguity or change the baseline must be recorded.
  2. \n
  3. A transformation SHALL preserve relevant obligations or carry an explicitly approved mapping or change. This includes relationships, polarity, quantifiers, units, thresholds, scope and assumptions, not just concept identifiers.
  4. \n
  5. A decision SHALL distinguish pass, fail, indeterminate and justified not applicable. Missing context, unresolved mappings or insufficient evidence cannot yield a pass; not applicable needs a recorded reason and authorised acceptance.
  6. \n
  7. A result SHALL bind to the evaluated state and be reassessed when a relevant change invalidates it. Passing semantic or quality checks never substitutes for an action's separate authority decision.
  8. \n
\n

Context compatibility is necessary but insufficient. A perfectly aligned test can still have a weak predicate, unreliable evidence or inadequate coverage. Criteria must themselves be verified and validated for their purpose; their evaluation must establish the promised claim. Checks should report only what their evidence supports.

\n

Connecting meaning with authority

\n

The architecture needs four cooperating parts: identity and assignment, semantic knowledge, mediated execution, and audit evidence.

\n

\"Proposed

\n

Figure 1. Connecting meaning with authority. Open the diagram source in Mermaid Live.

\n

Role-based access control provides a foundation: subjects are assigned authorised roles, and operations are mediated through the permissions of those roles. A session can activate a subset of the roles available to a subject. NIST RBAC

\n

The proposed architecture adds organisational meaning to this assignment and applies contextual conditions to execution. Project identity, resource scope, environment, approval state and expiry can all matter. This is consistent with attribute-based access control, which evaluates attributes of the subject, object, operation and environment against policy. NIST SP 800-162

\n

Let a request be q = (principal, role, service, tool, action, resource, arguments, context). A simplified decision rule is:

\n
permit(q) =\n    authenticated(principal)\n    AND active_assignment(principal, role, scope, expiry)\n    AND capability_service_binding(role, service)\n    AND registered_service_tool_action(service, tool, action)\n    AND resource_and_argument_scope_match(q)\n    AND required_evidence_valid_for_exact_request(q)\n    AND contextual_policy_allows(q)\n    AND NOT explicit_deny(q)
\n

An absent or indeterminate condition produces no permit. This is a proposed contract, not a description of a policy engine already supplied by Terraphim.

\n

The gateway must mediate actual effects. Hiding a deployment tool in a user interface is insufficient if the same principal can deploy through a shell, network client or overprivileged service credential. The action boundary includes alternative routes, delegated agents and mutable resource references. A delegate receives only an authorised subset of the parent’s scope; it cannot turn a planning request into a production privilege.

\n

Terraphim graph embeddings as a role context mechanism

\n

Terraphim’s graph approach supplies a concrete semantic mechanism for the role view. Concepts and surface forms are represented explicitly, and graph relationships support retrieval and explanation. Its documented graph retrieval path uses normalised concepts, synonym matching and document co-occurrence structure, rather than requiring a neural vector embedding model. Terraphim graph embeddings

\n

The inspected general RoleGraph implementation uses Aho–Corasick matching to resolve surface forms to concept identifiers. Its document indexer creates edges between consecutive recognised concepts in the indexed text and retains supporting document references. Retrieval then accumulates node, edge and document rank. Matched edges and tags make the retrieval basis inspectable. A high rank indicates retrieval support under that graph, not factual certainty or authority.

\n

For organisational planning, this allows terms such as “delivery roadmap” and “project plan” to resolve according to the organisation’s chosen vocabulary. The interpretation is role-dependent: a delivery roadmap may emphasise dependencies and milestones for a Project Manager, while a product roadmap emphasises outcomes and prioritisation.

\n

The organisation should keep equivalence and relationship separate. A synonym maps a surface form to a concept. A relationship links distinct concepts. “Risk log” may be an agreed synonym for “risk register”, but a register and an individual risk remain distinct concepts. “Risk” and “deployment permission” must not become equivalent merely because they occur together in operational documents.

\n

Graph embeddings make the retrieval context reproducible. A result can identify the recognised concept and supporting relationship evidence, explaining why a planning template or dependency policy was selected. Template currency, the truth of retrieved claims and action authority require separate checks against authoritative metadata, evidence and policy.

\n

From co-occurrence to typed organisational relationships

\n

The existing concept vocabulary and the associations found in text play different parts in constructing the role's conceptual space. The role's thesaurus supplies the concepts that can be recognised and the synonyms that resolve to them. Indexing identifies occurrences of those concepts in notes and documents, then constructs connections from the matching text. Retrieval uses that structure to select relevant material. In this path, a connection is discovered between recognised concepts; it does not establish a new concept definition or a new organisational rule.

\n

Sentence-level co-occurrence provides an intuitive starting assumption: concepts mentioned together may be related. The inspected implementation has a broader boundary. The document indexer takes the matching concept sequence from the indexed document and connects adjacent matches without invoking the available sentence-splitting helper. Associations can therefore cross sentence boundaries. A sentence-bounded association rule would require an explicit indexing change; it is not a current guarantee.

\n

Even within one sentence, proximity leaves the relationship unspecified. A note can mention a requirement and a test because the test satisfies it, fails it, or has not yet been performed. The association graph cannot decide which statement holds. The proposed semantic layer must establish the connection's meaning, direction and conditions before admitting it as a typed organisational claim.

\n\n\n\n\n\n
LayerRepresentationWhat it establishes
Role vocabularyCanonical concepts and approved synonymsWhich concept a recognised expression denotes within the role's domain.
Text associationConnections between matched concepts, with document evidenceWhere concepts occur together or in the indexed matching sequence.
Organisational interpretationTyped, contextual propositions governed by role templatesWhether a connection expresses a dependency, responsibility, requirement, evidence claim or other admitted meaning.
Evaluation and executionApplicable criteria, evidence checks and permission decisionsWhether the specific claim is supported and whether the requested action is authorised.
\n

The organisational semantic layer supplies the relationship vocabulary. A role selects an applicable profile of that vocabulary, together with the templates for its work products. A Project Manager's delivery-plan template may admit dependencies between milestones and assignments of responsibility. A Product Manager's feature template may admit derivation from stakeholder needs and links to acceptance criteria. Shared concepts retain their identities across these views; the role determines which governed relationships are relevant, rather than inventing their meanings during each interaction.

\n

The proposed governance rule is that every admitted organisational relationship type SHALL be defined in a versioned, approved template or semantic profile, with traceability to the applicable adopted standards or an explicitly approved organisational extension. The profile records the standard edition and relevant requirement or definition where applicable. Where a standard leaves a modelling choice open, the organisation records its extension and rationale. This preserves standards-based discipline without asserting that industry standards already name every graph predicate used by an organisation. ISO/IEC/IEEE 42010:2022 and ISO/IEC/IEEE 29148:2018 inform the distinctions discussed earlier; the illustrative predicate names here are not standard-prescribed vocabulary.

\n

A relation definition needs more than a label. It identifies the permitted source and target concept types, direction, interpretation, applicable role and work product, and required qualifiers such as environment, time, polarity or evidence state. For example, verifies might connect a verification activity to a requirement, while producedEvidence connects its execution to a result. A planned test and a completed test cannot be substituted merely because both connect to the same requirement.

\n

Consider the sentence: “The Release Manager must approve production deployment after verification.” An association graph can connect the recognised concepts Release Manager, production deployment and verification. In a governed release template, those associations could support the candidate propositions deployment requiresApprovalFrom ReleaseManager and deployment requiresEvidence verificationResult. These describe obligations; they do not record that approval was given or that verification passed. An observed approval requires separate evidence identifying its issuer, subject, decision and applicable version. The runtime must still establish that the issuer and acting agent have the necessary authority.

\n

Typing a connection is therefore a controlled interpretation step. A candidate keeps its source text and concept matches; the role template supplies permissible predicates and structural constraints; a check establishes whether the text and independent evidence support the proposed proposition. An extraction model can propose a type, but a plausible label is not sufficient to admit the claim as organisational knowledge. Ambiguous, unsupported or conflicting propositions remain unresolved. The association evidence is retained so that a reviewer can reconstruct how the candidate arose.

\n

Verification and validation must compare typed propositions and their qualifiers across the input, output and evaluation criteria, using approved mappings where views differ. A test that finds every concept but converts requiresApprovalFrom into approvedBy, or removes a prohibition, has changed the claim. The audit must expose that change. Graph coverage can find omitted concepts; checking typed relationships addresses whether the connections preserve the intended meaning.

\n

Retrieving an approved planning template

\n

The following command is an illustrative invocation using the installed terraphim-grep interface. The paths represent a proposed organisational knowledge layout; they are not supplied sample data or a claim that this particular corpus was executed.

\n
terraphim-grep "delivery plan" \\\n  --role "Project Manager" \\\n  --haystack all \\\n  --paths ./organisation/templates/planning \\\n  --thesaurus ./organisation/roles/project-manager/thesaurus.json \\\n  --search-only -n 8 -C 2 --json
\n

The dedicated thesaurus makes the role’s vocabulary concrete. --search-only keeps this invocation on its search path without LLM synthesis. The implementation can fall back to thesaurus matching when populated graph evidence is absent; a result alone does not demonstrate graph traversal. Search produces candidates; the service then checks each candidate against the authoritative template registry. The chosen template must be approved, applicable to the project type, available to the active role and identified by an immutable revision or content digest. Restricting search paths narrows retrieval; it does not establish an execution sandbox.

\n

This distinction matters when a semantically excellent match is obsolete. The organisation’s semantic layer may help locate it, but the planning service must select an eligible template. If none exists, the service should request a governed template decision rather than manufacture an “approved” template itself.

\n

A graph-based planning checklist

\n

A checklist makes an expected output contract explicit. Terraphim’s published checklist approach places enumerated concepts outside the model and uses the same graph machinery for recognition and retrieval. Graph embeddings for checklists

\n

The current source also supports a paired input/output graph-coverage mode. An illustrative planning checklist, saved as .terraphim/checklists/project_plan.md, is:

\n
# Project plan\n\nmode:: graph-coverage-v1\nchecklist:: scope, dependencies, risk register, acceptance criteria\n\n### risk register\n\nsynonyms:: risk log
\n

Its concepts and aliases must resolve uniquely in the configured role graph; declaring an alias in a checklist does not create the missing graph concept. The checklist is not a substitute for building and governing that graph. A source-level invocation for the inspected implementation is:

\n
terraphim-agent validate \\\n  --role "Project Manager" \\\n  --checklist project_plan \\\n  --input-file brief.md \\\n  --json < plan.md
\n

This --input-file form is supported by the inspected client source, not by the locally installed 1.21.22 command interface. The example is therefore a source-supported integration example, not an installed-binary demonstration.

\n

The paired check requires recognised input concepts and checklist concepts to appear in the output, with supporting stored graph paths. Its report distinguishes pass, fail and indeterminate. A service using it as a gate must inspect that verdict; successful CLI completion alone does not establish a passing checklist.

\n

Concept coverage is one limited checking predicate. A plan can mention “dependencies” while assigning them incorrectly, or include “acceptance criteria” that do not test the promised outcome. The complete planning contract consequently layers structural checks, graph concept coverage and evidence review. The graph checker supplies an inspectable signal within that contract. It does not establish the full semantic compatibility contract, verify every asserted relationship or confer permission to publish or execute the plan. Its lexical negation handling does not constitute general verification of proposition meaning.

\n

Worked example from brief to bounded plan

\n

Consider a project to deliver a customer-facing feature. The Product Manager agent receives an authorised assignment to prepare a feature breakdown. Its graph connects the product outcome to needs, features, value assumptions and acceptance criteria. It produces a versioned breakdown and hands that artefact to the Project Manager service.

\n

The Project Manager agent authenticates under its own scoped assignment. It retrieves candidate planning templates through its role thesaurus, selects an approved revision, and creates a draft connecting the agreed features to milestones, dependencies, owners and risks. It preserves the incoming acceptance criteria and identifies unresolved assumptions.

\n

The service checks the draft’s structure and invokes the graph checklist. A missing recognised requirement produces a failed or indeterminate result that must be resolved before the relevant transition. A passing result is recorded alongside its limitations. Further review establishes whether the proposed dependencies and acceptance evidence are adequate.

\n

When the service requests create_draft on the assigned project’s document store, the gateway evaluates the exact operation, destination and content digest. Assuming the role assignment, template, checks and policy conditions are valid, it permits that write and records its outcome.

\n

Now suppose the agent requests deploy against production. The request fails the role-to-service/action binding and the production policy. Mentioning deployment in the plan, retrieving a release runbook, or passing the planning checklist cannot repair that missing authority. The gateway records a denial before an effect occurs.

\n

The agent may instead hand the approved plan to the release management process. That handoff supplies evidence, not inherited production credentials. A separately authorised release principal must satisfy its own contract. The organisation can therefore automate preparation without collapsing preparation, approval and execution into a single unrestricted agent.

\n

Checking the planning context

\n

Consider an illustrative requirement R-17 revision 3: the plan must specify a staging rollback rehearsal whose planned start is no earlier than the simulated-failure trigger and no later than eight elapsed hours afterwards. The baseline defines that trigger as the rehearsal protocol's injected-failure timestamp. Production deployment is outside the Project Manager's scope. The stakeholder need is to coordinate a usable rehearsal without delegating production execution.

\n

A verification criterion checks the plan's environment, planned start offset and preserved prohibition against that baseline. A validation scenario asks the delivery and release stakeholders whether the proposed handoff enables the intended rehearsal with the available people and resources. The plan's check does not prove that a rehearsal occurred. Later testing of the delivered system requires its own evidence; a good plan does not establish successful system behaviour.

\n\n\n\n\n\n\n
Candidate interpretation or evidenceRequired treatment under the proposed contract
“One business day” replaces eight elapsed hoursNo silent equivalence. Resolve the calendar and conversion; fail if the interval changes, otherwise retain the approved mapping.
The plan schedules the rehearsal in productionFail the known environment mismatch; separately deny unauthorised production actions.
The evaluator uses revision 2's 24-hour thresholdReject the stale criterion binding and reassess against revision 3.
“Must not deploy” becomes “may deploy”Fail proposition preservation despite matching deployment concepts.
Relevant calendar or requirement revision is unknownReturn indeterminate until the baseline is resolved.
\n

The same protection applies when views use different language. “Pre-production” can map to the named staging environment if the organisation explicitly defines that equivalence. Matching the spelling “staging” in an unrelated project's evidence cannot establish that the requirement was met.

\n

Terraph audit with one role and one checklist

\n

This Terraph example connects a role graph, a checklist and three verbs: audit, check and deploy. Here, engineer is the example role; it does not change the Project Manager’s authority. The graph and checklist are Markdown inputs compiled into constant matching automata. embed and satisfies are language intrinsics, so retrieval and checking use the same concept definitions.

\n
import std;\n\nrole engineer from "kg";\nchecklist preflight from "checklists/preflight.md";\n\n// Audit concept coverage and report the matching source spans.\nfn audit(path: str) -> i64 {\n    let src = read_file(path);\n    if src.len == 0 {\n        print("cannot read ", path);\n        return 1;\n    }\n    let h = embed(src);\n    let cov = coverage(h);\n    print("file: ", path);\n    print("kg_hits: ", cov.kg_hits, "  concepts: ", cov.concepts,\n          "  confidence: ", cov.confidence, "  diversity: ", cov.diversity);\n    for m in h {\n        print("  ", m.concept, " @", m.start, "..", m.end);\n    }\n    if cov.sufficient() {\n        print("sufficient: yes");\n        return 0;\n    }\n    print("sufficient: no");\n    return 1;\n}\n\n// Diagnose missing checklist items. This verb does not issue a proof.\nfn check(text: str) -> i64 {\n    let miss = text satisfies preflight;\n    if miss.any() {\n        print("checklist preflight: incomplete");\n        miss.report();\n        return 4;\n    }\n    print("checklist preflight: complete");\n    return 0;\n}\n\n// The generated wrapper supplies a sealed proof for the checked snapshot.\nfn deploy(plan: str, ev: Proof<preflight>) -> i64 {\n    print("deploy: preflight evidence present, proceeding");\n    return 0;\n}
\n

audit exposes which concepts matched and where. check reports missing checklist items. At the gated entry point, the generated verb wrapper obtains a sealed Proof<preflight> through .require(), which succeeds only when no items are missing. The function signature makes that evidence a prerequisite for entering deploy.

\n

The proof carries the checked snapshot as ev.subject. Any operation justified by the gate must consume that snapshot; reopening the path in plan could read changed, unchecked bytes. The sample body only prints a message. Actual deployment still requires an authorised role assignment and resource policy, and concept coverage alone does not establish that the plan is correct. The example shows the language boundary; it is not a recorded deployment.

\n

ADF configuration gives the role an organisational task

\n

The ADF agent orchestration configurations assign organisational responsibilities. The checked-in Product Owner template names an implementation and declares these fields. Product Owner is the configured role; it is distinct from the illustrative Product Manager role used earlier:

\n
[[agents]]\nname = "product-owner"\npersona = "Themis"\ncapabilities = [\n    "planning",\n    "roadmap",\n    "issue-creation",\n    "prioritisation",\n]\nproject = "terraphim"
\n

Its skill chain includes research, design, specification, architecture, business scenario design, requirements traceability and acceptance testing. The task defines the work product: each proposed issue must identify its organisational goal, describe the problem and proposed solution, and supply Given–When–Then acceptance criteria. The template also requires a priority estimate, a communication hint and the suggested delivery agent. The workflow narrows the backlog, scores the surviving items and proposes up to two new issues.

\n

These selected literal fields show the existing issue template. Its brackets indicate fields the Product Owner must fill; WIG means the organisation’s current Wildly Important Goal.

\n
Title: [concise, imperative]\nWIG: [which WIG this serves, or skip if none]\nProblem: [2-3 sentences describing the problem]\nProposed solution: [bullet points, no hand-waving]\nAcceptance criteria (mini-UAT):\n  Given [precondition]\n  When [action]\n  Then [observable outcome]
\n

The configuration connects the Product Owner’s planning capability to a defined work product and template. It records operational intent; this inspection does not establish that the agent is deployed or that every configured step succeeds.

\n

The repo-local ADF configuration separately declares a Developer, Reviewer and Merge Coordinator, with distinct capability lists and named Terraphim roles:

\n\n\n\n\n
Configured agentDeclared Terraphim roleResponsibility expressed in the configuration
developerTerraphim AI DeveloperImplementation, Rust development and test-driven development
reviewerTerraphim AI ReviewerCode review, quality gates and review coordination
merge-coordinatorTerraphim AI Merge CoordinatorMerge coordination, CI/CD and conflict resolution
\n

The orchestrator’s merge path checks four required status contexts: native-ci / build (push), adf/pr-reviewer, adf/validation and adf/verification. It also requires canonical gate-result records for the current pull request head. Blocking findings and the project's treatment of concerns affect the decision. Evidence for an older commit cannot substitute for the current head’s review, binding the decision to that part of its context baseline. The labels adf/validation and adf/verification do not establish that their producers perform the corresponding ISO requirements or system activities.

\n

Configuration intent and runtime enforcement must remain distinguishable. The inspected repo-local loader sets terraphim_role to None; the declaration alone does not prove that an agent receives the named role graph. Instructions such as “do not merge” are task text, and the Product Owner’s broad shell access is not a production-deployment barrier. The merge gate controls the orchestrator’s merge path; it does not prove that every alternative credential or tool path is mediated. Its structured records are not independently authenticated merely by having the expected fields.

\n

ADF declares responsibility, capabilities, workflow and expected artefacts. Terraph implements bounded tool access, checks and receipts; Terraphim provides the role vocabulary and graph context. Connecting them still requires authenticated role assignments, capability-to-service identifiers, approved artefact versions and policy decisions tied to exact requests.

\n

W3C PROV provides terms for recording entities, activities and agents, including derivation, association and delegated responsibility. These relationships can describe an artefact’s origin and an agent’s participation; a provenance statement alone is not an authorisation decision. W3C PROV-O

\n

Evaluation and implementation boundaries

\n

The proposal can be evaluated without assuming that more graph coverage means safer behaviour. A test corpus should pair legitimate planning tasks with boundary cases and preserve the same role assignments and policy across agent implementations.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n
TestExpected observable result
Valid plan from an approved templateDraft created in the assigned project with complete evidence links
Semantically relevant but withdrawn templateCandidate rejected by template eligibility policy
Required concept omittedGraph report identifies a fail or indeterminate result; gated transition does not proceed
All terms present but a dependency is wrongConcept coverage may pass; substantive review detects the error
Project Manager requests production deploymentDenied before any deployment effect
Deployment attempted through an alternative toolSame authority boundary enforced
Artefact changes after checkingPrevious evidence rejected for the changed request or bytes
Role assignment expires before executionGateway denies or requires a fresh valid assignment
Agent implementation is replacedService contract and authority boundaries remain unchanged
Concepts match but polarity, environment or units differRelevant semantic predicate fails, or unresolved meaning is indeterminate
A role projection drops a critical obligationTrace check rejects the transformation or requires an approved change
Different role views have an approved meaning-preserving mappingCompatibility can be established; substantive criteria still require evidence
\n

Measurements should include accepted legitimate tasks, incorrect denials, unauthorised effects, trace completeness and the cost of reconstructing a decision. Report semantic-check accuracy separately from policy enforcement. A test that finds no bypass in a bounded suite is evidence about that suite, not proof that all possible tool paths are mediated.

\n

Terraphim already supplies relevant semantic components, and Terraph supplies concrete prototype mechanisms for compiled checks and evidence. ADF provides configured organisational responsibilities and an implemented orchestrator merge gate. The end-to-end organisational contract, contextual requirement-to-criterion alignment, authenticated role binding, complete execution mediation and linked audit schema remain the integration proposed in this paper. No production deployment result or benchmark is claimed here.

\n

Role owners must maintain concepts, templates and policies, and reviewers must understand what each checklist establishes. Graph updates need controlled promotion because a vocabulary change can alter which obligations appear satisfied. Audit records should retain enough versioned evidence to reconstruct decisions while limiting sensitive content.

\n

Conclusion

\n

An agent’s organisational role provides a durable basis for knowledge, responsibility and delegated authority. Capabilities explain the outcomes the role must support; services describe how an agent implementation supplies them; tools expose actions; contextual policy determines which actions may occur. A role-based knowledge graph makes the concepts and relationships behind the work inspectable. Meaningful verification and validation also require those concepts, their propositions and the operating context to align with the applicable criteria through explicit, traceable mappings. A shared vocabulary alone cannot preserve an obligation.

\n

Terraphim supplies explicit concept matching, co-occurrence associations, graph retrieval and graph-based checklists. The proposed next layer types those associations using governed role templates and standards-traceable relationship definitions, distinguishing a mention from an obligation and an obligation from an evidenced event. Terraph illustrates how compiled checks can produce evidence tied to the subject being checked. Connecting these mechanisms to authenticated role assignments and mediated execution yields a traceable organisational contract.

\n

For a Project Manager agent, the contract requires an approved planning template, preserved requirements, a checked plan and operation within the assigned project scope. Production deployment belongs to a separately authorised role and process. The evidence must allow a reviewer to reconstruct both the planning result and the authority decision.

\n

Sources and scope

\n

Public sources are linked beside the claims they support. The preceding article by David Hunt and Alexander Mikhalev was published on 31 October 2026 and read on 4 November. The continuation and concept mapping are this paper's synthesis.

\n

Terraphim and ADF implementation details were checked against local source on 3–4 November 2026. The Terraph example above was supplied by Alexander Mikhalev; its comments have been shortened and its function bodies retained. Source revisions, implementation paths, recorded fixture results and inspection limits remain in the internal research notes.

\n

The standards cross-check used official catalogue records and accessible previews. The complete normative text of ISO/IEC/IEEE 42010:2022 was not inspected, so no clause-by-clause conformity claim is made. The role contracts, typed relationship profiles, contextual criteria and cross-system bindings are proposed integration work. The examples do not demonstrate a live production deployment.

\n", "https://reference-architecture.ai/properties/date": "2026-11-07", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["ai-agents","governance","knowledge-graphs","terraphim","verification"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/posts/from-ai-risk-to-accountable-agents/", "https://atomicdata.dev/properties/name": "From AI Risk to Accountable Agents", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/from-ai-risk-to-accountable-agents/", "https://atomicdata.dev/properties/description": "

In AI Risk is evolving, every day., David Hunt and I examined how an AI agent can pursue a legitimate objective through methods outside its mandate. Oversight needs to connect organisational intent, knowledge, authority, actions and consequences.

\n

\n

This article summarises the full paper. To supervise that agent, we need to specify its responsibilities, the concepts it uses, the evidence its work requires and the actions it may take.

\n

Start with an organisational role

\n

“Planning agent” tells us what an agent does, but leaves its responsibility unclear.

\n

A Product Manager breaks outcomes into features and acceptance criteria. A Project Manager turns agreed scope into a delivery plan with dependencies, owners and milestones. Both perform planning, but their work products and authority differ.

\n

An agent's planning capability should belong to a defined organisational role. Its implementation provides services that use tools to perform specific actions.

\n

The chain becomes:

\n

Organisation → role → capability → service → tool → action → contextual permission.

\n

A Project Manager agent might select an approved planning template and create a draft in its assigned project. That assignment does not authorise production deployment. The permission decision must govern the actual tool operation, including alternative routes through shells or delegated agents.

\n

Give the role a defined conceptual space

\n

Each role needs a governed view of organisational knowledge. A Product Manager works with user needs, value hypotheses and acceptance criteria. A Project Manager needs dependencies, resources, risks and delivery commitments. Shared concepts connect their work without making the roles interchangeable.

\n

\"Role-based

\n

Figure 1. Proposed role contract: the knowledge needed to plan and the authority to act are explicit.

\n

Terraphim's role-based knowledge graphs make this vocabulary explicit through concepts and synonyms. The inspected indexer connects consecutive recognised concepts in document text, retaining supporting document references. Those associations help retrieve relevant notes and explain why they were selected.

\n

The next layer is to type the relationships. A connection might mean “depends on”, “addresses a requirement” or “requires approval”. The role selects the applicable relationship profile from approved templates, traced to adopted standards or documented organisational extensions.

\n

A sentence mentioning a release, a manager and approval does not establish that approval occurred. “Requires approval” and “was approved” are different claims.

\n

Verify meaning in its context

\n

A checklist can recognise every expected concept while missing a change in meaning.

\n

Suppose a delivery plan must place a rollback rehearsal in staging, with its planned start within eight elapsed hours of a defined failure trigger. An output can contain “rollback”, “rehearsal” and “hours” while moving the activity to production or replacing elapsed hours with business days.

\n

Successful checking therefore requires compatible concepts, relationships and context across the input, output and criteria. Different terminology can be acceptable through an explicit mapping. Changed scope, units, conditions or prohibitions cannot pass silently.

\n

Our paper uses ISO/IEC/IEEE 42010:2022 to frame architecture descriptions and ISO/IEC/IEEE 29148:2018 to distinguish requirements quality and stakeholder intent from evidence about the realised system. This is a standards-informed design, not a conformity claim.

\n

The practical requirement is traceability: identify the requirement version, relevant context, applicable criterion and evidence behind each result. When those conditions change, reassess the affected findings. Unknown context should produce an indeterminate result, not an assumed pass.

\n

\"Contextual

\n

Figure 2. Proposed contextual check: passing a planning criterion neither proves the rehearsal occurred nor grants deployment authority.

\n

Make the audit concrete

\n

Terraph, the Terraphim language, connects a role graph, a checklist and a gated action in one program. The graph and checklist compile from Markdown into constant matching automata. embed identifies concepts and source spans; text satisfies preflight reports which checklist items are missing.

\n

The deploy verb declares the evidence it needs. Here, engineer is the example role; it does not change the Project Manager’s authority:

\n
import std;\n\nrole engineer from "kg";\nchecklist preflight from "checklists/preflight.md";\n\nfn deploy(plan: str, ev: Proof<preflight>) -> i64 {\n    print("deploy: preflight evidence present, proceeding");\n    return 0;\n}
\n

The generated verb wrapper obtains a sealed Proof<preflight> through .require() only when no checklist items are missing. The proof binds to the checked snapshot, available as ev.subject.

\n

\"Terraph

\n

Figure 3. The Terraph proof gate. The example body prints a message; it does not deploy a system.

\n

Any actual operation must use that checked snapshot and pass a separate authority check. Reopening the plan could read changed bytes. Passing the checklist cannot give a Project Manager production privileges, nor can concept coverage establish that the plan is correct.

\n

Our earlier argument calls for supervision that follows an agent's changing context. An inspectable role contract gives that supervision a defined mandate to check. A Project Manager agent can prepare a plan, preserve its requirements and record the evidence, while release authority stays with the role responsible for exercising it.

\n", "https://reference-architecture.ai/properties/date": "2026-11-07", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["ai-agents","governance","knowledge-graphs","terraphim","verification"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/posts/terraphim-engine-architecture/", "https://atomicdata.dev/properties/name": "The Terraphim Engine: Six Layers, Fifty-Two Crates", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/terraphim-engine-architecture/", "https://atomicdata.dev/properties/description": "

The diagram on the front page of this site is not an illustration. Every box in it is a\npublished Rust crate you can cargo add today, read the documentation for, and use\nwithout taking the rest of the system. This article explains what sits behind each box,\nand why the decomposition is the interesting part.

\n

\n

Why decomposition is the architecture

\n

Most agent frameworks ship as a single dependency. You take the orchestrator, the memory\nlayer, the retrieval stack and the tool protocol together, or you take none of it. That\nis convenient at first and expensive later: when one layer misbehaves you cannot isolate\nit, and when you want to replace one you replace all of them.

\n

The pattern this site argues for is the opposite. Draw the boundaries first, make each\none independently useful, and let the seams stay visible. Terraphim is built that way —\n52 crates across six layers, each with a single responsibility — which makes it a\nreasonable worked example rather than a product pitch.

\n

The test of a boundary is whether you can use one side without the other. Here you can:\nterraphim_automata is a text-matching engine that happens to be used by a knowledge\ngraph; terraphim_persistence is a storage abstraction that does not know what it is\nstoring.

\n

The six layers

\n

Terraphim organises its crates into a dependency graph with six levels. Lower layers do\nnot know the higher ones exist.

\n\n\n\n\n\n\n\n
LayerConcernRepresentative crates
6User interfacesterraphim_agent, terraphim-cli, terraphim_server
5Orchestrationterraphim_orchestrator, terraphim_kg_orchestration, terraphim_symphony
4Agent systemterraphim_spawner, terraphim_router, terraphim_agent_supervisor
3Service layerterraphim_service, terraphim_middleware, haystack_*
2Core engineterraphim_automata, terraphim_rolegraph, terraphim_persistence
1Types and configurationterraphim_types, terraphim_config, terraphim_settings
\n

That ordering matters more than the crate count. An agent system that cannot say which\nlayer a failure came from is a system you cannot debug in production.

\n

Layer 1: types and configuration

\n

terraphim_types is the shared vocabulary — the structures every other crate agrees on.\nterraphim_config holds role definitions, haystacks and LLM routing;\nterraphim_settings handles runtime preferences.

\n

Putting roles in configuration rather than code is what makes the context boundary\nstructural. A role has access to certain haystacks; the knowledge graph is built from\nthose haystacks; therefore the agent cannot reach information outside its role. That is\nnot a permission check at runtime that could be bypassed — it is an absence. The data\nwas never loaded.

\n

Layer 2: the core engine

\n

This is the layer the front-page diagram mostly depicts.

\n

terraphim_automata is the matching engine:\nAho-Corasick finite state automata that match thousands of patterns simultaneously in\nO(n) time, where n is the length of the text. Not \"usually fast\" — bounded, and the same\nbound every time. It compiles to WebAssembly, which is why the same matcher runs in a\nbrowser as on a server.

\n

terraphim_rolegraph is the knowledge graph.\nIt is deliberately not a general-purpose graph database. It maps search roles to\ndomain-specific graph views, and it exists to do one thing quickly: turn text into\nconcepts, deterministically. Terraphim reports knowledge-graph inference in the 5-10\nnanosecond range and a 15 MB memory footprint, with no GPU — figures worth checking\nagainst your own workload, but the shape of the claim follows from the design rather\nthan from optimisation tricks.

\n

terraphim_persistence provides the\nPersistable trait and DeviceStorage backends across memory, SQLite and redb. Storage is\nan interface here, not an assumption, which is what lets the same engine run on a laptop\nand at the edge.

\n

Layer 3: the service layer

\n

terraphim_middleware searches haystacks —\npluggable data-source backends. This is the boundary the front-page diagram labels\nDocument Input. A haystack might be a local folder, a repository, or a connector to\nsomething else; the layers above do not change when you add one.

\n

terraphim_service handles requests and responses\nfor the core, which keeps request handling out of the engine itself.

\n

Layers 4 and 5: agents and orchestration

\n

Above the service layer sit the agent system (terraphim_spawner, terraphim_router,\nterraphim_agent_supervisor) and orchestration (terraphim_orchestrator,\nterraphim_kg_orchestration, terraphim_symphony).

\n

The distinction is worth holding onto. The agent system is concerned with individual\nagents — spawning them, routing to them, supervising them. Orchestration is concerned\nwith what happens between them. Collapsing those two into one \"agent framework\" is the\nusual mistake, and it is why so many systems cannot tell a stuck agent from a stuck\nworkflow.

\n

The MCP-Native Agent Architecture goes\ninto the protocol surface these layers expose, and why treating MCP as an architectural\nprimitive rather than a transport changes what the system can guarantee.

\n

Layer 6: the interfaces

\n

terraphim_agent is the interactive REPL with session\nsearch and learning capture. terraphim-cli is the\nautomation-shaped counterpart, with JSON output for scripting.\nterraphim_server provides the REST API.

\n

Three interfaces over one engine, none of them privileged. That is the payoff of the\nlayering: the CLI is not a thin wrapper around the server, and the server is not a\nspecial case. They are peers over the same core.

\n

What to take from this

\n

You do not need to adopt Terraphim to use the pattern. The transferable parts are:

\n\n

Documentation

\n

Every crate named above is published and documented:

\n\n

Installation is a single command, or cargo install terraphim_agent if you would prefer\nto start from the REPL.

\n", "https://reference-architecture.ai/properties/date": "2026-09-08", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["terraphim","rust","knowledge-graphs","agents","reference-architecture"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/posts/mcp-native-agent-architecture/", "https://atomicdata.dev/properties/name": "The MCP-Native Agent Architecture", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/mcp-native-agent-architecture/", "https://atomicdata.dev/properties/description": "

A reference architecture for production AI systems, drawn from five years building Terraphim — a privacy-first AI assistant with ~50 Rust crates, MCP-native interfaces, and role-based multi-agent orchestration.

\n

The Production Agent Crisis

\n

In the spring of 2024, I watched a well-funded AI startup burn through $180,000 in cloud credits in six weeks. Their crime? They let an LLM-powered agent manage their Kubernetes cluster. The agent had root access, a vague prompt (\"optimize resource utilization\"), and no guardrails. It deleted a production namespace, scaled a stateful set to zero, and triggered a cascading failure that took down their primary revenue pipeline for four hours.

\n

The post-mortem was illuminating. The LLM wasn't \"malicious.\" It was doing exactly what LLMs do: generating plausible-sounding text based on pattern matching. The prompt \"optimize resource utilization\" is semantically close to \"remove unused resources.\" The agent found a namespace with low CPU utilization and removed it. Logical, if you squint. Catastrophic, if you're the on-call engineer.

\n

This is the production agent crisis in miniature: we've given probabilistic systems deterministic powers without deterministic boundaries.

\n

The current generation of agent frameworks — LangChain, CrewAI, AutoGPT, and their descendants — share a common architectural flaw: they treat the LLM as both the decision-maker AND the execution engine. The LLM decides what to do, how to do it, and when to stop. This is fine for demos. It's a liability for production.

\n

Consider what production systems actually require:

\n\n

None of these are properties of LLMs. LLMs are probabilistic text generators. They hallucinate. They drift. They respond to temperature. They have no concept of \"side effects\" or \"resource limits\" or \"this action is irreversible.\"

\n

The solution is architectural, not algorithmic. We need a boundary layer between the probabilistic decision-maker (the LLM) and the deterministic execution environment (the system). That boundary layer is the Model Context Protocol (MCP), and treating it as an architectural primitive rather than a communication protocol changes everything.

\n

MCP as Architectural Primitive

\n

MCP was introduced by Anthropic in late 2024 as a standardized protocol for connecting AI assistants to external data sources and tools. At its simplest, it defines how an AI client discovers capabilities, requests context, and invokes tools from a server.

\n

But MCP is more than a wire format. It is a contract layer that enforces capability boundaries. And when you treat it as an architectural primitive — a fundamental building block of your system, not just a communication detail — you get something powerful: the ability to build deterministic agent systems on top of probabilistic reasoning engines.

\n

The Protocol Surface

\n

An MCP server exposes three things:

\n
    \n
  1. Resources: Read-only data sources (files, databases, API responses)
  2. \n
  3. Tools: Functions the client can invoke, with defined schemas and side effects
  4. \n
  5. Prompts: Pre-defined templates for common interactions
  6. \n
\n

The key insight: the server defines WHAT is possible. The LLM decides WHICH possibility to pursue. The protocol enforces the boundary between them.

\n

Terraphim's MCP server (terraphim_mcp_server) implements this boundary in ~200 lines of Rust. It exposes three tools: search (read-only, queries the knowledge graph), build_autocomplete_index (idempotent, rebuilds the Aho-Corasick automata), and update_config (destructive, modifies runtime configuration). Each tool has a schema. Each tool has a known effect. The LLM cannot invoke a tool it hasn't discovered, and it cannot pass arguments that don't match the schema.

\n

This is the difference between \"I hope the LLM doesn't delete something\" and \"the system is physically incapable of deleting something without authorization.\"

\n

The Four-Layer Reference Architecture

\n

\"Four-Layer\nThe MCP-native agent stack: four layers with the LLM as a probabilistic reasoning engine, bounded by deterministic architectural layers.

\n

Based on production experience building Terraphim — a privacy-first AI assistant with ~50 Rust crates, MCP server integration, and multi-agent orchestration — I've distilled the following reference architecture. It separates concerns that current frameworks conflate and introduces verification at every boundary.

\n

Layer 1: Role & Configuration

\n

What the agent IS.

\n

In Terraphim, everything starts with a Role. A Role is not a prompt. It is a structured configuration that defines an agent's identity, knowledge sources, capabilities, and constraints. Think of it as a compile-time contract for an agent's behavior.

\n
// From terraphim_config — the actual Role struct\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct Role {\n    pub name: RoleName,\n    pub relevance_function: RelevanceFunction,  // BM25, TitleScorer, etc.\n    pub haystacks: Vec<Haystack>,               // Data sources\n    pub kg: Option<KnowledgeGraph>,             // Linked knowledge graph\n    pub llm_enabled: bool,\n    pub llm_model: Option<String>,              // e.g., "gemma3:270m"\n    pub llm_router_enabled: bool,               // 6-phase intelligent routing\n    pub extra: AHashMap<String, Value>,         // Extensible config\n}
\n

Key properties:

\n\n

The Role is the \"type system\" of your agent. Just as Rust's borrow checker prevents data races at compile time, the Role configuration prevents capability leakage at configuration time.

\n

Layer 2: Knowledge Graph

\n

What the agent KNOWS.

\n

Terraphim's knowledge graph (terraphim_rolegraph) is not a general-purpose graph database. It is a specialized structure optimized for one thing: fast, deterministic text-to-concept matching using Aho-Corasick automata.

\n
// From terraphim_rolegraph — the actual RoleGraph\npub struct RoleGraph {\n    pub role: RoleName,\n    nodes: AHashMap<u64, Node>,\n    edges: AHashMap<u64, Edge>,\n    documents: AHashMap<String, IndexedDocument>,\n    pub thesaurus: Thesaurus,\n    pub ac: AhoCorasick,                    // Compiled automata\n    pub ac_reverse_nterm: AHashMap<u64, NormalizedTermValue>,\n}\n\nimpl RoleGraph {\n    pub fn find_matching_node_ids(&self, text: &str) -> Vec<u64> {\n        self.ac.find_iter(text)\n            .map(|mat| self.aho_corasick_values[mat.pattern()])\n            .collect()\n    }\n}
\n

Key properties:

\n\n

The Context Boundary emerges naturally from the Role configuration: if a Role only has access to certain haystacks, and the RoleGraph is built from those haystacks, the agent cannot access information outside its Role. This is not a runtime permission check. It is a structural impossibility.

\n

Layer 3: Multi-Agent Orchestration

\n

What the agent DOES with others.

\n

Terraphim's multi-agent system (terraphim_multi_agent) treats each Role as an autonomous agent. Agents discover each other through capability registration, communicate through structured messages, and coordinate through workflow patterns.

\n
// From terraphim_multi_agent — the actual AgentRegistry\npub struct AgentRegistry {\n    agents: Arc<RwLock<HashMap<AgentId, Arc<TerraphimAgent>>>>,\n    capabilities: Arc<RwLock<HashMap<String, Vec<AgentId>>>>,\n    role_agents: Arc<RwLock<HashMap<String, AgentId>>>,\n    agent_load: Arc<RwLock<HashMap<AgentId, LoadMetrics>>>,\n}\n\nimpl AgentRegistry {\n    pub async fn find_agents_by_capability(&self, capability: &str) -> Vec<AgentId> {\n        let capabilities = self.capabilities.read().await;\n        capabilities.get(capability).cloned().unwrap_or_default()\n    }\n}
\n

Key properties:

\n\n

The Execution Harness is not a Wasm sandbox (though we have WASM support in terraphim_automata for browser contexts). It is the agent's own TerraphimAgent struct, which enforces resource limits through Rust's type system: token budgets are usize, timeouts are Duration, and context windows are bounded by configuration.

\n

Layer 4: MCP Protocol Surface

\n

How the agent COMMUNICATES.

\n

The terraphim_mcp_server crate exposes Terraphim's capabilities through the Model Context Protocol. It acts as both server (exposing Terraphim tools to external clients) and client (discovering external tools for Terraphim agents).

\n
// From terraphim_mcp_server — the actual McpService\n#[derive(Clone)]\npub struct McpService {\n    config_state: Arc<ConfigState>,\n    resource_mapper: Arc<TerraphimResourceMapper>,\n    autocomplete_index: Arc<tokio::sync::RwLock<Option<AutocompleteIndex>>>,\n}\n\nimpl McpService {\n    pub async fn search(\n        &self,\n        query: String,\n        role: Option<String>,\n        limit: Option<i32>,\n    ) -> Result<CallToolResult, ErrorData> {\n        // 1. Resolve role (tenant boundary)\n        let role_name = if let Some(role_str) = role {\n            RoleName::from(role_str)\n        } else {\n            self.config_state.get_selected_role().await\n        };\n\n        // 2. Query knowledge graph within role boundary\n        let search_query = SearchQuery {\n            search_term: NormalizedTermValue::from(query),\n            role: Some(role_name),\n            limit: limit.map(|l| l as usize),\n            ..Default::default()\n        };\n\n        // 3. Return structured results\n        match service.search(&search_query).await {\n            Ok(documents) => { /* ... */ }\n            Err(e) => { /* ... */ }\n        }\n    }\n}
\n

Key properties:

\n\n

Why Rust?

\n

The reference architecture is implemented in Rust. This is not aesthetic preference. It is an engineering requirement.

\n

Zero-cost abstractions: The Role configuration, knowledge graph, and agent registry introduce no runtime overhead. The type system enforces safety at compile time.

\n

Deterministic resource usage: No garbage collection pauses. Memory is explicitly managed. The terraphim_automata Aho-Corasick matcher runs in bounded time and space.

\n

Fearless concurrency: The AgentRegistry is accessed by multiple agents simultaneously. Rust's Arc<RwLock<_>> prevents data races without runtime overhead.

\n

WASM portability: terraphim_types and terraphim_automata compile to WebAssembly, enabling browser-based autocomplete with TypeScript bindings.

\n

Production Patterns

\n

\"Traditional\nTraditional agents conflate decision and execution. MCP-native agents enforce a hard boundary: the LLM decides, the architecture executes.

\n

Pattern 1: Multi-Tenant Agent Isolation

\n

In Terraphim, multiple users share the same agent infrastructure but have completely isolated contexts:

\n
// Each tenant gets their own Role configuration\nlet tenant_role = Role::new("tenant_acme")\n    .with_haystacks(vec![acme_documents, acme_wiki])\n    .with_llm_model("gemma3:270m")\n    .with_relevance_function(RelevanceFunction::BM25);\n\n// The RoleGraph is built ONLY from tenant's haystacks\nlet role_graph = RoleGraph::new("tenant_acme".into(), tenant_thesaurus).await?;\n\n// MCP search automatically scopes to tenant's role\nlet results = mcp_service.search(query, Some("tenant_acme".to_string()), None).await?;
\n

Key insight: The boundary is per-tenant, but the automata engine and MCP server are shared. This gives you multi-tenant isolation without multi-tenant cost.

\n

Pattern 2: Deterministic Replay

\n

When a bug occurs in production, you need to reproduce it exactly. With probabilistic agents, this is nearly impossible. With MCP-native architecture, it's trivial:

\n

\"Deterministic\nRecording LLM responses (not regenerating them) makes replay deterministic. The same inputs produce the same execution trace.

\n
// The RoleGraph + automata are deterministic\n// Same query + same thesaurus → same matches, every time\nlet matches = role_graph.find_matching_node_ids("rust programming");\n// Always returns the same node IDs for the same input\n\n// For LLM variability: record responses, don't regenerate\nlet recorded_response = load_recording(task_id)?;\nlet plan = agent.generate_plan_with_response(context, recorded_response)?;
\n

Key insight: The knowledge graph is deterministic. The LLM is not. Separate the two, and replay becomes deterministic where it matters.

\n

Pattern 3: Human-in-the-Loop for Destructive Operations

\n

\"Approval\nDestructive operations require explicit approval. The system logs every request but does not execute without authorization.

\n
// terraphim_mcp_server's update_config tool is inherently bounded\npub async fn update_config_tool(&self, config_str: String) -> Result<CallToolResult, ErrorData> {\n    match serde_json::from_str::<Config>(&config_str) {\n        Ok(new_config) => {\n            // Config validation happens BEFORE mutation\n            self.validate_config(&new_config)?;\n            self.update_config(new_config).await?;\n            Ok(CallToolResult::success(vec![Content::text(\n                "Configuration updated".to_string()\n            )]))\n        }\n        Err(e) => {\n            Ok(CallToolResult::error(vec![Content::text(\n                format!("Invalid configuration: {}", e)\n            )]))\n        }\n    }\n}
\n

Key insight: The MCP schema IS the approval gate. The LLM cannot construct a valid Config JSON without knowing the schema, and invalid configs are rejected before mutation.

\n

Integration with Terraphim

\n

\"Terraphim\nTerraphim instances share an MCP protocol layer while maintaining isolated Role configurations and Knowledge Graphs. Each agent discovers tools through MCP without data leakage.

\n

Terraphim implements this architecture across ~50 Rust crates. Here's how the layers map to real code:

\n\n\n\n\n\n\n\n\n
LayerCrateResponsibility
Role & Configterraphim_configRole definitions, haystacks, LLM routing
Knowledge Graphterraphim_rolegraphAho-Corasick automata, concept matching
Fast Matchingterraphim_automataFST autocomplete, link generation, WASM
Multi-Agentterraphim_multi_agentAgent registry, capability discovery, workflows
MCP Serverterraphim_mcp_serverProtocol surface, schema enforcement
Capabilitiesterraphim_agent_registryCapability matching, score-based discovery
Persistenceterraphim_persistenceDeviceStorage, memory/file backends
\n

The MCP integration is bidirectional. Terraphim acts as an MCP server (exposing search and config tools to external clients) and can discover external MCP servers for additional capabilities. This enables multi-agent orchestration where each agent has its own Role and Knowledge Graph, but they can discover and invoke each other's tools through MCP.

\n

The Determinism Guarantee

\n

Let's be precise about what this architecture guarantees and what it doesn't.

\n

What IS guaranteed:

\n\n

What is NOT guaranteed:

\n\n

The key insight: We don't need the LLM to be deterministic. We need the KNOWLEDGE GRAPH and the PROTOCOL BOUNDARY to be deterministic. The LLM provides variable intelligence within fixed architectural bounds. The bounds are what matter for production.

\n

When This Architecture Is Overkill

\n

Not every AI system needs four layers of deterministic boundaries. Here's when you DON'T need MCP-native architecture:

\n\n

Add MCP-native boundaries when:

\n\n

Implementation Checklist

\n

Building MCP-native agents?

\n\n

Conclusion: The Protocol Is the Architecture

\n

The current generation of agent frameworks treats LLMs as the center of the universe. The LLM decides everything, and the framework provides convenient wrappers around API calls.

\n

This is backwards.

\n

The protocol is the architecture. The LLM is just one implementation detail.

\n

A production agent system needs:

\n\n

MCP provides the protocol surface. The reference architecture provides the safety properties. Rust provides the implementation guarantees.

\n

The LLM? It's just the reasoning engine. Powerful, probabilistic, and properly contained.

\n
\n

Reference Implementation

\n

The architecture described in this article is implemented in Terraphim, an open-source privacy-first AI assistant:

\n\n

Contributions welcome. Issues tracked in the Terraphim Gitea.

\n
\n

Alexander Mikhalev is CTO & Head of AI at Zestic AI, where he architects AI-native platforms with deterministic safety guarantees. He is the creator of Terraphim, an open-source privacy-first AI assistant built in Rust. His project \"The Pattern\" won the $10,000 Platinum Prize at the Redis \"Build on Redis\" Hackathon 2021 for ML-powered knowledge discovery. He previously led AI/ML architecture at Nationwide Building Society, where he co-authored the organization's first technology patent — a blockchain-inspired distributed system for resilient consensus. He speaks on AI architecture, Rust, and knowledge graphs.

\n", "https://reference-architecture.ai/properties/date": "2026-09-07", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["mcp","agents","rust","knowledge-graphs","production","terraphim"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/posts/harness-engineering-agents/", "https://atomicdata.dev/properties/name": "Harness Engineering for Self-Improving Agents", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/harness-engineering-agents/", "https://atomicdata.dev/properties/description": "

Many AI agent systems plateau after deployment. They execute tasks, but the surrounding system may not measure whether they are getting better, repeating old work, or normalizing stalled execution. This article proposes a reference pattern for agent harnesses: supervised feedback loops that make agent behavior observable, measurable, and easier to improve.

\n

The Problem: Agents That Don't Learn From Their Own Trace

\n

Most agent architectures follow a simple pattern:

\n
User Request -> Agent Reasoning -> Tool Execution -> Response
\n

This works for single-turn tasks. But over hundreds or thousands of runs, the agent can make the same mistakes, retry the same failing strategies, and generate the same redundant outputs. It may have no mechanism to:

\n\n

These are plausible failure modes in long-running agent systems: repeated status updates, hand-waved health scores, accumulated \"lessons learned\" that never change system behavior, and stale priorities that remain nominally critical while no one acts on them.

\n

This is not necessarily a bug in the agent. It is often a missing architectural layer.

\n

The Harness Architecture

\n

A harness is a feedback loop that monitors an agent's output, detects patterns, and triggers bounded corrective action. The proposed pattern uses four harness types, each addressing a specific failure mode:

\n\n\n\n\n\n
HarnessFailure ModeTriggerAction
Novelty FilterRedundant documentation accumulationSemantic similarity > thresholdSuppress redundant output
Calculated HealthNarrated metrics drift from realityDaily evaluation windowCompute objective health score
Break-GlassSystem paralysis without actionThreshold breachNotify, escalate, or reduce authorized scope
Lesson WindowLessons identified but never appliedAge > window or count > limitArchive stale active lessons
\n
+-------------------------------------------------------------+\n|                         AGENT CORE                          |\n|   +-----------+      +-----------+      +----------------+   |\n|   | Reasoning | ---> | Execution | ---> | Output         |   |\n|   +-----------+      +-----------+      +----------------+   |\n+-------------------------------------------------------------+\n                                |\n                                v\n+-------------------------------------------------------------+\n|                        HARNESS LAYER                        |\n|   +----------------+   +----------------+   +-------------+ |\n|   | Novelty Filter|   | Calc. Health   |   | Break-Glass | |\n|   | (dedup)       |   | (metrics)      |   | escalation  | |\n|   +----------------+   +----------------+   +-------------+ |\n|   +-------------------------------------------------------+ |\n|   |             Lesson Window (time-bounded)              | |\n|   +-------------------------------------------------------+ |\n+-------------------------------------------------------------+\n                                |\n                                v\n+-------------------------------------------------------------+\n|                        FEEDBACK LOOP                        |\n|        Metrics -> Review -> Approved Changes -> Policies    |\n+-------------------------------------------------------------+

Harness 1: Novelty Filter

\n

Problem: The agent may produce repeated documentation or status text that restates known issues without adding decision value.

\n

Illustrative implementation: This example is self-contained enough to show the state-management rule. In production, persist history outside the process and choose the embedding model, threshold, and retention period through evaluation.

\n
from dataclasses import dataclass\nfrom datetime import datetime, timedelta, timezone\n\nfrom sentence_transformers import SentenceTransformer\nimport numpy as np\n\n@dataclass(frozen=True)\nclass HistoryEntry:\n    content: str\n    embedding: np.ndarray\n    created_at: datetime\n\nclass NoveltyFilter:\n    def __init__(self, threshold=0.82, window_days=7):\n        self.model = SentenceTransformer("all-MiniLM-L6-v2")\n        self.threshold = threshold\n        self.window_days = window_days\n        self.history: list[HistoryEntry] = []\n\n    def is_novel(self, content: str) -> bool:\n        """Return True only if content is sufficiently novel."""\n        now = datetime.now(timezone.utc)\n        self._evict_expired(now)\n        new_embedding = self.model.encode(content)\n\n        for entry in self.history:\n            similarity = np.dot(new_embedding, entry.embedding) / (\n                np.linalg.norm(new_embedding) * np.linalg.norm(entry.embedding)\n            )\n            if similarity > self.threshold:\n                return False\n\n        self.history.append(HistoryEntry(content, new_embedding, now))\n        return True\n\n    def _evict_expired(self, now: datetime) -> None:\n        cutoff = now - timedelta(days=self.window_days)\n        self.history = [\n            entry for entry in self.history\n            if entry.created_at >= cutoff\n        ]
\n

Hypothesis: A correctly tuned novelty filter should reduce redundant persisted text while preserving new decisions, new evidence, and changed conclusions. A proposed target is a 30-50% reduction in persisted daily status text with manual review showing no loss of material decisions.

\n

Harness 2: Calculated Health

\n

Problem: Health metrics are often narrated rather than calculated. A stable phrase such as \"roughly healthy\" can mask variation in the underlying jobs.

\n

Illustrative implementation: The health calculator should receive concrete check functions. This avoids hidden dependencies on undefined methods and makes the metric auditable.

\n
from dataclasses import dataclass\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\nfrom typing import Callable\n\n@dataclass\nclass JobMetrics:\n    name: str\n    weight: float\n    check_func: Callable[[int], float]\n\nclass HealthCalculator:\n    def __init__(self, jobs: list[JobMetrics]):\n        total_weight = sum(job.weight for job in jobs)\n        if round(total_weight, 6) != 1.0:\n            raise ValueError("job weights must sum to 1.0")\n        self.jobs = jobs\n\n    def calculate(self, days: int = 7) -> float:\n        """Calculate health as weighted average of job success rates."""\n        total = 0.0\n        for job in self.jobs:\n            success_rate = job.check_func(days)\n            total += success_rate * job.weight\n        return round(total * 100, 1)\n\ndef recent_output_check(directory: Path, min_bytes: int = 1024) -> Callable[[int], float]:\n    """Return a check where success means one meaningful output per day."""\n    def check(days: int) -> float:\n        cutoff = datetime.now(timezone.utc) - timedelta(days=days)\n        outputs = [\n            path for path in directory.glob("*.md")\n            if datetime.fromtimestamp(path.stat().st_mtime, timezone.utc) >= cutoff\n            and path.stat().st_size >= min_bytes\n        ]\n        return min(len(outputs) / max(days, 1), 1.0)\n    return check\n\ncalculator = HealthCalculator([\n    JobMetrics("extraction", 0.40, recent_output_check(Path("runs/extraction"))),\n    JobMetrics("briefing", 0.30, recent_output_check(Path("runs/briefing"))),\n    JobMetrics("weekly_review", 0.30, recent_output_check(Path("runs/review"))),\n])
\n

Measurement definition: Health is the weighted average of job-specific success rates over a fixed evaluation window. Each job must define its own observable success condition before the run starts.

\n

Harness 3: Break-Glass

\n

Problem: Some systems need a fail-safe escalation path when important work stalls or health drops below an agreed threshold. Break-glass rules should not silently grant broad autonomy to the agent.

\n

Canonical configuration:

\n
[break_glass]\nenabled = true\nmode = "supervised"\n\n[[break_glass.rules]]\nname = "commit_gap"\ncondition = "days_since_commit > 14"\nseverity = "critical"\naction = "notify_and_require_ack"\nmessage = "Critical commit gap: human acknowledgement required."\n\n[[break_glass.rules]]\nname = "health_critical"\ncondition = "calculated_health < 15"\nseverity = "critical"\naction = "reduce_authorized_scope"\nallowed_jobs = ["extraction", "health_check"]\nmessage = "Health critical: non-essential jobs are paused within pre-authorized limits."\n\n[[break_glass.rules]]\nname = "lesson_accumulation"\ncondition = "unapplied_lessons > 50 AND oldest_unapplied_days > 14"\nseverity = "warning"\naction = "notify_owner"
\n

Rule semantics:

\n
notify_and_require_ack:\n  Send an escalation and block risky automatic changes.\n\nreduce_authorized_scope:\n  Automatically disable only jobs already marked non-essential.\n\nnotify_owner:\n  Create a visible warning with no automatic operational change.
\n

Abbreviated trigger evaluation: This sketch assumes Rule.evaluate is a safe expression evaluator over a constrained state object, not arbitrary code execution.

\n
class BreakGlass:\n    def evaluate(self, system_state: dict) -> list[Trigger]:\n        triggers = []\n        for rule in self.rules:\n            if rule.evaluate(system_state):\n                triggers.append(Trigger(\n                    rule=rule.name,\n                    severity=rule.severity,\n                    action=rule.action,\n                    message=rule.message.format(**system_state),\n                ))\n        return triggers
\n

Fail-safe principle: Notification and escalation remain available through approved channels and rate limits. Automatic action is limited to pre-authorized scope reduction, such as pausing non-essential jobs. Anything outside that predefined reduction policy—including changing data, deleting history, shipping code, or broadening production behavior—requires human authorization.

\n

Harness 4: Lesson Window

\n

Problem: \"Lessons learned\" can become a growing active queue that never changes system behavior.

\n

Illustrative implementation: The window archives stale active lessons. It does not prove that lessons have been applied, and it does not force application.

\n
from datetime import datetime, timedelta, timezone\n\nclass LessonWindow:\n    def __init__(self, window_days: int = 7, max_unapplied: int = 30):\n        self.window = timedelta(days=window_days)\n        self.max_unapplied = max_unapplied\n\n    def process(self, lessons: list[Lesson]) -> tuple[list[Lesson], list[Lesson]]:\n        """Return (active_lessons, archived_lessons)."""\n        active = []\n        archived = []\n        now = datetime.now(timezone.utc)\n        candidates = [\n            lesson for lesson in lessons\n            if lesson.status not in {"applied", "archived"}\n        ]\n        overflow = max(len(candidates) - self.max_unapplied, 0)\n\n        for lesson in lessons:\n            if lesson.status == "archived":\n                archived.append(lesson)\n                continue\n            if lesson.status == "applied":\n                active.append(lesson)\n                continue\n\n            age = now - lesson.created_at\n\n            if age > self.window:\n                lesson.archive_once(reason=f"expired_after_{self.window.days}_days")\n                archived.append(lesson)\n            elif overflow > 0:\n                lesson.archive_once(reason="overflow")\n                archived.append(lesson)\n                overflow -= 1\n            else:\n                active.append(lesson)\n\n        return active, archived
\n

Rule: A lesson is only \"learned\" if system state changes because of it. If not applied within the active window, it can be archived, not forgotten, so that it no longer clutters the active queue.

\n

Integration Architecture

\n

The harnesses integrate at the output boundary of the agent core, before content is persisted to memory:

\n
Agent Output\n    |\n    v\n+-----------------+\n| Novelty Filter  | --> Suppress? Yes: discard and log filter event\n+-----------------+     No: continue\n    |\n    v\n+-----------------+\n| Calculate Health| --> Update metrics store\n+-----------------+\n    |\n    v\n+-----------------+\n| Break-Glass     | --> Notify or apply authorized reduction\n+-----------------+\n    |\n    v\n+-----------------+\n| Lesson Window   | --> Archive expired, queue active\n+-----------------+\n    |\n    v\nPersist to Memory
\n

This placement is important: harnesses operate on output, not input. Filtering input could prevent useful ideas from reaching the agent. Filtering output ensures only valuable, non-redundant content is preserved.

\n

Deployment Pattern

\n

Configuration

\n
# harness.toml\n[novelty_filter]\nenabled = true\nthreshold = 0.82\nwindow_days = 7\nmodel = "all-MiniLM-L6-v2"\n\n[health]\nenabled = true\ncalculation_window_days = 7\njobs = [\n    { name = "extraction", weight = 0.40, check = "recent_output:runs/extraction" },\n    { name = "briefing", weight = 0.30, check = "recent_output:runs/briefing" },\n    { name = "weekly_review", weight = 0.30, check = "recent_output:runs/review" },\n]\n\n[break_glass]\nenabled = true\nmode = "supervised"\n\n[[break_glass.rules]]\nname = "commit_gap"\ncondition = "days_since_commit > 14"\nseverity = "critical"\naction = "notify_and_require_ack"\n\n[[break_glass.rules]]\nname = "health_critical"\ncondition = "calculated_health < 15"\nseverity = "critical"\naction = "reduce_authorized_scope"\nallowed_jobs = ["extraction", "health_check"]\n\n[[break_glass.rules]]\nname = "lesson_accumulation"\ncondition = "unapplied_lessons > 50 AND oldest_unapplied_days > 14"\nseverity = "warning"\naction = "notify_owner"\n\n[lesson_window]\nenabled = true\nwindow_days = 7\nmax_unapplied = 30

Platform-Neutral Integration Contract

\n

The following is an illustrative contract, not a schema from a real product. The goal is to show the integration boundary a scheduler, orchestrator, or agent runtime would need to provide.

\n
{\n  "event": "agent.output.proposed",\n  "timestamp": "2026-09-06T23:00:00Z",\n  "run_id": "run_123",\n  "agent_id": "research_agent",\n  "content": "Proposed output content",\n  "metrics": {\n    "days_since_commit": 15,\n    "calculated_health": 42.5,\n    "unapplied_lessons": 12\n  }\n}

Evaluation Plan

\n

As of 2026-09-07, a 14-day evaluation starting on 2026-09-06 is not complete. A coherent 14-day inclusive window would run from 2026-09-06 through 2026-09-19. The table below is a prospective evaluation plan, not reported results.

\n\n\n\n\n\n
MetricBaseline DefinitionTreatment DefinitionTarget / Hypothesis
Daily persisted textBytes of agent-generated status or lesson text per daySame measure after novelty filtering30-50% reduction without losing material decisions
Health score stabilitySeven-day weighted health score from fixed checksSame measure after calculated health is introducedChanges should track job outcomes rather than remain narratively fixed
Break-glass behaviorNumber of threshold breaches that produce no visible escalationNumber of threshold breaches with notification, acknowledgement, or authorized scope reductionCritical breaches should create reviewable events
Lesson queue ageAge of oldest active unapplied lessonSame measure after lesson window archivalActive queue should remain bounded
\n

Methodology

\n
    \n
  1. Freeze the rule set, thresholds, job weights, and measurement definitions before the evaluation starts.
  2. \n
  3. Run a seven-day baseline with harnesses disabled, then a seven-day treatment with the same workload and harnesses enabled.
  4. \n
  5. Record raw events: outputs proposed, outputs persisted, filter decisions, health inputs, health score, threshold breaches, notifications, scope reductions, lessons created, lessons applied, and lessons archived.
  6. \n
  7. Manually audit a sample of filtered outputs for false positives: content that was suppressed but should have been retained.
  8. \n
  9. Report all metrics as observations only after the window closes. Do not backfill missing data or infer results from anecdotes.
  10. \n
\n

Sample size and limitations: A single 14-day run is enough to detect obvious integration failures and generate hypotheses, but it is not enough to claim general performance. Workload mix, agent prompts, human review behavior, and calendar effects can dominate the result. Treat the outcome as local evidence for this system, not proof that the pattern works everywhere.

\n

Related-work caveat: This pattern overlaps with established ideas in observability, control loops, MLOps monitoring, guardrails, and human-in-the-loop operations. The novelty here, if any, is the packaging of those ideas around agent output boundaries. This article does not claim a new algorithm or publish comparative research.

\n

Failure Modes

\n

Over-Filtering

\n

If the novelty threshold is too aggressive, genuinely novel but structurally similar content may be suppressed. Mitigation: Start with a conservative threshold, then adjust based on measured filter hit rate and manual false-positive review. The target should be meaningful reduction, not maximum suppression.

\n

Break-Glass Fatigue

\n

If thresholds are too tight, break-glass triggers too frequently and is ignored. Mitigation: Reserve critical alerts for conditions that require review. Warning-level thresholds should log or notify without interrupting work.

\n

Lesson Loss

\n

Archiving lessons after a fixed window may hide valuable but long-term insights. Mitigation: Archived lessons remain searchable. The archive is cold storage: retrievable, but not cluttering the active queue.

\n

Conclusion

\n

The harness architecture addresses a practical gap in agent design: agents need feedback loops about their own behavior, not just about the tasks they execute. Without these loops, systems can accumulate redundant memory, drifted metrics, and normalized paralysis.

\n

The four harnesses (novelty filter, calculated health, break-glass, lesson window) form a proposed minimal layer for supervised agent improvement. They require a small, explicit contract from the agent core—output events, job metrics, and authorization state—plus careful placement at the output boundary and human authorization for risky actions.

\n

Key Takeaway: An agent that cannot detect its own stagnation will stagnate. Harnesses are the architectural layer that makes stagnation visible, reviewable, and fixable.

\n
\n

This is a proposed reference pattern. The methodology above is intended to make future evaluation reproducible without relying on private supporting material.

\n

License: CC BY-SA 4.0

\n", "https://reference-architecture.ai/properties/date": "2026-09-06", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["agents","self-improvement","harnesses","feedback-loops"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/posts/github-oauth2/", "https://atomicdata.dev/properties/name": "Turning Open Source project into Product with Redis Enterprise", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/github-oauth2/", "https://atomicdata.dev/properties/description": "

Turning Open Source project into Product with Redis Enterprise

\n

Overview

\n

Background

\n

History

\n

Last year, my reference project, \"The Pattern\", was the hackathon winner 2021 and got a bit of publicity and, in total, seven forks. But as with many open source projects, it is now stale. Time to revive \"The Pattern\" with new features and GitHub sponsors or Patreon patrons to help and inspire developers and creatives. In return, it's common to provide sponsor-only features and articles. Nevertheless, how can we do it with a large Redis-based machine learning pipeline?

\n

Plan sponsor only features

\n

This article will introduce a simple first step:\nfor GitHub sponsors, we start with offering persistent storage of preferences: I have a simple flask POST API which adds nodes into the user's preference storage - a simple Redis set per user. And it will be a foundation to build other sponsor-only features.\nFor now, let's cover the basics:

\n

Overall architecture overview

\n
\nflowchart LR\n    id1(User) --> flask_login(Flask Login API)--> github(GitHub OAuth2)\n    github-->flask_callback(Flask API callback)-->GitHubGraphQL(GitHub GraphQL)\n
\n

Add Github oauth2 to Rest API

\n

There are a number of API's that GitHub offers to help developers, but the GitHub Authentication API is one of the most popular. This API allows you to log in to GitHub using your username and password, or OAuth token.

\n

A login button with a standard OIDC/OAuth2 dance is one of the most common ways for a user to authenticate to an API.\nBelow is code taken from this gist and is very common for OAuth2 flows:

\n
import os \nclient_id = os.getenv('GITHUB_CLIENT_ID')\nclient_secret = os.getenv('GITHUB_SECRET')\n@app.route('/', methods=['GET', 'POST'])\ndef index():\n    url = 'https://github.com/login/oauth/authorize'\n    params = {\n        'client_id': client_id,\n\n        'scope': 'read:user,read:email',\n        'state': str(uuid4().hex),\n        'allow_signup': 'true'\n    }\n    url = furl(url).set(params)\n    return redirect(str(url), 302)
\n

where GITHUB_CLIENT_ID and GITHUB_SECRET are client/secret GitHub Oauth2 apps. Register for following process on GitHub

\n\n
org_name="applied-knowledge-systems"\n@app.route('/oauth2/callback')\ndef oauth2_callback():\n\n    code = request.args.get('code')\n    access_token_url = 'https://github.com/login/oauth/access_token'\n    payload = {\n        'client_id': client_id,\n        'client_secret': client_secret,\n        'code': code,\n        # 'redirect_uri':\n        'state': str(uuid4().hex)\n    }\n    r = requests.post(access_token_url, json=payload, headers={'Accept': 'application/json'})\n    access_token = json.loads(r.text).get('access_token')\n    print(access_token)\n    \n    access_user_url = 'https://api.github.com/user'\n    response = requests.get(access_user_url, headers={'Authorization': 'token ' + access_token})\n    data=response.json()\n    user_email=data["email"]\n    user_login=data["login"]\n    user_id=data["id"]\n    # response=redirect(url_for('login',next=redirect_url()))\n    # response.set_cookie('user_id', str(user_id))\n    # response.set_cookie('user_login', str(user_login))\n    # return response\n    query = """\n        {\n        viewer {\n            sponsorshipsAsSponsor(first: 100) {\n            nodes {\n                sponsorable {\n                ... on User {\n                    id\n                    email\n                    url\n                }\n                ... on Organization {\n                    id\n                    email\n                    name\n                    url\n                }\n                }\n                tier {\n                id\n                name\n                monthlyPriceInDollars\n                monthlyPriceInCents\n                }\n            }\n            }\n        }\n        }\n        """\n    response_graphql = requests.post('https://api.github.com/graphql', json={'query': query}, headers={'Authorization': 'token ' + access_token})\n    response_graphql_data=response_graphql.json()["data"]\n    if isinstance (response_graphql_data["viewer"]["sponsorshipsAsSponsor"]["nodes"], list):\n      if response_graphql_data["viewer"]["sponsorshipsAsSponsor"]["nodes"][0]["sponsorable"]["name"]==org_name:\n          # if user is a sponsor of Applied Knowledge System add them to set of sponsors\n         redis_client.sadd(f'sponsors:{org_name}',user_id)\n    # if RedisJSON enabled:\n    # redis_client.json().set(f"user_details:{user_id}", '$', {\n    #     'email': user_email,\n    #     'id': user_id,\n    #     'user_login': user_login,\n    #     'graphql': response_graphql_data,\n    # }) \n    #if not\n    redis_client.hset(f"user_details:{user_id}", mapping={\n        'email': user_email,\n        'id': user_id,\n        'user_login': user_login\n    })\n    return jsonify({\n        'status': 'success',\n        'email': user_email,\n        'id': user_id,\n        'user_login': user_login\n    })
\n

The API we are using for our sponsor-only feature is straightforward:

\n
@app.route('/exclude', methods=['POST','GET'])\ndef mark_node():\n    if request.method == 'POST':\n        if 'id' in request.json:\n            node_id=request.json['id']\n    else:\n        if 'id' in request.args:\n            node_id=request.args.get('id')\n    user_id = session.get('user_id')\n    log(f"Got user {user_id} from session")\n    if not user_id:\n        user_id = request.cookies.get('user_id')\n        log(f"Got user {user_id} from cookie")\n    redis_client.sadd("user:%s:mnodes" % user_id,node_id)\n    response = jsonify(message=f"Finished {node_id} and {user_id}")\n    return response
\n

And the only purpose of this API is to mark nodes as unimportant for the given user by adding nodes to RedisSet, and those nodes will be excluded from search API output. So far, everything was pretty standard: basic flask API and GitHub Social login flow. Now let's add Redis Enterprise and synchronise sponsors preferences.

\n

Add Redis Enterprise

\n

Why not use Redis Enterprise directly for everything?\nThe project is memory-heavy, with a lot of data and machine learning inside Redis. This allows to achieve state-of-the-art performance, but it also takes over 120 GB RAM (or as much RAM as you can give it), and 128 GB Redis Enterprise instance will exceed my budget for open-source project. Obviously if there will be enough sponsors we can move more functionality into Redis Enterprise, but for that we need to finish building basic blocks. Register on Redis.com cloud and create a database with the subscription.

\n

\"Redis\nTake a note host, port and password for Redis Enterprise and create docker enviroment file:

\n
cat .env.gears \nREDISENT_PWD="123"\nREDISENT_PORT="13444"\nREDISENT_HOST="hostname.cloud.redislabs.com"
\n

and create a docker compose with section passing .env.gears. Mine looks like this:

\n
  redisgraph:\n    image: redislabs/redismod\n    container_name: redisgears\n    hostname: redisgears\n    env_file:\n      - ./.env.gears\n    ports:\n      - 127.0.0.1:9001:6379\n

Synchronize Redis OSS to Redis Enterprise using RedisGears

\n

Synchronize all user preferences

\n

First flow:\nWe will be using RedisGears to synchronize all preferences with Redis Enterprise

\n
\nflowchart LR\n    id1(User Preferences Redis OSS) --> redis_gears1(Redis Gears)--> redise(Redis Enterprise)\n
\n

If you are new to RedisGears, there is a pattern rgsync that covers exacly this use case, but I already have RedisGears, so I am going to build it step by step:

\n
# gears_sync_preferences.py\nrconn=None\n\ndef connecttoRedisEnterise():\n    import redis \n    import os \n    log(str(os.environ))\n    # Get environment variables\n\n    HOST = os.getenv('REDISENT_HOST')\n    PASSWORD = os.getenv('REDISENT_PWD')\n    PORT = os.getenv('REDISENT_PORT')\n\n    redis_client=redis.Redis(host=HOST,port=PORT,charset="utf-8", password=PASSWORD, decode_responses=True)\n    return redis_client\n\ndef sync_users(record):\n    global rconn\n    if not rconn:\n        rconn=connecttoRedisEnterise()\n    # Uncomment logs to check \n    # log(str(record['key']))\n    # log(str(record['value']))\n\n    rconn.hset(record['key'],mapping=record['value'])\n\ngb = GB()\ngb.foreach(sync_users)\ngb.count()\ngb.run('user_details:*')
\n

this is a \"batch\" mode for RedisGears, which is easier to debug than streams. Install gears-cli](https://github.com/RedisGears/gears-cli) with pip install gears-cli run above script:

\n
gears-cli run --host 127.0.0.1 --port 9001 gears_sync_preferences.py --requirements req_sync.txt
\n

where req_sync.txt

\n
redis==3.5.3
\n

This RedisGears will copy all user's profiles into RedisEnterprise. Now let us add sponsors:

\n
# gears_sync_sponsors.py\nrconn=None\n\ndef remove_prefix(text, prefix):\n    return text[text.startswith(prefix) and len(prefix):]\n\ndef connecttoRedisEnterise():\n    import redis \n    import os \n    log(str(os.environ))\n    # Get environment variables\n\n    HOST = os.getenv('REDISENT_HOST')\n    PASSWORD = os.getenv('REDISENT_PWD')\n    PORT = os.getenv('REDISENT_PORT')\n    log(HOST)\n    log(PORT)\n    log(PASSWORD)\n    redis_client=redis.Redis(host=HOST,port=PORT,charset="utf-8", password=PASSWORD)\n    return redis_client\n\n\n\ndef sync_sponsors(record):\n    global rconn\n    if not rconn:\n        rconn=connecttoRedisEnterise()\n\n    log(str(record['key']))\n    values=execute('SMEMBERS',record['key'])\n    log(str(values))\n    for each_value in values:    \n        rconn.sadd(record['key'],each_value)\n\ngb = GB('KeysReader')\ngb.foreach(sync_sponsors)\ngb.count()\ngb.run('user:*')
\n

But this one will sync all user's preferences, but we only need sponsors - let us add another feature of RedisGears - filter:

\n
rconn=None\n\ndef remove_prefix(text, prefix):\n    return text[text.startswith(prefix) and len(prefix):]\n\ndef connecttoRedisEnterise():\n    import redis \n    import os \n    log(str(os.environ))\n    # Get environment variables\n\n    HOST = os.getenv('REDISENT_HOST')\n    PASSWORD = os.getenv('REDISENT_PWD')\n    PORT = os.getenv('REDISENT_PORT')\n\n    redis_client=redis.Redis(host=HOST,port=PORT,charset="utf-8", password=PASSWORD, decode_responses=True)\n    return redis_client\n\ndef filter_sponsors(record):\n    org_name="applied-knowledge-systems"\n    user_id = remove_prefix(record['key'],'user:')\n    sponsor=execute('SISMEMBER',f'sponsors:{org_name}',user_id)\n    return bool(sponsor==True)\n\ndef sync_sponsors(record):\n    global rconn\n    if not rconn:\n        rconn=connecttoRedisEnterise()\n\n    log(str(record['key']))\n    values=execute('SMEMBERS',record['key'])\n    log(str(values))\n    for each_value in values:    \n        rconn.sadd(record['key'],each_value)\n\ngb = GB()\ngb.filter(filter_sponsors)\ngb.foreach(sync_sponsors)\ngb.count()\ngb.run('user:*')

Fetch sponsor's preferences back to Redis OSS from Redis Enterprise

\n

Then we are going to use Key miss events from Redis Gears to fetch data for all users:

\n
\nflowchart LR\n    redise(Redis Enterprise)\n    redis_gears2(Redis Gears)--key miss--->redise\n    redis_gears2-->redisOSS[Redis OSS]\n
\n

and it's very easy, right from key miss example:

\n
\ndef fetch_data(r):\n    key = r['key']\n    global rconn\n    if not rconn:\n        rconn=connecttoRedisEnterise()\n    values=rconn.smembers(record['key'])\n    log(str(values))\n    for each_value in values:    \n        execute('SADD',record['key'],each_value)\n\nGB().foreach(fetch_data).register(prefix='user:*', commands=['smember'],eventTypes=['keymiss'], mode="async_local")
\n

There is one more option - to turn fetch_data into the async call, by wrapping it into async/await, but Redis Enterprise is fairly fast, and I don't think it's worth adding an async call in this case. For curiosity, see the example code in The Pattern repository.

\n

Conclusion

\n

In this article, we walked through steps on how to create sponsor-specific \"nanoservices\" using RedisOSS, RedisGears and Redis Enterprise. This allows us to leverage the best of all worlds open source Redis, high availability and persistence with Redis Enterprise and RedisGears as the glue which holds everything together.

\n

This post is in collaboration with Redis.

\n

References

\n\n", "https://reference-architecture.ai/properties/date": "2022-08-19", "https://atomicdata.dev/properties/tags": {"categories":["Redis"],"tags":["Redis Enterprise","roadmap","product","oauth2","github","sponsors"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/posts/post-0/", "https://atomicdata.dev/properties/name": "Announcing Reference Architecture for AI", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/posts/post-0/", "https://atomicdata.dev/properties/description": "

There are tools for advanced analytics, including free ones from Google and Kaggle.

\n

There are well-known and validated deployment architectures for applications and the cloud.

\n

Yet the number of practical applications is still tiny, and they retained niche implementations.\nWhile the benefits of AI are clear, there are still many gaps in AI architecture that need to be filled. For example, there is a gap between analytical tools and verified architectures for real-time deployments. This gap often stems from a lack of specific reference architectures and patterns, demonstrating the trade-offs between technologies, libraries, and tools.

\n

Let's bridge the gap in knowledge and drive a connection between science and engineering to make fast, efficient, and practical AI deployments.\nThree things need to be in place to build an AI product:

\n
    \n
  1. AI Product itself
  2. \n
  3. Core Capabilities required to build AI/ML product
  4. \n
  5. Enabling capabilities
  6. \n
\n

I will use The Pattern, my [“Build on Redis” Hackathon prize-winning open source](https://github.com/applied-knowledge-systems/the-pattern) project, to illustrate how the capabilities below can be implemented and invite you to contribute or donate.

\n

We launch in two full-featured articles - NLP ML pipeline for turning unstructured JSON text into a knowledge graph and fresh off the press Benchmarks for BERT Large Question Answering inference for RedisAI and RedisGears with Grafana Dashboards by Mikhail Volkov

\n", "https://reference-architecture.ai/properties/date": "2022-06-16", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["reference architecture","ai","announcement"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/patterns/", "https://atomicdata.dev/properties/name": "The Twelve Core Patterns", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/patterns/", "https://atomicdata.dev/properties/description": "

The harness, not the model, determines agent success. A harness has three jobs — deciding\nwhat the model sees, enforcing what it can do, and ensuring it learns from its own history\n— and each job is served by four patterns.

\n

Every pattern below is documented elsewhere on this site. This page is the index, not the\nexplanation: each entry links to where the argument is actually made.

\n

Context Architecture

\n

What the model sees at each step. Context is a scarce resource: a 1M token window with\n800K of noise performs worse than 200K with 150K of curated signal.

\n

1. Structural Context Boundary

\n

Put the boundary in configuration, not in a runtime permission check. A role has access\nto certain sources; the knowledge graph is built from those sources; the agent therefore\ncannot reach anything outside its role. Not a check that could be bypassed — an absence.\nThe data was never loaded.

\n

→ The Terraphim Engine, Layer 1

\n

2. Deterministic Concept Matching

\n

Resolve text to concepts with finite state automata rather than vector similarity.\nAho-Corasick finds every match in O(n) time, with the same result every run. Probabilistic\nretrieval gives you a different context window each time you ask, which makes every\ndownstream failure irreproducible.

\n

→ The MCP-Native Agent Architecture, Layer 2

\n

3. Structured Compaction

\n

Compact before the window fills, not when it overflows. By 100K tokens a working context\nis largely noise: superseded search results, abandoned reasoning paths, old file reads.\nAdding capacity without compaction makes performance worse, not better.

\n

→ Three ways 1M token windows fail

\n

4. Priority-Weighted Instruction Injection

\n

Instructions compete, and by default the loudest wins rather than the most important. The\nsystem prompt says one thing, the project file another, the README a third. Weight them\nexplicitly and re-inject the high-priority ones, rather than trusting position in a\ntranscript.

\n

→ Three ways 1M token windows fail

\n

Execution Guardrails

\n

What the model can and cannot do. Risk-tiered, machine-readable, and enforced by the\nsystem rather than requested in a prompt.

\n

5. Multi-Tenant Agent Isolation

\n

Agents sharing a protocol layer must not share state. Each agent keeps its own role\nconfiguration and its own knowledge graph, discovering the others' tools without\ndiscovering their data.

\n

→ Pattern 1: Multi-Tenant Agent Isolation

\n

6. Deterministic Replay

\n

An agent run you cannot replay is an agent run you cannot debug. Record the inputs and the\ndecisions such that the same run can be reconstructed exactly — which is only possible if\nthe layers underneath are themselves deterministic.

\n

→ Pattern 2: Deterministic Replay

\n

7. Human-in-the-Loop for Destructive Operations

\n

Classify tools by effect, not by name. Read-only operations run freely; destructive ones\nstop for approval. The protocol enforces the distinction, so an agent cannot talk its way\npast it.

\n

→ Pattern 3: Human-in-the-Loop for Destructive Operations

\n

8. Break-Glass

\n

Every guardrail needs a deliberate, logged way through it. A guardrail with no override\ngets disabled wholesale the first time it blocks something urgent; an override that is\nnot recorded is indistinguishable from the guardrail not existing.

\n

→ Harness 3: Break-Glass

\n

Memory Infrastructure

\n

Ensuring the model learns from its own history. Without it, agents repeat solved\nproblems indefinitely and no amount of context helps.

\n

9. Novelty Filter

\n

Distinguish output that is new from output that merely looks new. An agent that cannot\ntell repetition from progress will re-report the same finding until someone stops reading.

\n

→ Harness 1: Novelty Filter

\n

10. Calculated Health

\n

Derive a health signal from observable metrics rather than asking the agent how it is\ndoing. Self-assessment is the least reliable input available; commit recency, unapplied\nlessons and stall time are not.

\n

→ Harness 2: Calculated Health

\n

11. Lesson Window

\n

Capture corrections at the moment of failure and apply them within a bounded window.\nLessons with no expiry become noise; lessons never applied were never lessons.

\n

→ Harness 4: Lesson Window

\n

12. Drift Detection

\n

Long-running agents optimise for local coherence and lose the original objective. Ask\nperiodically, and mechanically: do we actually have the thing we set out to build? The\nquestion has to be asked by the harness, because an agent that has drifted cannot detect\nits own drift.

\n

→ Three ways 1M token windows fail ·\nThe nine-step orchestration arc

\n

On this list

\n

These twelve are the patterns this site currently makes an argument for, grouped by\nthe three jobs of an agent harness. The grouping is deliberate: a pattern\nthat does not serve one of those three jobs probably belongs in a different architecture.

\n

The list is not a claim to completeness. It is what is documented, and the honest test of\nany addition is whether there is an article behind it.

\n", "https://reference-architecture.ai/properties/date": "2026-09-08", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["reference-architecture","patterns","agents","harnesses"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/donate/", "https://atomicdata.dev/properties/name": "Support the project", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/donate/", "https://atomicdata.dev/properties/description": "

The patterns described on this site are not theory. They are extracted from\nTerraphim, a privacy-first AI assistant built across 52 Rust\ncrates, and every one of those crates is open source.

\n

The most valuable support is use and scrutiny.

\n

Use it

\n\n

Contribute

\n

Issues, benchmarks and architecture critique are worth more than money here. If a\ntrade-off on this site looks wrong, say so — with evidence — at\ngithub.com/terraphim. Deployment guides and reproducible\nbenchmarks are especially welcome.

\n

Sponsor

\n

If your organisation depends on this work, Terraphim Pro funds\nthe open-source engine directly: connectors, an LLM proxy and agentic skills for\nenterprise deployment.

\n", "https://reference-architecture.ai/properties/date": "2026-09-08", "https://atomicdata.dev/properties/tags": {"categories":["donations","open-source"],"tags":["donate","support","terraphim"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/legacy-capability-map/", "https://atomicdata.dev/properties/name": "Capability Map (2020-2022)", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/legacy-capability-map/", "https://atomicdata.dev/properties/description": "

Introduction

\n

There are tools for advanced analytics, including free ones from Google and Kaggle.

\n

There are well-known and validated deployment architectures for applications and the cloud.

\n

Yet the number of practical applications is still tiny, and they retained niche implementations.\nWhile the benefits of AI are clear, there are still many gaps in AI architecture that need to be filled. For example, there is a gap between analytical tools and verified architectures for real-time deployments. This gap often stems from a lack of specific reference architectures and patterns, demonstrating the trade-offs between technologies, libraries, and tools.

\n

Let's bridge the gap in knowledge and drive a connection between science and engineering to make fast, efficient, and practical AI deployments.

\n

Three things need to be in place to build an AI product:

\n
    \n
  1. AI Product itself
  2. \n
  3. Core Capabilities required to build AI/ML product
  4. \n
  5. Enabling capabilities
  6. \n
\n

I will use The Pattern, my Build on Redis Hackathon prize-winning open source project to illustrate how the capabilities below can be implemented and invite you to contribute or donate.\nThe diagrams below are clickable.

\n

AI Product

\n

\n

Core capabilities for AI/ML

\n
\nflowchart LR; \nsubgraph \" \"\n  data_intake(Data Acquisition);\n  data_preprocessing(Data Preprocessing);\n  data_validation(Data Validation);\n  data_streaming(Data Streaming);\n  ai_ml_pipe(AI ML Pipeline 
Model Training/Test/Validation);\n kg(Knowledge Graph);\n ML_inference(ML Inference);\n interaction(Interaction Layer
Voice/VR/AR/Meta);\n click data_intake \"/docs/intake/\" \"Data Acquisition\"\n click ML_inference \"/docs/bert-qa-benchmarking/\" \"BERT Large Question Answering\"\n click kg \"/docs/nlp/#what-is-a-knowledge-graph\" \"Knowledge Graph\"\n click data_preprocessing \"/docs/nlp/#redisgears-for-nlp-pre-processing\" \"Data pre-processing\"\n click data_streaming \"/docs/nlp/#goal\" \"Data Streaming\"\n click ai_ml_pipe \"/docs/nlp/#overall-architecture-overview-components-diagram\" \"Overall ML pipeline\"\nend\n
\n

Enabling Capabilities

\n
\nflowchart LR;\nsubgraph 2 [Enabling Capabilities]\n  subgraph \" \"\nD1[Data Governance]\nD2[Data Quality Management]\nD3[Metadata Management]\nclick D3 \"/docs/metadata/\" \"Metadata Management\"\nend\nsubgraph \" \"\n  id2[ML performance and bias monitoring];\n  id3[Application Performance Monitoring];\n  id4[Hardware Performance Monitoring];\n  click id3 \"/docs/bert-qa-benchmarking/#running-the-benchmark\" \"Running Benchmarks\"\n  click id4 \"/docs/bert-qa-benchmarking/#using-grafana-to-monitor-redisgears-throughput-cpu-and-memory-usage\" \"CPU and Memory Benchmarks\"\nend\nsubgraph \" \"\n  id5[DevOps: Continuous Integration/Continuous Deployment];\n  c1[Configuration management]\n  c2[Collaboration and knowledge management tooling]\n  c3[Change Management]\nend \n  subgraph 3 [Self Serving Infrastructure]\n    s1[Computing Infrastructure]\n    s2[Serving infrastructure]\n  end\n  end\n  style 2 fill:#485fc754,stroke:#333,stroke-width:4px;\n
\n", "https://reference-architecture.ai/properties/date": "2022-06-16", "https://atomicdata.dev/properties/tags": {"categories":["Architecture"],"tags":["reference-architecture","ai-product"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/bert-qa-benchmarking/", "https://atomicdata.dev/properties/name": "Benchmarks for BERT Large Question Answering inference for RedisAI and RedisGears", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/bert-qa-benchmarking/", "https://atomicdata.dev/properties/description": "

Summary of the article

\n

This article will explore the challenges and opportunities of deploying a large BERT Question Answering Transformer model(bert-large-uncased-whole-word-masking-finetuned-squad) from inside Huggingface, where RedisGears and RedisAI perform heavy lifting while leveraging in-memory datastore Redis.

\n

Why do we need RedisAI?

\n\n

Some numbers for inspiration and why to read this article:

\n
python3 transformers_plain_bert_qa.py \nairborne transmission of respiratory infections is the lack of established methods for the detection of airborne respiratory microorganisms\n10.351818372 seconds
time curl -i -H "Content-Type: application/json" -X POST -d '{"search":"Who performs viral transmission among adults"}' http://localhost:8080/qasearch\n\nreal\t0m0.747s\nuser\t0m0.004s\nsys\t0m0.000s\n

Background

\n

BERT Question Answering inference works where the ML model selects an answer from the given text. In other words, BERT QA \"thinks\" through the following: \"What is the answer from the text, assuming the answer to the question exists within the paragraph selected.\"

\n

So it's important to select text potentially containing an answer. A typical pattern is to use Wikipedia data to build Open Domain Question Answering.

\n

Our QA system is a medical domain-specific question/answering pipeline, hence we need a first pipeline that turns data into a knowledge graph. This NLP pipeline is available at Redis LaunchPad, is fully open source, and is described in a previous article. Here is a 5 minute video describing it, and below you will find an architectural overview:

\n

\"featured\"

\n

BERT Question Answering pipeline and API

\n

In the BERT QA pipeline (or in any other modern NLP inference task), there are two steps:

\n
    \n
  1. Tokenize text - turn text into numbers
  2. \n
  3. Run the inference - large matrix multiplication
  4. \n
\n

With Redis, we have the opportunity to pre-compute everything and store it in memory, but how do we do it? Unlike with the summarization ML learning task, the question is not known in advance, so we can't pre-compute all possible answers. However, we can pre-tokenize all potential answers (i.e. all paragraphs in the dataset) using RedisGears:

\n
def parse_sentence(record):\n    import redisAI\n    import numpy as np\n    global tokenizer\n    if not tokenizer:\n        tokenizer=loadTokeniser()\n    hash_tag="{%s}" % hashtag()\n\n    for idx, value in sorted(record['value'].items(), key=lambda item: int(item[0])):\n        tokens = tokenizer.encode(value, add_special_tokens=False, max_length=511, truncation=True, return_tensors="np")\n        tokens = np.append(tokens,tokenizer.sep_token_id).astype(np.int64)\n        tensor=redisAI.createTensorFromBlob('INT64', tokens.shape, tokens.tobytes())\n\n        key_prefix='sentence:'\n        sentence_key=remove_prefix(record['key'],key_prefix)\n        token_key = f"tokenized:bert:qa:{sentence_key}:{idx}"\n        redisAI.setTensorInKey(token_key, tensor)\n        execute('SADD',f'processed_docs_stage3_tokenized{hash_tag}', token_key)\n
\n

See the full code on GitHub.

\n

Then for each Redis Cluster shard, we pre-load the BERT QA model by downloading, exporting it into torchscript, then loading it into each shard:

\n
def load_bert():\n    model_file = 'traced_bert_qa.pt'\n\n    with open(model_file, 'rb') as f:\n        model = f.read()\n    startup_nodes = [{"host": "127.0.0.1", "port": "30001"}, {"host": "127.0.0.1", "port":"30002"}, {"host":"127.0.0.1", "port":"30003"}]\n    cc = ClusterClient(startup_nodes = startup_nodes)\n    hash_tags = cc.execute_command("RG.PYEXECUTE",  "gb = GB('ShardsIDReader').map(lambda x:hashtag()).run()")[0]\n    print(hash_tags)\n    for hash_tag in hash_tags:\n        print("Loading model bert-qa{%s}" %hash_tag.decode('utf-8'))\n        cc.modelset('bert-qa{%s}' %hash_tag.decode('utf-8'), 'TORCH', 'CPU', model)\n        print(cc.infoget('bert-qa{%s}' %hash_tag.decode('utf-8')))
\n

The full code is available on GitHub.

\n

And when a question comes from the user, we tokenize and append the question to the list of potential answers before running the RedisAI model:

\n
    token_key = f"tokenized:bert:qa:{sentence_key}"\n    # encode question\n    input_ids_question = tokenizer.encode(question, add_special_tokens=True, truncation=True, return_tensors="np")\n    t=redisAI.getTensorFromKey(token_key)\n    input_ids_context=to_np(t,np.int64)\n    # merge (append) with potential answer, context - is pre-tokenized paragraph\n    input_ids = np.append(input_ids_question,input_ids_context)\n    attention_mask = np.array([[1]*len(input_ids)])\n    input_idss=np.array([input_ids])\n    num_seg_a=input_ids_question.shape[1]\n    num_seg_b=input_ids_context.shape[0]\n    token_type_ids = np.array([0]*num_seg_a + [1]*num_seg_b)\n    # create actual model runner for RedisAI\n    modelRunner = redisAI.createModelRunner(f'bert-qa{hash_tag}')\n    # make sure all types are correct\n    input_idss_ts=redisAI.createTensorFromBlob('INT64', input_idss.shape, input_idss.tobytes())\n    attention_mask_ts=redisAI.createTensorFromBlob('INT64', attention_mask.shape, attention_mask.tobytes())\n    token_type_ids_ts=redisAI.createTensorFromBlob('INT64', token_type_ids.shape, token_type_ids.tobytes())\n    redisAI.modelRunnerAddInput(modelRunner, 'input_ids', input_idss_ts)\n    redisAI.modelRunnerAddInput(modelRunner, 'attention_mask', attention_mask_ts)\n    redisAI.modelRunnerAddInput(modelRunner, 'token_type_ids', token_type_ids_ts)\n    redisAI.modelRunnerAddOutput(modelRunner, 'answer_start_scores')\n    redisAI.modelRunnerAddOutput(modelRunner, 'answer_end_scores')\n    # run RedisAI model runner\n    res = await redisAI.modelRunnerRunAsync(modelRunner)\n    answer_start_scores=to_np(res[0],np.float32)\n    answer_end_scores = to_np(res[1],np.float32)\n    answer_start = np.argmax(answer_start_scores)\n    answer_end = np.argmax(answer_end_scores) + 1\n    answer = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens(input_ids[answer_start:answer_end],skip_special_tokens = True))\n    log("Answer "+str(answer))\n    return answer\n
\n

Checkout the full code, available on GitHub.

\n

The process for making a BERT QA API call looks like this:

\n

\"Architecture

\n

Here I use two cool features of RedisGears: capturing events on key miss and using async/await to run RedisAI on each shard without locking the primary thread - so that Redis Cluster can continue to serve other customers. For benchmarks, caching responses from RedisAI is disabled. If you are getting response times in nanoseconds on the second call rather then milliseconds, check to make sure the line linked above is commented out.

\n

Running the Benchmark

\n

Pre-requisites for running the benchmark:

\n

Assuming you are running Debian or Ubuntu and have Docker and docker-compose installed (or can create a virtual environment via conda), run the following commands:

\n
git clone --recurse-submodules https://github.com/applied-knowledge-systems/the-pattern.git\ncd the-pattern\n./bootstrap_benchmark.sh
\n

The above commands should end with a curl call to the qasearch API, since Redis caching is disabled for the benchmark.

\n

Next, invoke curl like this:

\n
time curl -i -H "Content-Type: application/json" -X POST -d '{"search":"Who performs viral transmission among adults"}' http://localhost:8080/qasearch
\n

Expect the following output, or something similar based on your runtime environment:

\n
HTTP/1.1 200 OK\nServer: nginx/1.18.0 (Ubuntu)\nDate: Sun, 29 May 2022 12:05:39 GMT\nContent-Type: application/json\nContent-Length: 2120\nConnection: keep-alive\n\n{"links":[{"created_at":"2002","rank":13,"source":"C0001486","target":"C0152083"}],"results":[{"answer":"adenovirus","sentence":"The medium of 40 T150 flasks of adenovirus transducer dec CAR CHO cells yielded 0 5 1 my of purified msCEACAM1a 1 4 protein","sentencekey":"sentence:PMC125375.xml:{mG}:202","title":"Crystal structure of murine sCEACAM1a[1,4]: a coronavirus receptor in the CEA family"}] OUTPUT_REDUCTED}
\n

I modified the output of API for the benchmark to return results from all shards - even if the answer is empty, in the run above five shards return answers, overall API call response under second with all additional hops to search in RedisGraph.

\n

I modified the output of the API for the benchmark to return results from all shards - even if the answer is empty. In the run above five shards return answers. The overall API call response takes less than one second with all additional hops to search in RedisGraph!

\n

\"Architecture

\n

Deep Dive into the Benchmark

\n

Let's dig deeper into what's happening under the hood:

\n

You should have a sentence key with shard id, which you get by looking at the \"Cache key\" from docker logs -f rgcluster. In my setup the cache key is, \"bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults\". If you think it looks like a function call it's because it is a function call. It is triggered if the key isn't present in the Redis Cluster, which for the benchmark will be every time since if you remember we disabled caching the output.

\n

One more thing to figure out from the logs is the port of the shard corresponding to the hashtag, also known as the shard id. It is the text found in betweeen the curly brackets – looks like {6fd} above. The same will be in the output for the export_load script. In my case the cache key was found in \"30012.log\", so my port is 30012.

\n

Next I run the following command:

\n
redis-cli -c -p 300012 -h 127.0.0.1 get "bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults"
\n

and then run the benchmark:

\n
redis-benchmark -p 30012 -h 127.0.0.1 -n 10 get "bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults"\n====== get bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults ======\n  10 requests completed in 0.04 seconds\n  50 parallel clients\n  3 bytes payload\n  keep alive: 1\n\n10.00% <= 41 milliseconds\n100.00% <= 41 milliseconds\n238.10 requests per second
\n

If you are wondering, -n = number of times. In this case we run the benchmark 10 times. You can also add:

\n

– csv if you want to output in CSV format

\n

– precision 3 if you want more decimals in the ms

\n

More information about the benchmarking tool can be found on the redis.io Benchmarks page.

\n

if you don't have redis-utils installed locally, you can use Docker as follows:

\n
docker exec -it rgcluster /bin/bash\nredis-benchmark -p 30012 -h 127.0.0.1 -n 10 get "bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults"\n====== get bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults ======\n  10 requests completed in 1.75 seconds\n  50 parallel clients\n  99 bytes payload\n  keep alive: 1\n  host configuration "save":\n  host configuration "appendonly": no\n  multi-thread: no\n\nLatency by percentile distribution:\n0.000% <= 243.711 milliseconds (cumulative count 1)\n50.000% <= 987.135 milliseconds (cumulative count 5)\n75.000% <= 1577.983 milliseconds (cumulative count 8)\n87.500% <= 1662.975 milliseconds (cumulative count 9)\n93.750% <= 1744.895 milliseconds (cumulative count 10)\n100.000% <= 1744.895 milliseconds (cumulative count 10)\n\nCumulative distribution of latencies:\n0.000% <= 0.103 milliseconds (cumulative count 0)\n10.000% <= 244.223 milliseconds (cumulative count 1)\n20.000% <= 409.343 milliseconds (cumulative count 2)\n30.000% <= 575.487 milliseconds (cumulative count 3)\n40.000% <= 821.247 milliseconds (cumulative count 4)\n50.000% <= 987.135 milliseconds (cumulative count 5)\n60.000% <= 1157.119 milliseconds (cumulative count 6)\n70.000% <= 1497.087 milliseconds (cumulative count 7)\n80.000% <= 1577.983 milliseconds (cumulative count 8)\n90.000% <= 1662.975 milliseconds (cumulative count 9)\n100.000% <= 1744.895 milliseconds (cumulative count 10)\n\nSummary:\n  throughput summary: 5.73 requests per second\n  latency summary (msec):\n          avg       min       p50       p95       p99       max\n     1067.296   243.584   987.135  1744.895  1744.895  1744.895
\n

The platform only has 20 articles and 8 Redis nodes (4 masters + 4 slaves), so relevance would be wrong and it doesn't need a lot of memory.

\n

AI.INFO

\n

Now let's check how long our RedisAI model runs on the {6fd} shard:

\n
127.0.0.1:30012> AI.INFO bert-qa{6fd}\n 1) "key"\n 2) "bert-qa{6fd}"\n 3) "type"\n 4) "MODEL"\n 5) "backend"\n 6) "TORCH"\n 7) "device"\n 8) "CPU"\n 9) "tag"\n10) ""\n11) "duration"\n12) (integer) 8928136\n13) "samples"\n14) (integer) 58\n15) "calls"\n16) (integer) 58\n17) "errors"\n18) (integer) 0\n
\n

bert-qa{6fd} is the key of the actual (very large) model saved. The AI.INFO command gives us a cumulative duration of 8928136 microseconds and 58 calls, which is approximately 153 milliseconds per call.

\n

Let's double-check to make sure that's right by resetting the stats and then re-runnning the benchmark.

\n

First, reset the stats:

\n
127.0.0.1:30012> AI.INFO bert-qa{6fd} RESETSTAT\nOK\n127.0.0.1:30012> AI.INFO bert-qa{6fd}\n 1) "key"\n 2) "bert-qa{6fd}"\n 3) "type"\n 4) "MODEL"\n 5) "backend"\n 6) "TORCH"\n 7) "device"\n 8) "CPU"\n 9) "tag"\n10) ""\n11) "duration"\n12) (integer) 0\n13) "samples"\n14) (integer) 0\n15) "calls"\n16) (integer) 0\n17) "errors"\n18) (integer) 0
\n

Then, re-run the benchmark:

\n
redis-benchmark -p 30012 -h 127.0.0.1 -n 10 get "bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults"\n====== get bertqa{6fd}_PMC169038.xml:{6fd}:33_Who performs viral transmission among adults ======\n  10 requests completed in 1.78 seconds\n  50 parallel clients\n  99 bytes payload\n  keep alive: 1\n  host configuration "save":\n  host configuration "appendonly": no\n  multi-thread: no\n\nLatency by percentile distribution:\n0.000% <= 188.927 milliseconds (cumulative count 1)\n50.000% <= 995.839 milliseconds (cumulative count 5)\n75.000% <= 1606.655 milliseconds (cumulative count 8)\n87.500% <= 1692.671 milliseconds (cumulative count 9)\n93.750% <= 1779.711 milliseconds (cumulative count 10)\n100.000% <= 1779.711 milliseconds (cumulative count 10)\n\nCumulative distribution of latencies:\n0.000% <= 0.103 milliseconds (cumulative count 0)\n10.000% <= 189.183 milliseconds (cumulative count 1)\n20.000% <= 392.191 milliseconds (cumulative count 2)\n30.000% <= 540.159 milliseconds (cumulative count 3)\n40.000% <= 896.511 milliseconds (cumulative count 4)\n50.000% <= 996.351 milliseconds (cumulative count 5)\n60.000% <= 1260.543 milliseconds (cumulative count 6)\n70.000% <= 1456.127 milliseconds (cumulative count 7)\n80.000% <= 1606.655 milliseconds (cumulative count 8)\n90.000% <= 1692.671 milliseconds (cumulative count 9)\n100.000% <= 1779.711 milliseconds (cumulative count 10)\n\nSummary:\n  throughput summary: 5.62 requests per second\n  latency summary (msec):\n          avg       min       p50       p95       p99       max\n     1080.454   188.800   995.839  1779.711  1779.711  1779.711
\n

Now check the stats again:

\n
AI.INFO bert-qa{6fd}\n 1) "key"\n 2) "bert-qa{6fd}"\n 3) "type"\n 4) "MODEL"\n 5) "backend"\n 6) "TORCH"\n 7) "device"\n 8) "CPU"\n 9) "tag"\n10) ""\n11) "duration"\n12) (integer) 1767749\n13) "samples"\n14) (integer) 20\n15) "calls"\n16) (integer) 20\n17) "errors"\n18) (integer) 0
\n

Now we get 88387.45 microseconds per call ~0.088387 seconds, which is pretty fast! Also, considering we started with 10 seconds per call, I think the benefits of using RedisAI in combination with RedisGears are pretty obvious. However, the trade-off is high memory usage.

\n

There are many ways to optimize this deployment. For example, you can add a FP16 quantization and ONNX runtime. If you would like to try that, this script will be a good starting point.

\n

Using Grafana to monitor RedisGears throughput, CPU, and Memory usage

\n

Thanks to the contribution of Mikhail Volkov, we can now observe RedisGears and RedisGraph throughput and memory consumption using Grafana. When you cloned repository it started Graphana Docker, which has pre-build templates to monitor RedisCluster, including RedisGears and RedisAI, and Graph - which is Redis with RedisGraph. \"The Pattern\" dashboard provides an overview, with all the key benchmark metrics you care about:

\n

\"Grafana

\n

\"Grafana

\n

This post is in collaboration with Redis.

\n", "https://reference-architecture.ai/properties/date": "2022-06-16", "https://atomicdata.dev/properties/tags": {"categories":["inference","performance","monitoring"],"tags":["redis","redis-cluster","redisai","redisgears","benchmark","performance","benchmarks"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/nlp/", "https://atomicdata.dev/properties/name": "Building a Pipeline for Natural Language Processing using RedisGears", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/nlp/", "https://atomicdata.dev/properties/description": "

Goal

\n

Disclaimer originally published in collaboration with Ajeet Raina on Developer.Redis.Com

\n

In this tutorial, you will learn how to build a pipeline for Natural Language Processing(NLP) using RedisGears. For this demonstration, we will be leveraging the Kaggle CORD19 datasets. The implementation is designed to avoid running out of memory, leveraging Redis Cluster and RedisGears, where the use of RedisGears allows for processing data on storage without the need to move data in and out of the Redis Cluster—using Redis Cluster as data fabric. Redis Cluster allows for horizontal scalability up to 1000 nodes, and together with RedisGears, provides a distributed system where data science/ML engineers can focus on processing steps, without the worry of writing tons of scaffoldings for distributed calculations.

\n

\"nlp\"

\n

This project was built with the aim to make it easier for other people to contribute and build better information and knowledge management products.

\n

Why data scientists uses RedisGears?

\n

RedisGears have enormous potential, particularly for text processing—you can process your data “on data” without needing to move them in and out of memory. Summary of the important points:

\n\n

What is a knowledge graph?

\n

Today, we live in the world of new systems that operate not just files, folders, or web pages, but entities with their properties and relationships between them, organized into hierarchies of classes and categories. These systems are used everywhere from the military-industrial complex to our everyday lives. Palantir, Primer, and other data companies enable massive intelligence and counterintelligence projects in military and security forces, Quid and RecordedFuture enable competitive analytics, Bottlenose and similar enterprises enable online reputation analytics. Microsoft Graph enables new kinds of productivity apps for the enterprises, Google Knowledge Graph and Microsoft’s Satori enable everyday search queries, and together with Amazon Information Graph they power corresponding AI assistants by enabling them to answer questions about the world facts

\n

All these (and many other more specialized) systems are used in different domains, but all of them use Knowledge Graphs as their foundation.

\n

Knowledge graphs are one of the best ways to connect and make sense out of information from different data sources, following the motto of one of the vendors— “It’s about things not strings”.

\n

Knowledge Graph consists of thesaurus, taxonomy and ontology. In this pipeline I assume knowledge is captured in medical metathesaurus UMLS and concepts in text are related if they are part of the same sentence, therefore concept become node, their relationship becomes edge:

\n

\"concepts1\"\n\"concepts2\"

\n

Concepts have CUI (Concept Unique Identifiers) and those will be primary keys in nodes, linked to UMLS thesaurus. For example, if you search, “How does temperature and humidity affect the transmission of 2019-nCoV?” on the demo website http://thepattern.digital/ and move slider to 1996, there is an edge-connecting transmission (C5190195) and birth (C5195639) and the part of sentence matched, “the rate of transmission to an infant born to,” from the report titled, “Afebrile Pneumonia in infants.”

\n

\"concepts3\"

\n

RedisGears for NLP pre-processing

\n

Overall Architecture Overview (Components Diagram)

\n

\"component_diagram\"

\n

Intake step - is very simple put all JSON records into RedisCluster, then NLP pipeline starts processing all records, code is here.

\n

How does the NLP pipeline steps fit into RedisGears?

\n
    \n
  1. \n

    For each record — detect language (discard non English), it’s filter

    \n
  2. \n
  3. \n

    Map paragraphs into a sentence — flatmap

    \n
  4. \n
  5. \n

    Sentences spellchecker — it’s map

    \n
  6. \n
  7. \n

    Save sentences into hash — processor

    \n
  8. \n
\n

Step 1. Pre-requisite

\n

Ensure that you install virtualenv in your system

\n

Step 2. Clone the repository

\n
 git clone --recurse-submodules https://github.com/applied-knowledge-systems/the-pattern.git\n cd the-pattern

Step 3. Bring up the application

\n
 docker-compose -f docker-compose.dev.yml up --build -d

Step 4. Apply cluster configuration settings

\n

You can deploy PyTorch and spacy to run on RedisGears.

\n
 bash post_start_dev.sh
\n

For Data science-focused deployment, RedisCluster should be in HA mode with at least one slave for each master.\nOne need to change a few default parameters for rgcluster to accommodate the size of PyTorch and spacy libraries (each over 1GB zipped), gist with settings.

\n

Step 5. Create or activate Python virtual environment

\n
 cd ./the-pattern-platform/

Step 6. Create new environment

\n

You can create it via

\n
 conda create -n pattern_env python=3.8
\n

or

\n

Alternatively, you can activate by using the below CLI:

\n
 source ~/venv_cord19/bin/activate #or create new venv\n pip install -r requirements.txt

Step 7. Run pipeline

\n
 bash cluster_pipeline.sh

Step 8. Validating the functionality of the NLP pipeline

\n

Wait for a bit and then check:

\n

Verifying Redis Graph populated:

\n
 redis-cli -p 9001 -h 127.0.0.1 GRAPH.QUERY cord19medical "MATCH (n:entity) RETURN count(n) as entity_count" \n redis-cli -p 9001 -h 127.0.0.1 GRAPH.QUERY cord19medical "MATCH (e:entity)-[r]->(t:entity) RETURN count(r) as edge_count"

Checking API responds:

\n
 curl -i -H "Content-Type: application/json" -X POST -d '{"search":"How does temperature and humidity affect the transmission of 2019-nCoV"}'      \n http://localhost:8080/gsearch

Walkthrough

\n

While RedisGears allows to deploy and run Machine Learning libraries like spacy and BERT transformers, the solution above uses simpler approach:

\n
 gb = GB('KeysReader')\n gb.filter(filter_language)\n gb.flatmap(parse_paragraphs)\n gb.map(spellcheck_sentences)\n gb.foreach(save_sentences)\n gb.count()\n gb.register('paragraphs:*',keyTypes=['string','hash'], mode="async_local")
\n

This is the overall pipeline: those 7 lines allow you to run logic in a distributed cluster or on a single machine using all available CPUs - no changes required until you need to scale over more 1000 nodes. I use KeysReader registered for namespace paragraphs for all strings or hashes. My pipeline would need to run in async mode. For data scientists, I would recommend using gb.run to make sure gears function work and it will run in batch mode and then change it to register - to capture new data. By default, functions will return output, hence the need for count() - to prevent fetching the whole dataset back to the command issuing machine (90 GB for Cord19).

\n

Overall pre-processing is a straightforward - full code is here.

\n

Things to keep in mind:

\n
    \n
  1. Node process can only save locally - we don't move data, anything you want to save should have hashtag, for example to add to the set of processed_docs:
  2. \n
\n
 execute('SADD','processed_docs_{%s}' % hashtag(),article_id)
\n
    \n
  1. Loading external libraries into the computational threat, for example, symspell requires additional dictionaries and needs two steps to load:
  2. \n
\n
 """\n load symspell and relevant dictionaries\n """\n sym_spell=None \n\n def load_symspell():\n  import pkg_resources\n  from symspellpy import SymSpell, Verbosity\n  sym_spell = SymSpell(max_dictionary_edit_distance=1, prefix_length=7)\n  dictionary_path = pkg_resources.resource_filename(\n      "symspellpy", "frequency_dictionary_en_82_765.txt")\n  bigram_path = pkg_resources.resource_filename(\n      "symspellpy", "frequency_bigramdictionary_en_243_342.txt")\n  # term_index is the column of the term and count_index is the\n  # column of the term frequency\n  sym_spell.load_dictionary(dictionary_path, term_index=0, count_index=1)\n  sym_spell.load_bigram_dictionary(bigram_path, term_index=0, count_index=2)\n  return sym_spell
\n
    \n
  1. Scispacy is a great library and data science tool, but after a few iterations with deploying it I ended up reading data model documentation for UMLS Methathesaurus and decided to build Aho-Corasick automata directly from UMLS data. (MRXW_ENG.RRF contains all terms form for English mapped to CUI). Aho-Corasick allowed me to match incoming sentences into pairs of nodes (concepts from the medical dictionary) and present sentences as edges in a graph, Gears related code is simple:
  2. \n
\n
 bg = GearsBuilder('KeysReader')\n bg.foreach(process_item)\n bg.count()\n bg.register('sentence:*',  mode="async_local",onRegistered=OnRegisteredAutomata)\n
\n

OnRegisteredAutomata will perform similarly to symspell example above except it will download pre-build Aho-Corasick automata (30Mb).\nAho-Corasick is a very fast matcher and allows to perform >900 Mb text per second even on commodity laptop, RedisGears cluster makes a very smooth distribution of data and ML model and matching using available CPU and Memory. Full matcher code.

\n

Output of the matcher: nodes and edges are candidates to use another RedisGears pattern rgsync where you can write fast into Redis and RedisGears are going to replicate data into slower storage using RedisStreams.\nBut I decided to use streams and handcraft the population of the RedisGraph database, which will be focus of the next blog post.

\n

Output of the matcher: nodes and edges are candidates to use another RedisGears pattern rgsync where you can write fast into Redis and RedisGears are going to replicate data into slower storage using RedisStreams, while this demo uses streams and populates RedisGraph database with nodes and edges calculating rank of each.

\n

Call to action

\n

We took OCR scans in JSON format and turned them into Knowledge Graph, demonstrating how you can traditional Semantic Network/OWL/Methathesaurus technique based on Unified Medical Language System. Redis Ecosystem offers a lot to the data science community, and can take place at the core of Kaggle notebooks, ML frameworks and make deployment and distribution of data more enjoyable. The success of our industry depends on how our tools work together — regardless of whether they are engineering, data science, machine learning and organisational or architectural.

\n

With the collaboration of RedisLabs and community, the full pipeline code is available via https://github.com/applied-knowledge-systems/the-pattern-platform. In case, you want to try it locally, then you can find a Docker Launch script in the root of the repository along with short quickstart guide. PR and suggestions are welcome. The overall goal of the project is to allow other to build their more interesting pipeline on top of it.

\n

References

\n\n", "https://reference-architecture.ai/properties/date": "2022-05-28", "https://atomicdata.dev/properties/tags": {"categories":["redisgears","knowledge-graphs","performance"],"tags":["redis","redis-cluster","redisgears","nlp","redisgraph"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/metadata/", "https://atomicdata.dev/properties/name": "Metadata Management", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/metadata/", "https://atomicdata.dev/properties/description": "

In CORD 19 dataset mentioned in Data Acquisition Metadata stored in the separate csv file from the source data. Here simple script to parse date/times and attach it to JSON/XML files

\n", "https://reference-architecture.ai/properties/date": "2022-05-14", "https://atomicdata.dev/properties/tags": {"categories":["metadata"],"tags":["data","metadata"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/contribution/", "https://atomicdata.dev/properties/name": "Contribution Guidelines", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/contribution/", "https://atomicdata.dev/properties/description": "

General guidelines for contributing to the project.

\n

Main goals

\n

Be data-driven

\n\n

Engineering approach

\n

There should be a a path to be implemented in a real world - good prototype or production deployment.

\n

How to contribute

\n

Welcome pull requests on \n\n\n\n. Check out supported shortcuts Extended Shortcuts

\n

Licenses

\n

When contributing you agreeing to share your contribution under

\n", "https://reference-architecture.ai/properties/date": "2021-12-15", "https://atomicdata.dev/properties/tags": {"categories":["Documentation"],"tags":["contribute","zola"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/intake/", "https://atomicdata.dev/properties/name": "Data Acquisition", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/intake/", "https://atomicdata.dev/properties/description": "

For the Reference Architecture for AI, we used Kaggle Cord19 dataset, \"COVID-19 Open Research Dataset (CORD-19). CORD-19 is a resource of over 1,000,000 scholarly articles, including over 400,000 with full text, about COVID-19, SARS-CoV-2, and related coronaviruses. This freely available dataset is provided to the global research community to apply recent advances in natural language processing and other AI techniques to generate new insights in support of the ongoing fight against this infectious disease.\"

\n

Ingest documents

\n

Example script parses documents taking out body_text and saves under paragraphs in Redis cluster.

\n", "https://reference-architecture.ai/properties/date": "2020-08-31", "https://atomicdata.dev/properties/tags": {"categories":["intake","data acquisition"],"tags":["data","acquisition","intake"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] },{"localId": "https://reference-architecture.ai/docs/ai-product/", "https://atomicdata.dev/properties/name": "The Pattern: Machine Learning Natural Language Processing meets VR/AR", "https://reference-architecture.ai/properties/url": "https://reference-architecture.ai/docs/ai-product/", "https://atomicdata.dev/properties/description": "

To fight ever-increasing complexity, \"The Pattern\" projects help find relevant knowledge using Artificial Intelligence and novel UX elements, all powered by Redis - a new generation real-time data fabric turned into knowledge fabric

\n

Overall repository for CORD19 medical NLP pipeline, API and UI, design and architecture.

\n

Demo Video:

\n
\n \n
\n

Demo Server (no persistance): https://thepattern.digital/

\n

The challenge

\n

The medical profession put a lot of effort into collaboration, starting from Latin as a common language to industry-wide thesauruses like UMLS. However, if full of scandals where publications in a prestigious journal would be retracted, and the World Health Organisation would change its policy advice based on the article. I think \"paper claiming that eating a bat-like Pokémon sparked the spread of COVID-19\" takes a prize. One would say that editors in those journals don't do their job, and while it may seem true, I would say they had no chance: with a number of publications about COVID (SARS-V) passing 300+ per day, we need better tools to navigate via such flow of information.\nWhen exploring science or engineering topics, I look at the diversity of the opinion, not the variety of the same cluster of words or the same thought. I want to avoid confirmation bias. I want to find articles relevant to the same concept, not necessarily the ones which have similar words. My focus is to build a natural language processing pipeline capable of handling a large number of documents and concepts, incorporating System 1 AI (fast, intuitive reasoning) and System 2 (high-level reasoning) and then present knowledge in a modern VR/AR visualisation. Search or rather information exploration should be spatial, preferably in VR (memory palace, see Theatre of Giulio Camillo). A force-directed graph is a path towards it, where visuals are assisted by text — relevant text pops up on the connection and where people explore the concepts and then dig deeper into the text. The purpose of the pipeline is that knowledge should be reusable and shareable.

\n

Community

\n

Join our community on Discord or post on GitHub Discussions](https://github.com/applied-knowledge-systems/the-pattern/discussions)

\n", "https://reference-architecture.ai/properties/date": "2020-08-31", "https://atomicdata.dev/properties/tags": {"categories":["ai product","nlp","medical"],"tags":["ai product","ai","medical"]}, "https://atomicdata.dev/properties/isA": ["https://atomicdata.dev/classes/Article"] }]