IntermediateReferenceLearning Paths

Building Agents with LangChain and LangGraph

A runnable document-assistant course covering model interfaces, reducers, retrieval evaluation, bounded delegation and persistent approval, with an offline lab and an interactive state diagram.

Applies to
Python3.14.7Exact interpreter request in the lab; project accepts the 3.14 seriesuv0.12.10Local verification tool version, not installed by the project locklangchain-core / langgraph1.6.2 / 1.2.11Exact direct dependencies in the lab lockfilelanggraph-checkpoint-sqlite / pydantic3.1.1 / 2.13.5Local checkpoint storage and runtime contractslangchain-openai1.6.1Optional locked adapter; live compatibility exercise not run
establishedUpdated Reviewed
intermediateAssumes working familiarity; skips the fundamentals.

Before you start

What you will be able to do

  • Recreate the locked environment without confusing installation with interpreter pinning.
  • Exercise invoke, batch and stream locally, without presenting a fake model as an LLM.
  • Predict message replacement, event accumulation and scalar replacement in the graph.
  • Diagnose a retrieval miss separately from an answer that ignores available evidence.
  • Compare exact tree navigation with lexical ranking and explain why neither is PageIndex.
  • Restrict a delegated worker to a bounded lookup contract.
  • Resume a persisted approval in a new process and prove rejection causes no outbox write.
  • State which provider, privacy and delivery guarantees the lab does not establish.

Skills

  • Dependency pinning
  • Stateful graphs
  • Retrieval evaluation
  • Bounded delegation
  • Approval testing

Prove it to yourself

A document assistant with observable boundaries

Run one cumulative lab from a synthetic corpus through retrieval, validation and approval to a deduplicated local outbox.

A failure-first capstone

Extend the fixtures and prove that missing evidence, malformed approvals and repeated delivery attempts remain distinguishable.

The important boundary is not between a prompt and an answer. It is between an answer the program can justify and an action the program is permitted to carry out. Build that boundary first, using predictable inputs. A model can then be introduced without becoming the only explanation for what the system did.

This course follows the document-assistant lab, which is public and self-contained. It uses real LangChain messages, a real LangGraph execution loop and SQLite checkpoints, but its default decision policy is ordinary Python, not a large language model (LLM). The corpus contains invented handbook and runbook passages. None of its policies is operational advice.

The lab is already assembled. Work through its boundaries in order, run each exercise, predict a failure, then check the result. Later modules retain the same state, evidence and approval contracts rather than introducing unrelated sample applications.

What you will run

Layer Implemented here Not established by this lab
Model interface Fake responses for execution modes; deterministic policy for the graph Model reasoning quality, provider equivalence or inference latency
Retrieval Token-set overlap and exact fixture-tree traversal Embeddings, a vector database, document parsing or PageIndex
Evaluation Reference and evidence checks over synthetic fixtures General semantic accuracy or an independent benchmark
Delegation An isolated lookup subgraph with code-enforced bounds A planning model, parallel agents or Deep Agents integration
Approval Persisted pause, validated resume payload and local outbox Reviewer authentication, encrypted storage or external exactly-once delivery
Gateway Offline fallback-policy tests and a separate configuration example A running or version-pinned LiteLLM service

Verification scope. Local verification on Windows with Python 3.14.7 and uv 0.12.10 ran the 12 existing unit tests and the six exercise groups below. The fake-model exercise and all five baseline retrieval cases passed. Provider calls and gateway deployment were not run. These are test outcomes for this lab, not product performance claims. The retained human-review date does not certify this rewrite; editorial review is still required.

Module 1 - Reproduce the environment

Goal: another checkout selects the same interpreter patch and dependency versions, without manual activation or unrecorded package additions.

Clone the lab repository. Install uv using Astral’s platform instructions, then open a terminal at the checkout. The following commands work in PowerShell and POSIX shells:

git clone https://github.com/rameshsraj/metaintelligence-labs.git
cd metaintelligence-labs
uv --version
uv python install
uv sync --locked
uv run --locked python -m unittest -v

Installation may download Python and packages. The exercises themselves need no provider account, API key, model download or network access. After installation, add --offline to uv run to disable uv network access; that flag does not sandbox application networking. For local-only work, also disable tracing in any shell previously used with LangSmith. In PowerShell:

$env:LANGSMITH_TRACING = 'false'
$env:LANGCHAIN_TRACING_V2 = 'false'

