Before you start
- Python functions, dictionaries, type annotations and terminal use
- Tool calling
- Agent frameworks
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.
Concepts covered
Skills
- Dependency pinning
- Stateful graphs
- Retrieval evaluation
- Bounded delegation
- Approval testing
Prove it to yourself
Run one cumulative lab from a synthetic corpus through retrieval, validation and approval to a deduplicated local outbox.
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.
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.
- Input boundary
initial() redacts the question before graph invocation. No evidence or action exists yet.
- Model requests search
The deterministic policy emits an AIMessage containing a search tool call. The graph, not the message, authorises execution.
- Tool returns evidence
The tools node validates the request, retrieves the annual-leave passage and appends a ToolMessage with the matching call ID.
- 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.
- Validate support
The answer must match the extractive contract, including citations. A merely parseable answer is insufficient.
- Propose action
The proposed local outbox action gets a hash of request ID, destination and body. Nothing has been written to the outbox.
- 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.
- 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.
- 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.
Prepare the synthetic corpus
Passage ID and redacted text
chunks() flattens fixture leaves. The source fixture itself still contains the synthetic email.
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.
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.
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.
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:
- A lexical question and an exact path retrieve the intended passage; missing paths return no evidence.
- Removing evidence changes retrieval recall without turning an honest abstention into a support failure.
- An unsupported answer with a real citation ID fails validation before any action is proposed.
- Parent history cannot be passed through the worker task contract, and excessive tasks are rejected.
- A synthetic email canary is absent from saved input state, while the limits of the detector remain documented.
- Both approval and rejection survive reconstructing the process with the same checkpoint directory and thread.
- 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| Release | Latest | Released | End of life | Status |
|---|---|---|---|---|
3.14 | 3.14.8 | Oct 2025 | Oct 2030 | Supported |
3.13 | 3.13.16 | Oct 2024 | Oct 2029 | Supported |
3.12 | 3.12.14 | Oct 2023 | Oct 2028 | Supported |
3.11 | 3.11.16 | Oct 2022 | Oct 2027 | Supported |
3.10 | 3.10.21 | Oct 2021 | Oct 2026 | Supported |
3.9 | 3.9.25 | Oct 2020 | Oct 2025 | End of life |
3.8 | 3.8.20 | Oct 2019 | Oct 2024 | End of life |
Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.