All remaining commands assume the example directory as the working directory. Do not use Python’s -O option: it removes the assertion checks used by the exercises.

Three records, three different jobs

Record Checked-in value or role What it does not do
.python-version 3.14.7 Does not record library versions or install uv
pyproject.toml Python >=3.14,<3.15; explicit direct package pins Does not pin a Python patch merely by declaring a compatible range
uv.lock Resolved direct and transitive packages, including optional provider dependencies Does not guarantee a provider endpoint or model deployment still behaves identically

uv python install reads the existing interpreter request. Installing Python and writing that request are separate operations. In a new project, uv python pin 3.14.7 writes the pin; uv python install 3.14 alone does not pin an exact patch for collaborators. This checkout already has the pin, so do not recreate the project with uv init.

uv sync --locked rejects a stale lock rather than silently resolving a new environment. Do not replace it with --frozen to hide drift: frozen execution skips the freshness check. A deliberate dependency change requires regenerating the lock and rerunning the exercises. Sync without the provider extra installs the offline dependency set and can remove previously installed optional packages from this project environment.

Dependency Lab version Role
langchain-core 1.6.2 Messages and fake chat model interfaces
langgraph 1.2.11 Graph, reducers and interrupts
langgraph-checkpoint-sqlite 3.1.1 Persistent local checkpoints
pydantic 2.13.5 Validated answer, tool and decision contracts
langchain-openai 1.6.1, optional Separate live compatibility exercise

These versions come from the lab lock, not a claim that they are the latest releases. The site tracks Python support in the matrix below; it does not track support windows for these libraries. No support dates are inferred for them.

Check: the suite reports Ran 12 tests and OK. If imports fail, confirm that the command is running inside this uv project rather than the editor’s unrelated system interpreter. If --locked fails, compare project declarations with the lock before changing either.

Module 2 - Learn the message contract without a provider

Build on Module 1: inspect model_exercise() in the lab implementation, then run:

uv run --locked python lab.py models

The observed success line is invoke/batch/stream/schema: PASS (fake model). The FakeListChatModel returns a supplied answer JSON string. It demonstrates how a caller receives and parses messages, not how a model discovers an answer.

Operation Contract exercised Important limitation
invoke Receive a complete AIMessage, then validate its content as Answer Valid JSON alone says nothing about support for the answer
batch Submit independent inputs; this exercise sets max_concurrency: 1 This is the client-side interface, not a provider’s discounted asynchronous batch service
stream Collect content chunks and validate the assembled JSON Do not treat each partial JSON chunk as a complete answer or permission to act

In the graph, HumanMessage carries the sanitised question, AIMessage.tool_calls carries a requested operation, and ToolMessage returns evidence using the same tool_call_id. The tool request is not executable code and is not approval. The tools node checks its name, arguments, identifier and call count before retrieving anything. Read tool calling for the wider protocol.

The graph’s offline_model() is a different test double: it asks for search, then copies the returned passage into answer JSON. It does not call FakeListChatModel, create_agent or a provider. Answer forbids extra fields, limits text to 500 characters and citations to two items. The graph applies a stricter support check later: the answer must equal the result of extract(). That makes a paraphrase invalid even if a human would judge it correct. This deliberate constraint keeps support mechanically testable at the cost of expressiveness.

Exercise: change the fake response in a disposable working copy to malformed JSON, then to well-formed JSON with an unsupported answer. Identify which check rejects each. Restore the response and rerun the tests before continuing. The optional provider exercise near the end uses the same contracts but does not replace this offline graph automatically.

Module 3 - Follow state through the tool loop

Build on Module 2: run the reducer exercise and inspect the State definition.

uv run --locked python course.py reducers

A node returns a partial update. Each state field decides how that update combines with its previous value. This distinction is why returning a list does not necessarily replace a list. The Graph API documentation describes the reducer’s left argument as accumulated state and its right argument as the node’s update.

Field Lab reducer Example consequence
messages add_messages A new message ID appends; an existing ID replaces that message
events operator.add ['model'] followed by ['search'] becomes ['model', 'search']
calls Replacement A node returning 2 replaces 1; it does not produce 3
evidence, answer, action, status Replacement A later value replaces the earlier snapshot for that field

The reducer exercise replaces a question with the same ID and appends an answer with a new ID. Compare that with ordinary list concatenation, which retains both versions of the question. It also proves that returning an empty event list does not clear accumulated events.

Observe the successful path

Use the diagram to predict state before running the approval exercise. Advancing it does not run Python. The numbered flow remains a complete text explanation when scripting is unavailable.

From evidence to permission
A browser simulation of the checked-in offline graph. No model calls, Python execution, persistent storage or real approval occur here. Advance the flow, then choose a decision.
  1. Input boundary

    initial() redacts the question before graph invocation. No evidence or action exists yet.

  2. Model requests search

    The deterministic policy emits an AIMessage containing a search tool call. The graph, not the message, authorises execution.

  3. Tool returns evidence

    The tools node validates the request, retrieves the annual-leave passage and appends a ToolMessage with the matching call ID.

  4. Model returns answer JSON

    The policy copies the evidence into an answer. A second model update replaces calls; it does not add the new total to the old total.

  5. Validate support

    The answer must match the extractive contract, including citations. A merely parseable answer is insufficient.

  6. Propose action

    The proposed local outbox action gets a hash of request ID, destination and body. Nothing has been written to the outbox.

  7. Interrupt for review

    The checkpoint preserves this thread. On resume, the approval node starts again. No approval event is appended until the decision is processed.

  8. Decision branch

    Approve the matching action: status becomes approved. Reject or supply a stale action ID: status becomes cancelled and execution ends without a write.

  9. Publish only after approval

    The publish node rechecks the action, then inserts the local ledger row. Repeating the same insert does not add a duplicate. This is not external delivery.

Synthetic fixture answer
Annual leave requests need manager approval.

Proposed destination: local-review-outbox. The real CLI displays the full action ID for review.

Read the branch without JavaScript: outbox rows remain zero through review. A valid approval permits the publish node to record one row. Rejection, malformed input or a stale action ID records none. The Python exercises test persistence; resetting this diagram simply clears its in-memory teaching state.

The graph can also end outside the successful path. An unknown tool, malformed tool arguments or an unsupported answer results in invalid; repeated tool requests reach budget-exhausted. Inspect test_loop_budget and test_unknown_tool_and_unsupported_answer in the lab suite.

Bound Explicit lab value Trade-off
Tool-request routing Tools run only while calls < 3 after the model update A policy still requesting tools on its third call stops instead of completing more work
Parent graph recursion limit 16 super-steps per invocation A separate guard against graph loops, not a provider-token or wall-clock budget
Worker recursion limit 3 super-steps Enough for the single lookup graph, not general recursive research

Checkpoint: predict the pending state: calls is 2; events are model, search, model, validate, propose. Approval has interrupted before returning its event update. Module 6 checks this sequence against actual execution.

Module 4 - Diagnose retrieval before changing the model

Build on Module 3: keep the same answer schema and replace guesswork with fixture scores.

uv run --locked python lab.py evaluate
uv run --locked python course.py retrieval
uv run --locked python course.py structure

Indexing and retrieval are different operations

The fixture tree is authored in advance. chunks() walks its leaves, assigns slash-separated passage IDs and redacts email-shaped text. There is no PDF parser or learned index. Lexical retrieval lowercases text, extracts alphabetic token sets and ranks passages by intersection size divided by union size, called Jaccard similarity. It returns at most one positive-scoring passage, with ID ordering as the tie-breaker. Token order and repeated words contribute nothing additional.

  1. Prepare the synthetic corpus

    Passage ID and redacted text

    chunks() flattens fixture leaves. The source fixture itself still contains the synthetic email.

  2. Retrieve for the question

    Zero or one evidence passage

    Lexical ranking uses token overlap. Exact tree lookup uses a path instead; it does not rank passages.

  3. Extract and validate

    Answer text and citation IDs

    extract() copies evidence or returns No evidence found. The graph validates the same contract before proposing an action.

  4. Score independently

    Reference, relevance, support and retrieval scores

    course.py varies evidence and answers independently to expose which boundary failed. The evaluator itself does not publish anything.

Offline data flow. The evidence objects connect retrieval to both the answer and its evaluation; a final answer alone cannot reveal whether retrieval worked.

The baseline fixture command scores all five questions, including the deliberately unanswerable orbital bananas. All four values are 1 for each baseline case. That is expected for a tiny corpus whose extractor copies its references; it is not evidence that arbitrary document questions work.

Read each score as a different comparison

The LangSmith evaluation guide separates responses, references and retrieved documents. This lab uses deterministic proxies for those comparisons, not the guide’s model judges or hosted evaluation service.

Lab score Implemented comparison What a passing value can miss
correctness Answer text equals the fixture reference A valid paraphrase fails; citation correctness is checked separately
answer_relevance All fixture terms occur in answer tokens; empty terms expect the no-evidence answer An incorrect answer containing the right words can pass
groundedness Citations exist in evidence and answer equals a cited passage; empty evidence requires the no-evidence answer without citations Does not prove that the source passage itself is true
retrieval_recall Relevant IDs retrieved divided by relevant fixture IDs; no relevant IDs means success only for empty retrieval Does not penalise unrelated extra results as precision would

The retrieval exercise produces this scored comparison:

Controlled case Correctness Answer relevance Groundedness Retrieval recall
Baseline annual-leave lookup 1 1 1 1
Empty corpus for the answerable question 0 0 1 0
Correct evidence, but answer says leave is automatic 0 1 0 1

The middle row is a retrieval miss even though the answer safely abstains. The last row has the right evidence and a real citation ID, but invents a different rule. Retrying retrieval is not the fix for that row. These rows are evaluated directly: the graph would reject the unsupported answer before it could reach approval.

Exact paths are not PageIndex

Request and mode Result in the lab Reason
annual leave, lexical Annual-leave passage Positive token overlap wins the ranking
handbook/leave/emergency, tree Emergency-leave passage Exact descent through document, section and leaf
handbook/leave/missing, auto Empty evidence Auto chooses tree mode on a slash; missing branches do not fall back
handbook/leave, tree Empty evidence A section is not a leaf passage

PageIndex describes generating document trees and using LLM reasoning to search them. The lab does neither: the tree is hand-authored and traversal uses exact IDs. Use the comparison to understand address-based retrieval, not to claim a PageIndex implementation. Likewise, there are no embeddings here. Follow RAG architecture for the broader design.

Exercise: add an invented passage and a corresponding fixture case, then a paraphrased question that shares no useful tokens. Keep the reference correct and observe the retrieval miss. Do not change the reference to match the wrong answer just to restore a passing score. A lexical method buys determinism and inspection; it gives up semantic matching. Exact paths avoid ranking ambiguity but require a known, stable address.

Module 5 - Delegate less information, not more authority

Build on Module 4: send the same lookup through the worker and verify parity.

uv run --locked python course.py delegation

delegate_search() accepts exactly one task with the keys query and mode. The child graph has only query, mode and result in its state. Its only node retrieves evidence; it has no tool for delegating again or writing to the outbox. The parent receives the result and continues its own validation and approval flow.

Boundary Lab rule Cost or limitation
Task count Exactly one No fan-out or parallel research
Query 1 to 300 characters, validated by SearchArgs Character count is not a model-token budget
State passed to child Only query and mode The caller must provide enough context in the query
Returned result At most 1000 characters in json.dumps(result) A post-computation check, not a cap on retrieval memory or elapsed time
Authority Lookup only In-process separation is not an operating-system sandbox

The test rejects an empty task list, multiple tasks and extra keys carrying parent history. A type annotation alone would not enforce those boundaries. The code does. The mode is constrained by the CLI choices, not by a strict worker-mode schema; callers bypassing the CLI must not treat the worker dictionary as a fully validated public API.

This is bounded delegation, not planning. The --delegate switch selects it; no model decomposes the question or chooses a specialist. Deep Agents subagents offer a separate framework for delegated work and context handling. That package is not installed or tested here. For this single lookup, a direct function is simpler; the subgraph exists to make the information boundary visible before introducing more complicated workers.

Sanitise before the first checkpoint

uv run --locked python course.py privacy

Personally identifiable information (PII) can enter saved state before a model node runs. initial() therefore redacts the question before invoking the graph. Fixture text is redacted when passages are retrieved, and tool queries are redacted before lookup. The exercise checks checkpoint state history for a synthetic email canary and confirms that a synthetic name is not redacted.

This regular expression recognises email-shaped strings only. It does not identify every address, name, account number or indirect identifier. It does not erase the source fixture, terminal history, callback logs or checkpoint files. Request IDs are character-restricted, but a person’s name could still satisfy that restriction; thread IDs also need a caller policy. Use synthetic identifiers.

The graph accepts injected model functions for testing. Their returned messages enter state before downstream validation, so the email checks do not prove arbitrary model output or malicious tool arguments cannot enter checkpoints. A real integration needs an output boundary, logging policy, access control, retention policy and threat tests. Private subgraph state is not encryption.

Module 6 - Resume permission, not just execution

Build on all previous modules: run retrieval with delegation, close the process at approval, then resume from the persisted thread.

uv run --locked python course.py approval

The exercise creates temporary databases, invokes the lab CLI in separate processes and removes the temporary directory afterwards. Its checked outcomes are:

reject: pending -> cancelled; outbox rows=0
approve: pending -> recorded; outbox rows=1
replay: duplicate ledger insert ignored; completed and wrong-thread resumes denied
approval: PASS

Try an actual review in the terminal

For an interactive run, use PowerShell. The directory name below is an illustrative local output location. Start with unused thread names; repeating start on an existing thread deliberately fails.

$pending = uv run --locked python lab.py start --directory .lab/review --thread review-approve --request-id review-approve --question 'annual leave' --delegate | ConvertFrom-Json
$pending | ConvertTo-Json -Depth 6

Read the body, destination and full action.id before deciding. The displayed status is pending, not recorded. This is a durable pause, not a process blocked waiting for keyboard input. The start command has finished. To approve that exact action in another invocation:

uv run --locked python lab.py resume --directory .lab/review --thread review-approve --decision approve --action-id $pending.action.id

Try rejection on a distinct request. It must not add a row to the outbox:

$rejected = uv run --locked python lab.py start --directory .lab/review --thread review-reject --request-id review-reject --question 'annual leave' | ConvertFrom-Json
uv run --locked python lab.py resume --directory .lab/review --thread review-reject --decision reject --action-id $rejected.action.id

If you close the shell too, retain the thread, directory and reviewed action ID and supply those same values when resuming. The Python checkpoint persists; PowerShell variables do not. Use course.py approval for automated row-count assertions rather than inferring writes from a message.

What the gate enforces

The interrupt documentation requires a checkpointer and the same thread ID for resume. It also explains the replay hazard: resuming starts the interrupted node again from its beginning. The lab’s approval node has no outbox write before interrupt() and does not catch the interrupt in a broad exception handler.

The CLI’s resume() helper checks that the thread has a pending interrupt, then invokes the graph with a Command whose resume value is a dictionary containing decision and action_id. A new question dictionary would be new input, not the review response. Both invocations must open the same SQLite checkpoint file; reusing a thread label with a fresh database does not recover its state.

Input or event Lab response Check
Valid approve with matching action ID Approval succeeds, then publish rechecks the action Fresh-process approval exercise
Valid reject cancelled, no new row Rejection exercise
Boolean, arbitrary string, missing fields or wrong decision spelling Fail closed Malformed-decision unit test
Approve with stale action ID cancelled, no new row Stale-decision unit test
Wrong thread or completed thread No pending approval error Resume helper and fresh-process exercise
Identical ledger insertion repeated Existing row retained write_once() duplicate check

The action ID hashes the request ID, destination and redacted body. publish() compares the stored action with a recomputed one. The ID binds the decision to those fields; it is not a signature, authentication token or proof that an authorised person reviewed them. The same request ID and body produce the same ledger key even on another thread, while a different request ID produces another key. Choosing stable request IDs is part of deduplication design.

SQLite’s unique primary key and INSERT OR IGNORE make the local row the deduplicated effect. The checkpoint and outbox are separate databases, not one distributed transaction. Extending the lab to send mail after inserting a row creates a new delivery problem: a crash between the insert and send can lose delivery; a crash after send but before acknowledgement can cause a retry to duplicate it. A real sender needs a delivery protocol and downstream idempotency support. This lab does not establish exactly-once external effects.

Even a no-evidence answer can be proposed for local review if it satisfies the extractive contract. Approval is not a quality score. If the intended action is only allowed for supported answers, add an explicit evidence requirement rather than expecting the reviewer or model to infer it.

Optional provider compatibility, not a required course step

The separate provider exercise requires LAB_MODEL, LAB_API_KEY and LAB_BASE_URL and an explicit --allow-paid-network flag. Supply an approved deployment and credentials through your own secret-management process. There is no default model or endpoint. Do not paste credentials into the course or commit them.

Installing the adapter is separate from making paid calls:

uv sync --locked --extra provider

Only after reviewing cost and data handling, an operator can opt into the live exercise:

uv run --locked --extra provider python provider.py --allow-paid-network

Not executed for this course. The script tests invocation, sequential batch, streaming, JSON-schema output, forced search-tool arguments and a matching tool-result message. Its final tool-follow-up assertion checks non-empty content, not answer correctness. It does not connect the live model to build() or validate all graph failure paths against that provider.

Explicit adapter setting Value Consequence
timeout 20 seconds Per-call client setting, not a whole-course deadline
max_retries 0 No adapter retries obscuring a failed compatibility check
max_tokens 300 A requested output cap; compatibility and provider enforcement still need checking
use_responses_api False Exercises the Chat Completions path, not Responses-only features

The ChatOpenAI documentation limits its scope to the official OpenAI API format. A custom base URL does not guarantee preservation of a third-party gateway’s extra fields. Use an appropriate provider-specific integration when those fields matter. A failure is a compatibility finding, not a reason to remove validation.

Gateway routing has its own failure boundary

The lab’s gateway configuration illustrates aliases document-primary and document-fallback, environment-supplied deployment IDs and keys, zero retries, a 20-second request timeout, fallback routing and fallback access checks. It is not part of the uv environment and has no pinned LiteLLM runtime. Treat it as an operator-reviewed starting point, not a verified deployment recipe. Custom endpoints may require additional provider-specific settings.

The offline compatible_fallback() helper does not load that configuration or call LiteLLM. Its test supplies fake targets with declared capability sets, skips incompatible targets and allows at most two compatible attempts by default. Only TimeoutError and ConnectionError permit another attempt; other failures propagate. Real SDK exception classes need explicit mapping. Capability declarations must also be tested against real deployments before they can be trusted.

Operational question Evidence needed beyond this lab
Did a fallback actually serve the request? Gateway routing identity and server logs, not a model claiming its own name
Is the backup permitted to see the same data? Key permissions, tenancy and data-location policy for every target
Does it preserve tools and structured output? Compatibility tests through the gateway, including failure responses
Are retries bounded end to end? Combined adapter, gateway and application attempt counts and deadlines
Can it authorise an outbox action? It cannot replace the application approval gate

Relevant upstream behaviour changes are worth separating from the lab’s explicit choices:

Upstream change Release Course implication
LangGraph’s documented default recursion limit became 1000 super-steps 1.0.6 The lab sets 16 explicitly; an upgrade must not silently widen its budget
LiteLLM proxy requests stopped accepting mock fallback-testing flags 1.85.0 Do not claim a live fallback test using those flags; test a real error on an isolated deployment

These changes are recorded in the cited Graph API and LiteLLM reliability documentation. They do not establish compatibility for an unpinned gateway. This course makes no claim of a complete security-advisory review of the dependency set.

Capstone - Extend evidence without weakening permission

Run the cumulative checks from the same example directory:

uv run --locked python -m unittest -v
uv run --locked python lab.py models
uv run --locked python lab.py evaluate
uv run --locked python course.py capstone

Then add a synthetic document with a nested section and a leaf passage. Add a reference question, an exact-path question and an intentionally unanswerable question. Keep IDs stable and keep real personal data out of fixtures. Run both direct and delegated retrieval against the new passage.

Your extension is complete when its evidence demonstrates all of the following:

  1. A lexical question and an exact path retrieve the intended passage; missing paths return no evidence.
  2. Removing evidence changes retrieval recall without turning an honest abstention into a support failure.
  3. An unsupported answer with a real citation ID fails validation before any action is proposed.
  4. Parent history cannot be passed through the worker task contract, and excessive tasks are rejected.
  5. A synthetic email canary is absent from saved input state, while the limits of the detector remain documented.
  6. Both approval and rejection survive reconstructing the process with the same checkpoint directory and thread.
  7. Rejection and stale approval add no outbox row; repeated insertion of an approved action keeps one row.

Use the tests as executable specifications, not a checklist of function names. For any failed assertion, record the question, retrieved IDs, answer, score comparison, graph status and outbox count before changing code. The capstone is still an offline extractive assistant. Provider reasoning, document ingestion, authenticated review and external delivery are separate integration work, not hidden prerequisites or guarantees earned by these local tests.

Python release support

5 supported
ReleaseLatestReleasedEnd of lifeStatus
3.143.14.8Oct 2025Oct 2030Supported
3.133.13.16Oct 2024Oct 2029Supported
3.123.12.14Oct 2023Oct 2028Supported
3.113.11.16Oct 2022Oct 2027Supported
3.103.10.21Oct 2021Oct 2026Supported
3.93.9.25Oct 2020Oct 2025End of life
3.83.8.20Oct 2019Oct 2024End of life

Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.