# AbstractRuntime — llms-full

This file is a single-document snapshot of the local Markdown files (`*.md`) linked in `llms.txt`, intended for LLM/agent ingestion.
It is generated from `llms.txt` in first-link order, de-duplicated, with repo-root-normalized relative links.

---

## README.md

# AbstractRuntime

**AbstractRuntime** is a durable workflow runtime (interrupt → checkpoint → resume) with an append-only execution ledger.

It is designed for long-running workflows that must survive restarts and explicitly model blocking (human input, timers, external events, subworkflows) without keeping Python stacks alive.

**Version:** 0.4.29 • **Python:** 3.10+

**Status:** pre-1.0 (API may evolve). For production use, pin versions and follow `CHANGELOG.md`.

## AbstractFramework ecosystem

AbstractRuntime is one component of the wider [AbstractFramework](https://github.com/lpalbou/AbstractFramework) ecosystem:
- **AbstractRuntime** (this repo) — durable workflow kernel (`src/abstractruntime/core/*`)
- **AbstractCore** — LLM + tools integration (wired via `src/abstractruntime/integrations/abstractcore/*`)
  Repo: [lpalbou/abstractcore](https://github.com/lpalbou/abstractcore)

At a high level, hosts define workflow graphs (`WorkflowSpec`) and AbstractRuntime executes them durably. When nodes request LLM/tool work (`EffectType.LLM_CALL`, `EffectType.TOOL_CALLS`), those effects are typically handled via AbstractCore.

```mermaid
flowchart LR
  Host["Host app / orchestrator"] -->|"WorkflowSpec"| RT["AbstractRuntime"]
  RT -->|"LLM_CALL / TOOL_CALLS"| AC["AbstractCore"]
  AC -->|"results / waits"| RT
```

## Install

Remote-light runtime:

```bash
pip install abstractruntime
```

The base install includes AbstractCore 2.13.38 or newer with remote provider,
tool, vision, voice, audio, and music integration, plus the
`abstractruntime-mcp-worker` entry point. It keeps inference remote/light by
default: local engines such as MLX, vLLM, HuggingFace/Torch, Diffusers, and
sentence-transformer embeddings are not selected unless you choose a hardware
profile or another package-specific local extra.

VisualFlow PDF document nodes use permissive dependencies in Runtime's base
install: `Read PDF` extracts text and metadata with `pypdf`, and `Write PDF`
renders text or Markdown-style report content to real PDF bytes with
`reportlab`.

Native Python hardware profiles add local inferencer stacks:

```bash
pip install "abstractruntime[apple]"
pip install "abstractruntime[gpu]"
```

`abstractruntime[apple]` delegates to AbstractCore's native Apple aggregate;
`abstractruntime[gpu]` delegates to AbstractCore's GPU aggregate.

## Quick start (pause + resume)

```python
from abstractruntime import Effect, EffectType, Runtime, StepPlan, WorkflowSpec
from abstractruntime.storage import InMemoryLedgerStore, InMemoryRunStore


def ask(run, ctx):
    return StepPlan(
        node_id="ask",
        effect=Effect(
            type=EffectType.ASK_USER,
            payload={"prompt": "Continue?"},
            result_key="user_answer",
        ),
        next_node="done",
    )


def done(run, ctx):
    answer = run.vars.get("user_answer") or {}
    text = answer.get("text") if isinstance(answer, dict) else None
    return StepPlan(node_id="done", complete_output={"answer": text})


wf = WorkflowSpec(workflow_id="demo", entry_node="ask", nodes={"ask": ask, "done": done})
rt = Runtime(run_store=InMemoryRunStore(), ledger_store=InMemoryLedgerStore())

run_id = rt.start(workflow=wf)
state = rt.tick(workflow=wf, run_id=run_id)
assert state.status.value == "waiting"

state = rt.resume(
    workflow=wf,
    run_id=run_id,
    wait_key=state.waiting.wait_key,
    payload={"text": "yes"},
)
assert state.status.value == "completed"
```

## What’s included (v0.4.26)

Kernel (import-light):
- workflow graphs: `WorkflowSpec` (`src/abstractruntime/core/spec.py`)
- durable execution: `Runtime.start/tick/resume` (`src/abstractruntime/core/runtime.py`)
- durable waits/events: `WAIT_EVENT`, `WAIT_UNTIL`, `ASK_USER`, `EMIT_EVENT`
- append-only ledger (`StepRecord`) + node traces (`vars["_runtime"]["node_traces"]`)
- retries/idempotency hooks: `src/abstractruntime/core/policy.py`
- runtime-aware limits (`_limits`) with a default iteration budget of 50 (`docs/limits.md`)

Durability + storage:
- stores: in-memory, JSON/JSONL, SQLite (`src/abstractruntime/storage/*`)
- durable command inbox primitives (idempotent, append-only): `CommandStore`, `CommandCursorStore` (`src/abstractruntime/storage/commands.py`, `src/abstractruntime/storage/sqlite.py`)
- artifacts + offloading (store large payloads by reference)
- snapshots/bookmarks (`docs/snapshots.md`)
- tamper-evident hash-chained ledger (`docs/provenance.md`)

Drivers + distribution:
- scheduler: `create_scheduled_runtime()` (`src/abstractruntime/scheduler/*`)
- VisualFlow compiler + WorkflowBundles (`src/abstractruntime/visualflow_compiler/*`, `src/abstractruntime/workflow_bundle/*`)
- VisualFlow multi-entry execution lowering for fan-in routes and per-entry input overrides (`docs/workflow-bundles.md`)
- VisualFlow LLM Call and Agent nodes propagate Core generation params such as
  `thinking` through Runtime effects. Provider Models nodes can apply Core
  `capability_route` filters so run-time model discovery matches Gateway/Flow
  authoring.
- VisualFlow image/video nodes and Runtime media helpers preserve task-specific
  Core media controls, including `count`/`n`, `seeds`, ordered
  `lora_adapters`, and video `flow_shift`, while keeping provider/model/task
  truth in AbstractCore and AbstractVision.
- VisualFlow structured LLM/Agent results preserve `response` as text and expose
  the schema-conformant object on `data`, so Break Object and Switch can consume
  fields without reparsing the response string.
- run history export: `export_run_history_bundle(...)` (`src/abstractruntime/history_bundle.py`)

Runtime-owned integrations:
- AbstractCore (LLM + tools, `MODEL_RESIDENCY`, public discovery/host/run facades, cached sessions, local-only prompt-cache export/import admin, durable bloc prompt-cache controls, bindings, lifecycle operations, generated image/video/voice/music outputs with progress events, host email helpers, Telegram host wrappers, and tool approval waits): `docs/integrations/abstractcore.md`
- For outbound comms, use the durable run facade when the send belongs to a run: `get_abstractcore_run_facade(...).send_email(...)` / `send_telegram_message(...)`. If that child run pauses for approval or passthrough execution, resume it through `resume_tool_calls(...)`. Direct host-facade send helpers and the standalone email comms facade remain host-local and nondurable.
- AbstractMemory TripleStore integration for `MEMORY_KG_*` effects. Runtime
  depends on the light AbstractMemory contract; hosts choose storage backends
  such as LanceDB, SQLite, or in-memory stores.
- comms toolset gating (email/WhatsApp/Telegram): `docs/tools-comms.md`

## Built-in scheduler (zero-config)

```python
from abstractruntime import create_scheduled_runtime

sr = create_scheduled_runtime()
run_id, state = sr.run(my_workflow)

if state.status.value == "waiting":
    state = sr.respond(run_id, {"text": "yes"})

sr.stop()
```

For persistent storage:

```python
from abstractruntime import create_scheduled_runtime, JsonFileRunStore, JsonlLedgerStore

sr = create_scheduled_runtime(
    run_store=JsonFileRunStore("./data"),
    ledger_store=JsonlLedgerStore("./data"),
)
```

## Documentation

| Document | Description |
|----------|-------------|
| [Getting Started](docs/getting-started.md) | Install + first durable workflow |
| [API Reference](docs/api.md) | Public API surface (imports + pointers) |
| [Docs Index](docs/README.md) | Full docs map (guides + reference) |
| [FAQ](docs/faq.md) | Common questions and gotchas |
| [Troubleshooting](docs/troubleshooting.md) | Symptom-oriented setup, runtime, and integration fixes |
| [Architecture](docs/architecture.md) | Component map + diagrams |
| [Overview](docs/proposal.md) | Design goals, core concepts, and scope |
| [Integrations](docs/integrations) | Integration guides (AbstractCore) |
| [Snapshots](docs/snapshots.md) | Named checkpoints for run state |
| [Provenance](docs/provenance.md) | Tamper-evident ledger documentation |
| [Evidence](docs/evidence.md) | Artifact-backed evidence capture for web/command tools |
| [Limits](docs/limits.md) | `_limits` namespace and RuntimeConfig |
| [WorkflowBundles](docs/workflow-bundles.md) | `.flow` bundle format (VisualFlow distribution) |
| [MCP Worker](docs/mcp-worker.md) | `abstractruntime-mcp-worker` CLI |
| [Changelog](CHANGELOG.md) | Release notes |
| [Contributing](CONTRIBUTING.md) | How to build/test and submit changes |
| [Code of Conduct](CODE_OF_CONDUCT.md) | Contributor conduct expectations |
| [Security](SECURITY.md) | Responsible vulnerability reporting |
| [Acknowledgments](ACKNOWLEDGMENTS.md) | Credits |
| [ROADMAP](ROADMAP.md) | Prioritized next steps |

## Development

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install -e ".[test,docs]"
python -m pytest -q
```

See `CONTRIBUTING.md` for contribution guidelines and doc conventions.

---

## docs/getting-started.md

# Getting started

This guide gets you from install to a durable **pause → resume** workflow quickly.

If you only read one doc after `README.md`, read this one.

## Install

Remote-light runtime:

```bash
pip install abstractruntime
```

This installs AbstractCore 2.13.38 or newer with remote provider, vision,
voice, audio, music, tool, and MCP-worker support. The base install is
remote-light: it can route multimodal workflows to hosted or OpenAI-compatible
endpoints, but it does not select local inferencer stacks such as MLX, vLLM,
HuggingFace/Torch, Diffusers, or sentence-transformer embeddings.

Use hardware profiles only when this Runtime host should execute local engines:

```bash
pip install "abstractruntime[apple]"
pip install "abstractruntime[gpu]"
```

## Mental model (source of truth)

- Workflows are in-memory graphs: `WorkflowSpec` (`src/abstractruntime/core/spec.py`)
- Runs are durable checkpoints: `RunState` (`src/abstractruntime/core/models.py`)
- Nodes return “what to do next”: `StepPlan` (`src/abstractruntime/core/models.py`)
- Side effects are requested, not executed directly: `Effect` / `EffectType` (`src/abstractruntime/core/models.py`)
- Blocking is explicit and durable: `WaitState` (`src/abstractruntime/core/models.py`)
- Every step is append-only in the ledger: `StepRecord` (`src/abstractruntime/core/models.py`)

The execution loop is implemented in `Runtime.start/tick/resume` (`src/abstractruntime/core/runtime.py`).

## Quick start: pause + resume

```python
from abstractruntime import Effect, EffectType, Runtime, StepPlan, WorkflowSpec
from abstractruntime.storage import InMemoryLedgerStore, InMemoryRunStore


def ask(run, ctx):
    return StepPlan(
        node_id="ask",
        effect=Effect(type=EffectType.ASK_USER, payload={"prompt": "Continue?"}, result_key="answer"),
        next_node="done",
    )


def done(run, ctx):
    answer = run.vars.get("answer") or {}
    text = answer.get("text") if isinstance(answer, dict) else None
    return StepPlan(node_id="done", complete_output={"answer": text})


wf = WorkflowSpec(workflow_id="demo", entry_node="ask", nodes={"ask": ask, "done": done})
rt = Runtime(run_store=InMemoryRunStore(), ledger_store=InMemoryLedgerStore())

run_id = rt.start(workflow=wf)
state = rt.tick(workflow=wf, run_id=run_id)
assert state.status.value == "waiting"

state = rt.resume(workflow=wf, run_id=run_id, wait_key=state.waiting.wait_key, payload={"text": "yes"})
assert state.status.value == "completed"
print(state.output)
```

## Recommended: use the built-in scheduler wrapper

For most apps, use `create_scheduled_runtime()` which bundles:
- `Runtime`
- `Scheduler` (in-process polling driver)
- `WorkflowRegistry` (maps `workflow_id` → `WorkflowSpec`)

Implementation: `src/abstractruntime/scheduler/*`.

```python
from datetime import datetime, timedelta, timezone
import time

from abstractruntime import create_scheduled_runtime, Effect, EffectType, StepPlan, WorkflowSpec, RunStatus

def wait(run, ctx):
    until = (datetime.now(timezone.utc) + timedelta(seconds=2)).isoformat()
    return StepPlan(
        node_id="wait",
        effect=Effect(type=EffectType.WAIT_UNTIL, payload={"until": until}),
        next_node="done",
    )

def done(run, ctx):
    return StepPlan(node_id="done", complete_output={"ok": True})

wf = WorkflowSpec(workflow_id="demo_wait_until", entry_node="wait", nodes={"wait": wait, "done": done})

sr = create_scheduled_runtime(poll_interval_s=0.2)  # in-memory stores, scheduler auto-starts
run_id, state = sr.run(wf)
assert state.status == RunStatus.WAITING

time.sleep(3)
state = sr.get_state(run_id)
print(state.status.value, state.output)

sr.stop()
```

## Persist runs + ledgers (survive restarts)

Use file-backed stores:

```python
from abstractruntime import create_scheduled_runtime, JsonFileRunStore, JsonlLedgerStore

sr = create_scheduled_runtime(
    run_store=JsonFileRunStore("./data"),
    ledger_store=JsonlLedgerStore("./data"),
)
```

Notes:
- `JsonFileRunStore` stores `run_<run_id>.json`
- `JsonlLedgerStore` stores `ledger_<run_id>.jsonl`
- Durable state must be JSON-serializable; for large payloads use `ArtifactStore`/offloading (see `architecture.md`)

## Optional: LLM + tools (AbstractCore)

AbstractRuntime’s kernel is dependency-light; LLM/tool execution is wired via the AbstractCore integration:
- docs: `integrations/abstractcore.md`
- code: `src/abstractruntime/integrations/abstractcore/*`

Typical local mode:

```python
from abstractruntime.integrations.abstractcore import create_local_runtime

rt = create_local_runtime(provider="ollama", model="qwen3:4b")
```

## Next reading

- `faq.md` — common questions and gotchas
- `api.md` — public API surface (imports + pointers)
- `architecture.md` — component map + durability invariants (with diagrams)
- `manual_testing.md` — smoke tests and how to run `pytest`
- `../examples/README.md` — runnable scripts
- `integrations/abstractcore.md` — `LLM_CALL` / `TOOL_CALLS`, cached sessions, durable bloc prompt-cache, media inputs, generated media

---

## docs/api.md

# API reference

This document summarizes the **public Python API** of AbstractRuntime and points to the **source of truth in code**.

Public exports live in `src/abstractruntime/__init__.py`. If you are unsure what is supported for external use, start there.

Stability guideline:
- Prefer imports from `abstractruntime` (package root) and `abstractruntime.storage`.
- Deep imports from `abstractruntime.core.*` / `abstractruntime.storage.*` are fine for advanced use, but treat them as lower-stability unless they are explicitly documented/re-exported.

## Recommended imports

Core kernel:

```python
from abstractruntime import Effect, EffectType, Runtime, StepPlan, WorkflowSpec
```

Storage helpers (common stores):

```python
from abstractruntime.storage import (
    InMemoryLedgerStore,
    InMemoryRunStore,
    JsonFileRunStore,
    JsonlLedgerStore,
)
```

Scheduler convenience wrapper:

```python
from abstractruntime import create_scheduled_runtime
```

AbstractCore integration (included in the base `abstractruntime` install):

```python
from abstractruntime.integrations.abstractcore import (
    ApprovalToolExecutor,
    MappingToolExecutor,
    ToolApprovalPolicy,
    create_local_runtime,
)
```

See also: `getting-started.md` (end-to-end runnable examples).

## Core types (durable workflow semantics)

Implementation: `src/abstractruntime/core/models.py`, `src/abstractruntime/core/spec.py`.

- `WorkflowSpec`: in-memory workflow graph (`workflow_id`, `entry_node`, `nodes`).
- `StepPlan`: node return value (what happens next): `effect`, `next_node`, or `complete_output`.
- `Effect` / `EffectType`: durable side-effect request protocol (the runtime mediates execution).
- `RunState` / `RunStatus`: durable checkpoint for a run, persisted by a `RunStore`.
- `WaitState` / `WaitReason`: durable pause metadata for `WAIT_*` / `ASK_USER` / passthrough tool waits.

Durability invariant: `RunState.vars` must remain JSON-serializable (`src/abstractruntime/core/models.py`). For large payloads use artifacts/offloading (`src/abstractruntime/storage/artifacts.py`, `src/abstractruntime/storage/offloading.py`).

## Runtime (start / tick / resume)

Implementation: `src/abstractruntime/core/runtime.py`.

- `Runtime.start(workflow, vars=..., actor_id=..., session_id=...) -> run_id`
  - creates and persists a new `RunState`
- `Runtime.tick(workflow, run_id, max_steps=...) -> RunState`
  - executes node handlers and effects until the run becomes `WAITING`, `COMPLETED`, `FAILED`, or `CANCELLED`
- `Runtime.resume(workflow, run_id, wait_key, payload, max_steps=...) -> RunState`
  - validates the `wait_key`, writes `payload` to `WaitState.result_key` (if set), and continues from `WaitState.resume_to_node`
- `Runtime.get_state(run_id) -> RunState` and `Runtime.get_ledger(run_id) -> list[dict]`
  - host-facing read APIs for checkpoints and the append-only ledger

For the execution model (ledger records, effect outcomes, waits), see `architecture.md`.

## Scheduler convenience API

Implementation: `src/abstractruntime/scheduler/*`.

Use `create_scheduled_runtime()` for a zero-config wrapper that bundles `Runtime` + an in-process polling `Scheduler`:
- `ScheduledRuntime.run(workflow, vars=..., actor_id=..., max_steps=...) -> (run_id, state)` (`src/abstractruntime/scheduler/convenience.py`)
- `ScheduledRuntime.respond(run_id, payload) -> RunState` (resumes a waiting run using its stored `wait_key`)
- `ScheduledRuntime.stop()` (stops the scheduler thread/loop)

For time-based waits, the scheduler polls due runs via `QueryableRunStore.list_due_wait_until(...)` (`src/abstractruntime/storage/base.py`, `src/abstractruntime/scheduler/scheduler.py`).

## Storage layer (durability backends)

Interfaces: `RunStore`, `LedgerStore`, and `QueryableRunStore` are defined in `src/abstractruntime/storage/base.py`.

Included backends:
- In-memory (tests/dev): `InMemoryRunStore`, `InMemoryLedgerStore` (`src/abstractruntime/storage/in_memory.py`)
- Filesystem:
  - checkpoints: `JsonFileRunStore` (`src/abstractruntime/storage/json_files.py`)
  - append-only ledger: `JsonlLedgerStore` (`src/abstractruntime/storage/json_files.py`)
- SQLite:
  - `SqliteRunStore`, `SqliteLedgerStore` (`src/abstractruntime/storage/sqlite.py`)

Notes:
- `abstractruntime.storage` intentionally exports only the most common store types. SQLite types are available via:
  - `from abstractruntime import SqliteRunStore, SqliteLedgerStore`, or
  - `from abstractruntime.storage.sqlite import SqliteRunStore, SqliteLedgerStore`

Common decorators:
- `ObservableLedgerStore` for subscriptions (`src/abstractruntime/storage/observable.py`)
- `HashChainedLedgerStore` + `verify_ledger_chain(...)` for tamper-evidence (`src/abstractruntime/storage/ledger_chain.py`)
- `OffloadingRunStore` / `OffloadingLedgerStore` to store large values by artifact reference (`src/abstractruntime/storage/offloading.py`)

## Commands (durable control-plane inbox)

AbstractRuntime ships append-only, idempotent **command inbox** primitives designed for gateways/workers that must accept retries safely:
- models + interfaces: `CommandRecord`, `CommandStore`, `CommandCursorStore` (`src/abstractruntime/storage/commands.py`)
- backends: in-memory + JSONL (`src/abstractruntime/storage/commands.py`), SQLite (`src/abstractruntime/storage/sqlite.py`)

These APIs are exported at the package root (see `src/abstractruntime/__init__.py`).

## Artifacts (store by reference)

Implementation: `src/abstractruntime/storage/artifacts.py`.
Deep dive: `artifacts.md`.

Key types:
- `ArtifactStore` (interface), `InMemoryArtifactStore`, `FileArtifactStore`
- helpers: `artifact_ref(...)`, `resolve_artifact(...)`, `is_artifact_ref(...)`

The store keeps payload bytes out of run state and persists structured metadata:
- `ArtifactDescriptor` is the Runtime-owned descriptor used by Gateway and Observer. It separates `semantic_kind` such as `voice`, `music`, `sound`, or `image` from `render_kind` such as `audio`, `markdown`, `html`, or `json`, and can carry workflow/node/turn links, media facts, generation/provenance data, source refs, security, and action links.
- `ArtifactAccessStats` records explicit metadata/content/preview/download/export actions when HTTP or UI layers call `record_access(...)`. Plain `load(...)` and `get_metadata(...)` remain side-effect free.
- `search(...)`, `count(...)`, `facet_counts(...)`, and `stats(...)` provide metadata queries for host control planes. `FileArtifactStore` serves these from a repairable SQLite catalog when possible, including exact `total`, `total_bytes`, and requested facet counts without forcing Gateway/Observer to load every matching artifact.

Artifacts are used by:
- offloading wrappers (`src/abstractruntime/storage/offloading.py`)
- evidence capture (`docs/evidence.md`, `src/abstractruntime/evidence/recorder.py`)
- AbstractCore media integration: input artifact refs can be materialized for LLM calls, and generated image/video/voice/music/audio outputs are stored as artifact refs

## Snapshots / bookmarks

Implementation: `src/abstractruntime/storage/snapshots.py`.

- `SnapshotStore` interface + `InMemorySnapshotStore`, `JsonSnapshotStore`
- `Snapshot` model (a named bookmark of run state)

Docs: `snapshots.md`.

## Effect policies (retries + idempotency)

Implementation: `src/abstractruntime/core/policy.py`.

- `EffectPolicy` protocol and implementations: `DefaultEffectPolicy`, `RetryPolicy`, `NoRetryPolicy`
- `compute_idempotency_key(...)` helper

Docs: `architecture.md` (reliability section).

## WorkflowBundles (`.flow`) and VisualFlow distribution

Implementation:
- bundles: `src/abstractruntime/workflow_bundle/*`
- compiler: `src/abstractruntime/visualflow_compiler/*`

VisualFlow compiler helpers are available from `abstractruntime.visualflow_compiler`:
- `load_visualflow_json(...)` normalizes VisualFlow JSON into the stdlib model.
- `visual_to_flow(...)` lowers VisualFlow into the internal Flow IR.
- `compile_visualflow(...)` and `compile_visualflow_tree(...)` compile VisualFlow JSON into executable `WorkflowSpec` objects.

VisualFlow authoring note (media and document nodes):
- Runtime recognizes first-class VisualFlow media nodes such as `generate_image`, `edit_image`, `image_to_image`, `upscale_image`, `image_upscale`, `generate_video`, `text_to_video`, `image_to_video`, `generate_voice`, `generate_music`, `transcribe_audio`, and `listen_voice`.
- Generated-media and transcription nodes lower to a durable `EffectType.LLM_CALL` with an `output` selector (for example `{"modality":"music","task":"music_generation"}`), while `listen_voice` lowers to `WAIT_EVENT`. Hosts should persist the authoring node type rather than pre-lowering to `llm_call`.
- Runtime also recognizes file/document nodes. `read_file` and `write_file`
  handle UTF-8 text/JSON workspace paths. In Gateway-hosted runs, those paths
  follow the shared canonical contract: `rel/path` for the main workspace root
  and `mount_alias/rel/path` for approved mounts. `read_pdf` extracts text and
  metadata from PDF paths with `pypdf`; `write_pdf` renders text or
  Markdown-style content to real PDF bytes with `reportlab`; `list_folder_files`
  enumerates workspace-scoped folders with family/extension filters;
  `import_workspace_file` snapshots a workspace file into a durable artifact;
  `read_artifact` projects saved file content back out as text/JSON/bounded
  binary metadata; and `export_artifact` writes a durable artifact back to a
  workspace path. PDF bytes are written to the workspace path and only
  JSON-safe metadata/path values are stored in run state. In local Runtime-only
  runs with no workspace scope, relative file-node paths still fall back to the
  process working directory.

Public bundle APIs are exported from `src/abstractruntime/workflow_bundle/__init__.py` and re-exported in `src/abstractruntime/__init__.py`:
- open: `open_workflow_bundle(...)`
- registry: `WorkflowBundleRegistry`
- pack/unpack: `pack_workflow_bundle(...)`, `unpack_workflow_bundle(...)`

Docs: `workflow-bundles.md`.

## Run history bundle export (portable replay artifact)

Implementation: `src/abstractruntime/history_bundle.py`.

- `export_run_history_bundle(...)`
- `persist_workflow_snapshot(...)`

This produces a portable record of a run’s state + ledger + artifacts suitable for debugging/review.

## Runtime-owned integrations

### AbstractCore (LLM + tools)

Requires: `pip install abstractruntime` (AbstractCore 2.13.38 or newer is part of the base install).

Implementation: `src/abstractruntime/integrations/abstractcore/*`.

Entry points:
- `create_local_runtime(...)`, `create_remote_runtime(...)`, `create_hybrid_runtime(...)` (`src/abstractruntime/integrations/abstractcore/factory.py`)
- public discovery facade: `AbstractCoreDiscoveryFacade`, `get_abstractcore_discovery_facade(...)` (`src/abstractruntime/integrations/abstractcore/discovery_facade.py`)
- public host facade: `AbstractCoreHostFacade`, `get_abstractcore_host_facade(...)` (`src/abstractruntime/integrations/abstractcore/host_facade.py`)
- public email comms wrappers: `list_email_accounts(...)`, `list_emails(...)`, `read_email(...)`, `send_email(...)` (`src/abstractruntime/integrations/abstractcore/comms_facade.py`)
- public Telegram host wrappers: `TelegramTdlibNotAvailable`, `bootstrap_telegram_auth_from_env(...)`, `get_global_telegram_client(...)`, `stop_global_telegram_client()`, `send_telegram_message(...)` (`src/abstractruntime/integrations/abstractcore/telegram_facade.py`)
- public durable run facade: `AbstractCoreRunFacade`, `get_abstractcore_run_facade(...)` (`src/abstractruntime/integrations/abstractcore/run_facade.py`)
- effect handler wiring: `build_effect_handlers(...)` (`src/abstractruntime/integrations/abstractcore/effect_handlers.py`)
- tool executors: `MappingToolExecutor`, `AbstractCoreToolExecutor`, `PassthroughToolExecutor`, `ApprovalToolExecutor`, `ToolApprovalPolicy` (`src/abstractruntime/integrations/abstractcore/tool_executor.py`)
- discovery-facade delegation is implemented by the configured AbstractCore LLM clients in `src/abstractruntime/integrations/abstractcore/llm_client.py` (`list_providers`, `list_provider_models`, `get_voice_catalog`, `list_tts_models`, `list_stt_models`, `list_music_providers`, `list_music_models`, `list_vision_provider_models`, `list_cached_vision_models`, `list_vision_adapters`)
- host-facade client delegation is implemented by the configured AbstractCore LLM clients in `src/abstractruntime/integrations/abstractcore/llm_client.py` (`get_prompt_cache_capabilities`, `get_prompt_cache_stats`, `prompt_cache_set`, `prompt_cache_update`, `prompt_cache_fork`, `prompt_cache_clear`, `prompt_cache_prepare_modules`, `upsert_text_bloc`, `get_bloc_record`, `list_blocs`, `get_bloc_kv_manifest`, `ensure_bloc_kv_artifact`, `load_bloc_kv_artifact`, `list_bloc_kv_artifacts`, `delete_bloc_kv_artifact`, `prune_bloc_kv_artifacts`, `delete_bloc`, `get_model_residency_capabilities`, `list_model_residency`, `load_model_residency`, `unload_model_residency`)
- host-local prompt-cache export/import admin also lives on the host facade and client delegation layer (`list_prompt_cache_exports`, `prompt_cache_export`, `prompt_cache_import`) and is intentionally local-only
- host-facade email helpers delegate to Runtime's host-local comms facade/export layer (`list_email_accounts`, `list_emails`, `read_email`, `send_email`)
- run-facade helpers create and resume durable child runs for existing runs (`execute_llm_call`, `execute_tool_calls`, `resume_tool_calls`, `generate_image`, `edit_image`, `upscale_image`, `generate_video`, `image_to_video`, `generate_voice`, `generate_music`, `transcribe_audio`, `send_email`, `send_telegram_message`)
- task-specific image/video helpers preserve batch and adapter controls such as
  `count`/`n`, `seeds`, ordered `lora_adapters`, and video `flow_shift`; local
  subprocess isolation stays within the same public contract.

`LLM_CALL` payloads are JSON-safe effect payloads. Common fields:
- `prompt`, `messages`, `system_prompt`, and convenience `text`
- `media`: a media path, artifact ref (`{"$artifact": "..."}` or `{"artifact_id": "..."}`), media dict, or list of those
- `output`: AbstractCore output selector; top-level `outputs` is accepted as a runtime alias
- `params`: provider/model routing, generation controls, prompt-cache keys or `prompt_cache_binding`, structured-output schema options, and tracing metadata

Multimodal support:
- common remote-light AbstractCore media, vision, voice, audio, and music dependencies are part of the base Runtime install
- local clients call AbstractCore's unified `generate(..., media=..., output=...)`
- remote and hybrid clients support AbstractCore Server chat media content arrays plus image generation, image edits, image upscaling, text-to-video, image-to-video, speech, music generation, and transcription endpoints; pass an output-specific `model` for remote media provider routing, otherwise the server endpoint can use its configured capability default
- remote transcription requires one audio media item that resolves to a local file path or artifact-backed temporary file
- generated image/video/voice/music/audio bytes require a runtime `ArtifactStore`; the result contains `artifact_id` / `artifact_ref` instead of inline bytes
- media-only normalized results expose `runtime_provider` / `runtime_model` separately from `media_provider` / `media_model`
- optional local media residency failures complete with `status_hint="warning"` and `degraded=true`; unsupported local media warmup for `image_generation`, `image_upscale`, `video_generation`, `text_to_video`, `image_to_video`, `tts`, `stt`, and `music_generation` reports `requires_long_lived_server=true`, and generated image/video tasks also report `execution_mode="local_one_shot_subprocess"`
- Gateway/hosts remain responsible for explicit Core server URLs, Core server auth headers, provider/model defaults, selected local-inference profiles, and translation of Gateway-owned env/config into explicit Runtime inputs; Runtime persists only JSON-safe routing metadata and artifact refs

Prompt cache / cached sessions:
- LLM clients expose cache control methods listed above for host-side preparation and inspection
- `LLM_CALL.params.prompt_cache_key` selects a cache key for a call; runtime can also derive a session-scoped key from `run.vars["_runtime"]["prompt_cache"]` or the Runtime-owned `ABSTRACTRUNTIME_PROMPT_CACHE` process default
- `LLM_CALL.params.prompt_cache_binding` is the durable exact-reuse input for bloc-backed prompt caching; if a binding includes `key`, Runtime adopts it as the effective prompt-cache key and refuses mismatches before provider execution
- Runtime only auto-derives session prompt-cache keys for text/chat calls; non-text output selectors such as image, voice, music, and transcription keep explicit `prompt_cache_binding` support but do not receive an inferred cache key
- `get_abstractcore_host_facade(...)` also exposes durable bloc helpers (`upsert_text_bloc`, `get_bloc_record`, `list_blocs`, `get_bloc_kv_manifest`, `ensure_bloc_kv_artifact`, `load_bloc_kv_artifact`, `list_bloc_kv_artifacts`, `delete_bloc_kv_artifact`, `prune_bloc_kv_artifacts`, `delete_bloc`)
- local Runtime owns the bloc root policy: `~/.abstractruntime/blocs` by default, `<base_dir>/blocs` for `create_local_file_runtime(...)`, and explicit `bloc_root_dir=...` overrides when needed
- provider cache/session handles are not durable runtime state and should not be stored in `RunState.vars`

Attachment registration limits:
- `TOOL_CALLS.payload.max_attachment_bytes`, `run.vars["_runtime"]["max_attachment_bytes"]`, or `ABSTRACTRUNTIME_MAX_ATTACHMENT_BYTES` bound the bytes Runtime stores when local `read_file` outputs are captured as session attachments

Docs: `integrations/abstractcore.md`.

### AbstractMemory bridge (KG effects)

Implementation: `src/abstractruntime/integrations/abstractmemory/effect_handlers.py`.

This provides handlers for `MEMORY_KG_*` effects (opt-in wiring layer).

## Utilities (host UX)

- Rendering helpers: `abstractruntime.rendering.stringify_json(...)` and `abstractruntime.rendering.render_agent_trace_markdown(...)` (`src/abstractruntime/rendering/*`)
- Active-context helpers (what is sent to the LLM): `ActiveContextPolicy`, `TimeRange` (`src/abstractruntime/memory/active_context.py`, exports in `src/abstractruntime/memory/__init__.py`)

## See also

- `../README.md` — install + quick start
- `getting-started.md` — first durable workflow
- `architecture.md` — component map + durability invariants
- `faq.md` — common questions and gotchas
- `integrations/abstractcore.md` — `LLM_CALL` / `TOOL_CALLS` wiring

---

## docs/architecture.md

# AbstractRuntime — Architecture

> Updated: 2026-06-13
> Version: 0.4.29
> Scope: this describes **what is implemented in this repository**.

AbstractRuntime is a **durable workflow runtime**: it executes workflow graphs as a persisted state machine with explicit waits (user, time, events, jobs, subworkflows). A run can pause for hours/days and resume **without** keeping Python stacks/coroutines alive.

## Ecosystem (AbstractFramework)

AbstractRuntime is the durable execution kernel inside the wider AbstractFramework ecosystem:
- AbstractFramework umbrella: [lpalbou/AbstractFramework](https://github.com/lpalbou/AbstractFramework)
- AbstractCore (LLM + tools): [lpalbou/abstractcore](https://github.com/lpalbou/abstractcore)

The runtime stays dependency-light and delegates LLM/tool execution to integrations (notably AbstractCore): `src/abstractruntime/integrations/abstractcore/*`. Runtime also depends on the light AbstractMemory contract so `MEMORY_KG_*` effects always have the TripleStore model types available; hosts still choose the concrete memory backend.

```mermaid
flowchart LR
  Host["Host app / AbstractFlow / service"] -->|"WorkflowSpec"| RT["AbstractRuntime"]
  RT -->|"LLM_CALL / TOOL_CALLS"| AC["AbstractCore"]
  AC -->|"results / waits"| RT
```

Key invariants (enforced by code, not convention):
- **Durable state is JSON-safe**: `RunState.vars` must remain JSON-serializable (`src/abstractruntime/core/models.py`). Large payloads should be stored as artifacts and referenced (`src/abstractruntime/storage/artifacts.py`, `src/abstractruntime/storage/offloading.py`).
- **Append-only observability**: every step is recorded as a `StepRecord` in a `LedgerStore` (`src/abstractruntime/core/models.py`, `src/abstractruntime/storage/base.py`).
- **Side effects are mediated**: nodes request work via `Effect`/`EffectType`; execution happens via effect handlers (`src/abstractruntime/core/runtime.py`).

## AbstractCore capability boundary

AbstractRuntime's job is persistence and orchestration. AbstractCore owns model/provider capability execution: chat, structured output, cached sessions/prompt cache, media input analysis, image generation, video generation, voice/audio generation, music generation, and transcription.

The boundary is intentionally narrow:
- Workflow nodes request model work with `EffectType.LLM_CALL`; the runtime persists the request/result and delegates execution to the configured AbstractCore client.
- `media` inputs remain JSON-safe in the effect payload. Artifact refs are materialized into temporary provider-ready files for the call, then cleaned up.
- Generated binary outputs are written to `ArtifactStore` and returned as `artifact_id` / `artifact_ref`, keeping `RunState.vars` and ledger records bounded and JSON-safe.
- Remote chat media is sent to AbstractCore Server as provider-ready content arrays, but persisted provider-request metadata redacts data URLs so checkpoints and ledgers do not embed media bytes.
- Provider sessions and prompt-cache objects are not runtime state. Runtime may carry stable cache keys, while AbstractCore clients/servers manage warm caches.
- Hosts should use Runtime-owned AbstractCore facades for discovery snapshots, prompt-cache/model-residency control operations, and durable run-scoped media/comms child runs instead of reaching through private runtime attachments or importing Core internals directly.
- Local execution can use richer AbstractCore capability plugins. Remote and hybrid execution map the common media cases to AbstractCore Server endpoints and OpenAI-compatible content arrays, while hybrid keeps tool execution local.
- Gateway and other hosts compose Runtime with the desired memory and local-inference profile. Runtime's base package includes the AbstractMemory contract, AbstractCore remote/tool capability integration, Runtime-owned permissive PDF read/write support, and the MCP worker entry point, but not backend extras such as LanceDB, Core media document stacks, or local inferencer stacks. Hosts choose storage, embeddings, readiness policy, and whether to add `abstractruntime[apple]` or `abstractruntime[gpu]`.
- Remote and hybrid clients use explicit Core server URLs and auth headers supplied by the host. Runtime does not read Gateway auth environment variables for provider/model/auth decisions or treat Gateway bearer tokens as Core server/provider credentials.

This keeps the runtime usable by `../abstractgateway` and application layers such as `../abstractflow`, `../abstractassistant`, `../abstractobserver`, and `../abstractcode` without embedding provider-specific model logic in the durable kernel.

## Component map

```mermaid
flowchart TB
  subgraph Core["core/ (execution kernel)"]
    Models["models.py\nRunState / StepPlan / Effect / WaitState / StepRecord"]
    Runtime["runtime.py\nRuntime.start / tick / resume"]
    Spec["spec.py\nWorkflowSpec"]
    Policy["policy.py\nEffectPolicy + idempotency"]
    Vars["vars.py\nnamespaces + _limits + node_traces"]
    Config["config.py\nRuntimeConfig"]
  end

  subgraph Storage["storage/ (durability)"]
    RunStore["RunStore\nin_memory / json_files / sqlite"]
    LedgerStore["LedgerStore\n(+observable + hash_chain)"]
    Commands["commands.py\nCommandStore / CommandCursorStore"]
    Artifacts["ArtifactStore\nin_memory / file"]
    Offload["Offloading* wrappers\nstore large values by ref"]
    Snapshots["SnapshotStore\nin_memory / json"]
  end

  subgraph Scheduler["scheduler/ (drivers)"]
    Registry["WorkflowRegistry"]
    SchedulerMod["Scheduler\npoll + resume"]
    SR["ScheduledRuntime\nconvenience"]
  end

  subgraph Distribution["workflow_bundle/ + visualflow_compiler/"]
    Bundles["WorkflowBundles (.flow)\nmanifest + flows/*.json"]
    Compiler["VisualFlow compiler\nVisualFlow JSON -> WorkflowSpec"]
  end

  subgraph Integrations["integrations/ (optional wiring)"]
    AC["abstractcore/\nLLM_CALL, TOOL_CALLS, MCP worker"]
    AM["abstractmemory/\nMEMORY_KG_* handlers"]
  end

  Runtime --> RunStore
  Runtime --> LedgerStore
  Runtime --> Artifacts
  Runtime --> Policy
  Runtime --> Vars
  Runtime --> Config

  SR --> SchedulerMod
  SR --> Runtime
  SchedulerMod --> RunStore
  SchedulerMod --> Registry

  Bundles --> Compiler
  AC --> Runtime
  AM --> Runtime
```

## Durable execution model

### WorkflowSpec and node handlers
- A workflow is a `WorkflowSpec` (`src/abstractruntime/core/spec.py`): `workflow_id`, `entry_node`, `nodes: dict[node_id, handler]`.
- A node handler returns a `StepPlan` (`src/abstractruntime/core/models.py`):
  - `effect`: optional side-effect request (`Effect`)
  - `next_node`: move the execution cursor
  - `complete_output`: finish the run

### RunState and the ledger
- `RunState` is the durable checkpoint stored by a `RunStore` (`src/abstractruntime/core/models.py`, `src/abstractruntime/storage/base.py`).
- Each executed step appends a `StepRecord` to a `LedgerStore` (`src/abstractruntime/core/models.py`, `src/abstractruntime/storage/base.py`).

**Invariant:** values stored in `RunState.vars` must be JSON-serializable. Use artifact references for large values (`src/abstractruntime/storage/artifacts.py`) or wrap stores with `OffloadingRunStore` / `OffloadingLedgerStore` (`src/abstractruntime/storage/offloading.py`).

## Runtime loop (start / tick / resume)

Implemented in `src/abstractruntime/core/runtime.py`:

- `Runtime.start(...)` creates a new `RunState` and initializes `_limits` from `RuntimeConfig` (`src/abstractruntime/core/config.py`).
- `Runtime.tick(...)` executes nodes until the run becomes `WAITING`, `COMPLETED`, `FAILED`, or is `CANCELLED`.
- `Runtime.resume(...)` validates the `wait_key`, writes the payload to `WaitState.result_key`, and continues execution **from** `WaitState.resume_to_node`.

```mermaid
sequenceDiagram
  participant Host
  participant RT as Runtime
  participant Node as Node handler
  participant EH as Effect handler
  participant RS as RunStore
  participant LS as LedgerStore

  Host->>RT: start(workflow, vars)
  RT->>RS: save(RunState RUNNING)

  Host->>RT: tick(run_id)
  RT->>Node: handler(run, ctx)
  Node-->>RT: StepPlan(effect?, next_node?, complete_output?)

  alt StepPlan.effect
    RT->>LS: append(StepRecord STARTED)
    RT->>EH: handle(effect)
    EH-->>RT: outcome (completed|waiting|failed)
    RT->>LS: append(StepRecord COMPLETED/WAITING/FAILED)
  end

  alt outcome=waiting
    RT->>RS: save(RunState WAITING + WaitState)
    RT-->>Host: RunState(waiting)
  else outcome=completed
    RT->>RS: save(RunState RUNNING/COMPLETED)
  end

  Host->>RT: resume(run_id, wait_key, payload)
  RT->>RS: save(RunState RUNNING)
  RT->>RT: tick(...)
```

## Effects: built-in vs wired by hosts

### Built-in (kernel-owned) effects
Registered in `Runtime._register_builtin_handlers()` (`src/abstractruntime/core/runtime.py`):
- waits: `WAIT_EVENT`, `WAIT_UNTIL`, `ASK_USER`, `ANSWER_USER`
- durable events: `EMIT_EVENT` (resumes matching `WAIT_EVENT` runs; requires `QueryableRunStore` and a workflow registry when listeners exist)
- subworkflows: `START_SUBWORKFLOW` (requires `runtime.workflow_registry`; see `src/abstractruntime/scheduler/registry.py`)
- memory primitives (JSON-safe): `MEMORY_NOTE`, `MEMORY_QUERY`, `MEMORY_TAG`, `MEMORY_COMPACT`, `MEMORY_REHYDRATE`
  - `MEMORY_COMPACT` requires an `ArtifactStore`. It uses an injected `chat_summarizer` when available; otherwise it runs an internal `LLM_CALL` subworkflow and therefore requires an `LLM_CALL` handler to be wired.
- inspection: `VARS_QUERY` (read-only access to `RunState.vars` paths; parsing helpers in `src/abstractruntime/core/vars.py`)

### Host-wired effects
The kernel defines the protocol; concrete integrations provide handlers:
- `LLM_CALL`, `TOOL_CALLS`, `MODEL_RESIDENCY`: provided by AbstractCore integration (`src/abstractruntime/integrations/abstractcore/effect_handlers.py`). The integration supports local/remote/hybrid execution, cached sessions/prompt-cache control, discovery/catalog snapshots, model residency, durable run-scoped media child runs, media inputs, generated media outputs, provider progress callbacks as ledger events, provider-key header routing for remote servers, passthrough tools, and approval-gated local tool execution.
- `MEMORY_KG_*`: provided by the AbstractMemory bridge (`src/abstractruntime/integrations/abstractmemory/effect_handlers.py`)

### Reliability: retries + idempotency
- Policies live in `src/abstractruntime/core/policy.py` (e.g., `RetryPolicy`, `NoRetryPolicy`, `compute_idempotency_key()`).
- The runtime records `idempotency_key` and `attempt` on ledger records (`StepRecord`) and can reuse prior results after restarts (`src/abstractruntime/core/runtime.py`).

## Storage layer

Interfaces: `src/abstractruntime/storage/base.py`.

Included backends:
- in-memory (tests/dev): `src/abstractruntime/storage/in_memory.py`
- filesystem JSON/JSONL: `src/abstractruntime/storage/json_files.py`
- SQLite: `src/abstractruntime/storage/sqlite.py`

Decorators/helpers:
- `ObservableLedgerStore` for in-process subscriptions (`src/abstractruntime/storage/observable.py`, exposed via `Runtime.subscribe_ledger()`)
- `HashChainedLedgerStore` for tamper-evidence (`src/abstractruntime/storage/ledger_chain.py`)
- `ArtifactStore` + helpers (`src/abstractruntime/storage/artifacts.py`)
- `OffloadingRunStore` / `OffloadingLedgerStore` to keep checkpoints bounded (`src/abstractruntime/storage/offloading.py`)
- snapshots/bookmarks (`src/abstractruntime/storage/snapshots.py`)

## Drivers: scheduler

The scheduler is an in-process driver loop that resumes due waits and can deliver external events:
- `Scheduler` (`src/abstractruntime/scheduler/scheduler.py`) polls `QueryableRunStore.list_due_wait_until(...)`
- `ScheduledRuntime` + `create_scheduled_runtime()` (`src/abstractruntime/scheduler/convenience.py`) is the "zero-config" wrapper used in `examples/`

## Observability: what you can export

- Ledger (source of truth): `Runtime.get_ledger(run_id)` (`src/abstractruntime/core/runtime.py`)
- Runtime-owned node traces (bounded): stored at `vars["_runtime"]["node_traces"]` (`src/abstractruntime/core/runtime.py`, helpers in `src/abstractruntime/core/vars.py`)
- Evidence capture for external-boundary tools (`web_search`, `fetch_url`, `execute_command`):
  - recorder: `src/abstractruntime/evidence/recorder.py`
  - API: `Runtime.list_evidence(...)` / `Runtime.load_evidence(...)` (`src/abstractruntime/core/runtime.py`)
- Run history bundle export (portable replay artifact):
  - `export_run_history_bundle(...)` (`src/abstractruntime/history_bundle.py`)

## VisualFlow + WorkflowBundles

AbstractRuntime includes a compiler and a portable bundle format:
- VisualFlow compiler: `src/abstractruntime/visualflow_compiler/*` (VisualFlow JSON -> `WorkflowSpec`)
- Multi-entry VisualFlow authoring routes are lowered into internal `join_exec` and `path_mux` nodes when a target has multiple incoming `exec-in` edges plus per-route input overrides. This keeps authoring JSON clean while making runtime behavior explicit and restart-safe. See `workflow-bundles.md` for the concrete metadata shape.
- VisualFlow document nodes include `read_pdf` and `write_pdf`. They use
  workspace-scoped paths, keep PDF bytes out of checkpoints, and store only
  JSON-safe extracted text, metadata, hashes, content types, and file paths in
  node outputs.
- WorkflowBundles (`.flow`): `src/abstractruntime/workflow_bundle/*` (manifest + flows + assets)
  - pack/unpack helpers: `pack_workflow_bundle(...)`, `open_workflow_bundle(...)`

## See also
- `../README.md` — install + quick start
- `getting-started.md` — first steps
- `api.md` — public API surface (imports + pointers)
- `limits.md` — `_limits` and RuntimeConfig
- `snapshots.md` — snapshot/bookmark stores
- `provenance.md` — hash chain and verification
- `evidence.md` — artifact-backed evidence capture
- `workflow-bundles.md` — `.flow` bundles + VisualFlow distribution
- `mcp-worker.md` — MCP worker entrypoint (`abstractruntime-mcp-worker`)
- `integrations/abstractcore.md` — AbstractCore wiring
- `manual_testing.md` — end-to-end smoke tests
- `adr/README.md` — rationale (why)

---

## docs/README.md

# Documentation

This folder contains **user-facing docs** (how to use AbstractRuntime) and **maintainer docs** (ADRs/backlog).

If you are new: read `getting-started.md` → `api.md` → `architecture.md`.

## Ecosystem

AbstractRuntime is part of the wider AbstractFramework ecosystem:
- AbstractFramework umbrella: [lpalbou/AbstractFramework](https://github.com/lpalbou/AbstractFramework)
- AbstractCore (LLM + tools): [lpalbou/abstractcore](https://github.com/lpalbou/abstractcore)

In this repo, the AbstractCore wiring lives under `src/abstractruntime/integrations/abstractcore/*` and is documented in `integrations/abstractcore.md`.

## Start here

- `../README.md` — install + quick start
- `getting-started.md` — first steps (recommended)
- `api.md` — public API surface (imports + pointers)
- `architecture.md` — how the runtime is structured (with diagrams)
- `troubleshooting.md` — symptom-oriented setup, runtime, and integration fixes
- `proposal.md` — design goals and scope boundaries

## Guides

- `faq.md` — common questions (recommended)
- `troubleshooting.md` — symptom-oriented setup, runtime, and integration fixes
- `manual_testing.md` — manual smoke tests and how to run the test suite
- `artifacts.md` — Runtime artifact identity, descriptors, provenance, catalog search, and access stats
- `integrations/abstractcore.md` — wiring `LLM_CALL` / `TOOL_CALLS`, cached sessions, durable bloc prompt-cache control, media inputs, generated media outputs, video progress events, and tool approval waits via AbstractCore
- `tools-comms.md` — enabling the optional comms toolset (email/WhatsApp/Telegram)
- `api.md#workflowbundles-flow-and-visualflow-distribution` — VisualFlow compiler APIs, media nodes, and PDF document nodes (`read_pdf` / `write_pdf`)

## Features (reference)

- `evidence.md` — artifact-backed evidence capture for external-boundary tools
- `mcp-worker.md` — MCP worker CLI (`abstractruntime-mcp-worker`)
- `snapshots.md` — snapshot/bookmark model and stores
- `provenance.md` — tamper-evident hash-chained ledger
- `limits.md` — runtime-aware `_limits` namespace and APIs
- `workflow-bundles.md` — `.flow` bundle format, VisualFlow distribution, and multi-entry fan-in metadata

## Maintainers

- `../CHANGELOG.md` — release notes
- `../CODE_OF_CONDUCT.md` — contributor conduct expectations
- `../CONTRIBUTING.md` — how to build/test and submit changes
- `../SECURITY.md` — responsible vulnerability reporting
- `../ACKNOWLEDGMENTS.md` — credits
- `../ROADMAP.md` — prioritized next steps
- `adr/README.md` — architectural decisions (why)
- `backlog/README.md` — implemented and planned work items (what/how)

---

## docs/faq.md

# FAQ

## What is AbstractRuntime (in one sentence)?

AbstractRuntime is a **durable workflow runtime**: it runs workflow graphs as a persisted state machine with explicit waits (pause → resume) and an append-only execution ledger.
Code: `src/abstractruntime/core/runtime.py`, `src/abstractruntime/core/models.py`.

## Is AbstractRuntime an agent framework?

No. AbstractRuntime is the **execution substrate**. Agent logic (ReAct/CodeAct loops, prompt policies, etc.) is built *on top* of it.
Docs: `proposal.md`. Code: `src/abstractruntime/core/*`.

## How does AbstractRuntime relate to AbstractCore / AbstractFramework?

AbstractRuntime is the **durable execution kernel**. In the AbstractFramework ecosystem, it is commonly paired with:
- **AbstractCore** for LLM + tool execution (`EffectType.LLM_CALL`, `EffectType.TOOL_CALLS`)
  Code: `src/abstractruntime/integrations/abstractcore/*`. Repo: [lpalbou/abstractcore](https://github.com/lpalbou/abstractcore)

AbstractFramework umbrella: [lpalbou/AbstractFramework](https://github.com/lpalbou/AbstractFramework)

## Where is the public API documented?

- API guide: `api.md`
- Canonical export list: `src/abstractruntime/__init__.py`

## How do pause/resume work?

- A node returns a `StepPlan` with an `Effect` (e.g. `ASK_USER`, `WAIT_UNTIL`, `WAIT_EVENT`).
- The runtime persists a `WaitState` into `RunState.waiting` and returns `status=waiting`.
- You resume by calling `Runtime.resume(...)` (or `ScheduledRuntime.respond(...)`) with the matching `wait_key`.

Docs: `getting-started.md`, `architecture.md`. Code: `src/abstractruntime/core/runtime.py` (`tick`, `resume`) and `src/abstractruntime/core/models.py` (`WaitState`).

## Does time-based waiting (`WAIT_UNTIL`) progress automatically?

Only if **something drives the runtime**:
- `Runtime.tick(...)` will auto-unblock a due `WAIT_UNTIL` run *when called*.
- The built-in `Scheduler` provides a driver loop that polls due waits and ticks runs.

Docs: `getting-started.md`, `architecture.md`. Code: `src/abstractruntime/core/runtime.py` (`tick`), `src/abstractruntime/scheduler/scheduler.py`.

## How do I resume a waiting run?

- If you have the `WorkflowSpec`: call `Runtime.resume(workflow=..., run_id=..., wait_key=..., payload=...)`.
- If you use `create_scheduled_runtime()`: call `sr.respond(run_id, payload)` (it uses `state.waiting.wait_key`).

Docs: `getting-started.md`. Code: `src/abstractruntime/core/runtime.py`, `src/abstractruntime/scheduler/convenience.py`.

## Why is my `ASK_USER` answer a dict?

`Runtime.resume(..., payload=...)` always takes a **dict** payload. If the wait has a `result_key`, the runtime stores that dict into `RunState.vars` at `result_key`.
Code: `src/abstractruntime/core/runtime.py` (`Runtime.resume`) and `src/abstractruntime/core/models.py` (`WaitState.result_key`).

Common pattern:
- resume with `{"text": "..."}` (host-side)
- read `run.vars["my_result_key"]["text"]` (node-side)

## What storage backends are included?

AbstractRuntime includes:
- in-memory: `InMemoryRunStore`, `InMemoryLedgerStore`
- filesystem: `JsonFileRunStore` (checkpoints), `JsonlLedgerStore` (append-only JSONL ledger)
- SQLite: `SqliteRunStore`, `SqliteLedgerStore`

Docs: `architecture.md`. Code: `src/abstractruntime/storage/*`.

## What must be JSON-serializable (and why)?

Everything stored in `RunState.vars` must be JSON-serializable because it is persisted as durable state.
Code: `src/abstractruntime/core/models.py` (`RunState`) and store implementations under `src/abstractruntime/storage/`.

For large values, use:
- `ArtifactStore` references (`src/abstractruntime/storage/artifacts.py`)
- offloading wrappers (`OffloadingRunStore`, `OffloadingLedgerStore`) (`src/abstractruntime/storage/offloading.py`)

Docs: `architecture.md`.

## How do I run LLM calls and tools?

LLM and tool execution are wired via the **AbstractCore integration**:
- `EffectType.LLM_CALL`
- `EffectType.TOOL_CALLS`

Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/*`.

## Can `LLM_CALL` analyze images, audio, or files?

Yes, when the configured AbstractCore provider/model supports the media. Pass `payload.media` as a path, a media dict, an artifact ref such as `{"$artifact": "..."}`, or a list of those. The runtime keeps the effect payload JSON-safe and materializes artifact refs into temporary provider-ready files for the call.

Common remote-light media/vision/audio/music dependencies are included in the base `abstractruntime` install. Use `abstractruntime[apple]` or `abstractruntime[gpu]` only when this host should execute local inferencer stacks.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/effect_handlers.py`, `src/abstractruntime/integrations/abstractcore/llm_client.py`.

## How do I generate images, video, voice/audio, or music?

Use `LLM_CALL` with AbstractCore's `output` selector:

```python
{"text": "A red cube on a white table", "output": {"modality": "image", "format": "png"}}
{"text": "A logo reveal", "output": {"modality": "video", "task": "text_to_video", "provider": "mlx-gen", "model": "Wan-AI/Wan2.2-TI2V-5B-Diffusers", "format": "mp4"}}
{"text": "Hello from Runtime", "output": {"modality": "voice", "voice": "alloy", "format": "wav"}}
{"text": "Warm lo-fi piano with brushed drums", "output": {"modality": "music", "provider": "acemusic", "model": "ace-step", "format": "wav"}}
```

Generated bytes require a runtime `ArtifactStore`. The durable result contains `artifact_id` / `artifact_ref`, not inline binary data. Remote and hybrid runtimes support common AbstractCore Server endpoints for image generation, image edits, text-to-video, image-to-video, speech, music generation, transcription, and chat media. Local runtimes can use richer AbstractCore capability plugins for voice cloning, reference-guided generation, local text-to-music, and local video generation when those AbstractCore capabilities are installed.

## Does AbstractRuntime implement image, voice, music, or video engines?

No. AbstractRuntime provides the durable graph runner, checkpoint/ledger model, waits, and artifact boundary. AbstractCore provides the LLM/media generation and analysis capabilities. Image, video, voice, transcription, and music all flow through the same JSON-safe `output` selector plus artifact-backed result shape; Runtime does not implement provider engines itself.

## Where should cached session or prompt-cache state live?

Store stable cache selectors or cache configuration in runtime-visible JSON. There are two main tracks:

- best-effort session reuse: `payload.params.prompt_cache_key`, `run.vars["_runtime"]["prompt_cache"]`, or the Runtime-owned `ABSTRACTRUNTIME_PROMPT_CACHE`
- durable exact reuse: `payload.params.prompt_cache_binding` from a previously loaded bloc/KV artifact

If a binding includes `key`, Runtime uses it as the effective prompt-cache key and does not derive a competing session key. Do not store provider session objects, cache handles, clients, or warm-cache state in `RunState.vars`. AbstractCore clients/servers own those objects, and runtime correctness should still hold when a cache is cold.

Gateway-specific prompt-cache environment variables should be consumed by Gateway and passed to Runtime explicitly; Runtime does not read the Gateway env namespace directly.

Hosts can inspect, prepare, and now clean up caches through `abstractruntime.integrations.abstractcore.get_abstractcore_host_facade(runtime)`, which exposes the normal prompt-cache/model-residency controls plus durable bloc helpers such as `upsert_text_bloc(...)`, `ensure_bloc_kv_artifact(...)`, `load_bloc_kv_artifact(...)`, `list_bloc_kv_artifacts(...)`, `delete_bloc_kv_artifact(...)`, and `delete_bloc(...)` without depending on the private runtime attachment directly.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/host_facade.py`, `src/abstractruntime/integrations/abstractcore/llm_client.py`.

## Can a host still export or import local provider prompt caches?

Yes, but treat that as **host-local operator tooling**, not the main durable
workflow memory model.

Use the Runtime host facade:
- `list_prompt_cache_exports(...)`
- `prompt_cache_export(...)`
- `prompt_cache_import(...)`

Important limits:
- this surface is **local-only**; remote and hybrid runtimes return
  `prompt_cache_local_only`
- Runtime owns the export root policy:
  - `~/.abstractruntime/prompt_cache_exports` by default
  - `<base_dir>/prompt_cache_exports` for `create_local_file_runtime(...)`
- exports are partitioned per provider/model, so the same logical export name
  can coexist cleanly across different local backends

For durable replay-safe workflow reuse, prefer `prompt_cache_binding` from
durable bloc/KV artifacts instead of host-local provider cache exports.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/host_facade.py`, `src/abstractruntime/integrations/abstractcore/llm_client.py`.

## Does Runtime duplicate durable bloc text? How do per-model caches relate to it?

For local runtimes, Runtime owns the bloc root and stores one durable **text snapshot** per SHA256 within that root. That bloc is the source of truth. The provider/model cache is a **derived artifact** under that bloc, not a second independent memory model.

So the intended shape is:
- one text/file bloc per content hash inside one Runtime bloc root
- zero or more derived cache artifacts, one per provider/model pair

That means the same bloc text can back several model-specific caches, but those caches are intentionally separate because provider/model-native KV formats are not portable.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/llm_client.py`, `../abstractcore/abstractcore/core/file_blocs.py`.

## Can I delete a specific durable bloc or prune old bloc caches?

Yes.

Use the Runtime host facade:
- `list_blocs(...)`
- `list_bloc_kv_artifacts(...)`
- `delete_bloc_kv_artifact(...)`
- `prune_bloc_kv_artifacts(...)`
- `delete_bloc(...)`

The important safety flags are:
- `dry_run=True` to preview the affected artifact or bloc set
- `clear_loaded=True` to clear matching live prompt-cache keys before deletion when Runtime can see that live state
- `force=True` only when you intentionally want to bypass the live-binding safety check

The important scope distinction is:
- `delete_bloc_kv_artifact(...)`: delete one provider/model artifact, keep the durable text bloc
- `delete_bloc(...)`: delete the durable text bloc itself and, by default, all derived KV artifacts under it

## Where should a host get provider / voice / music / vision catalogs from?

From Runtime. Use `abstractruntime.integrations.abstractcore.get_abstractcore_discovery_facade(runtime)` for
provider discovery, provider models, model capability lookup, voice/TTS/STT catalogs, music provider/model catalogs,
vision provider catalogs, and cached vision model snapshots.

These are snapshot/query reads, not durable `LLM_CALL` effects, so replay should use the recorded snapshot rather than
re-querying the current machine or server and pretending the answer is unchanged.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/discovery_facade.py`, `src/abstractruntime/integrations/abstractcore/discovery_queries.py`, `src/abstractruntime/integrations/abstractcore/llm_client.py`.

## Should Gateway or another host import AbstractCore comms or Telegram helpers directly?

No. For the remaining host/operator paths, use Runtime's public wrappers instead:

- `get_abstractcore_host_facade(runtime).list_email_accounts(...)`
- `...list_emails(...)`
- `...read_email(...)`
- `...send_email(...)`
- `abstractruntime.integrations.abstractcore.list_email_accounts(...)`
- `...list_emails(...)`
- `...read_email(...)`
- `...send_email(...)`
- `abstractruntime.integrations.abstractcore.telegram_facade.bootstrap_telegram_auth_from_env(...)`
- `...get_global_telegram_client(...)`
- `...stop_global_telegram_client()`
- `...send_telegram_message(...)`

Important nuance: the read/bootstrap wrappers are still **host-local**. They do not proxy through a remote Core
server, and they do not write durable Runtime history on their own. They exist so hosts can depend
on Runtime as the package boundary instead of importing `abstractcore.tools.comms_tools`,
`abstractcore.tools.telegram_tdlib`, or `abstractcore.tools.telegram_tools` directly.

For outbound sends that belong to a run, use the durable run facade instead:

- `get_abstractcore_run_facade(runtime).send_email(...)`
- `get_abstractcore_run_facade(runtime).send_telegram_message(...)`
- `get_abstractcore_run_facade(runtime).resume_tool_calls(...)` when an approval-gated or passthrough tool child run needs to continue

Those create child runs, record the send request and outcome in the ledger, and replay should show
the recorded result rather than resending the external message.

## Should a host execute image / TTS / music / STT directly for an existing run?

No. If the work is run-scoped and should become part of durable run history, the host should ask Runtime to execute it. Use `abstractruntime.integrations.abstractcore.get_abstractcore_run_facade(runtime)` and create a child run with `generate_image(...)`, `edit_image(...)`, `upscale_image(...)`, `generate_voice(...)`, `generate_music(...)`, `transcribe_audio(...)`, or the lower-level `execute_llm_call(...)`.

That keeps the ledger, artifacts, and replay surface Runtime-authored instead of synthesizing history after host-side work already happened.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/run_facade.py`.

## Why can local media residency return `ok:false` without failing the run?

Because local media warmup is not always a meaningful reusable state. In particular, local image generation may execute through a one-shot subprocess isolation boundary, so a prior warmup cannot be reused by the next request. Runtime therefore reports unsupported local media residency explicitly instead of pretending success.

For optional residency (`required=false`), the effect still completes durably but includes `status_hint="warning"` and `degraded=true`. Unsupported local media responses also report `requires_long_lived_server=true` and a `config_hint` that points at `ABSTRACTCORE_SERVER_BASE_URL`; image generation additionally reports `execution_mode="local_one_shot_subprocess"`.
Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/effect_handlers.py`, `src/abstractruntime/integrations/abstractcore/llm_client.py`.

## What are “local / remote / hybrid” execution modes?

They refer to where LLM and tools execute:
- **Local**: in-process LLM + local tool execution
- **Remote**: HTTP to an AbstractCore server + tools typically passthrough
- **Hybrid**: remote LLM + local tools

`create_local_runtime(...)` currently uses `MultiLocalAbstractCoreLLMClient` under the hood. That client is still
local-only: it can keep multiple in-process `(provider, model)` local clients warm and route between them per request,
but it does not switch between local and remote AbstractCore backends. If you want remote model execution, use
`create_remote_runtime(...)` or `create_hybrid_runtime(...)`.

Docs: `integrations/abstractcore.md`, `../docs/adr/0002_execution_modes_local_remote_hybrid.md`. Code: `src/abstractruntime/integrations/abstractcore/factory.py`.

## What does passthrough tool mode mean?

In passthrough mode, tool calls are **not executed** in-process:
- the `TOOL_CALLS` handler returns `WAITING` with tool call details
- an external worker/operator executes the tools
- the host resumes the run with the tool results

Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/tool_executor.py` (`PassthroughToolExecutor`).

## How do I require approval before tools run?

Use `ApprovalToolExecutor` around a trusted local executor. Safe read-only/default bridge tools can execute immediately; write, command, email/WhatsApp, and unknown tools produce a durable approval wait. Resume with `{"approved": true}` to run the pending calls or `{"approved": false, "reason": "..."}` to return structured tool errors.

Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/tool_executor.py`.

## How should provider API keys be passed to a remote AbstractCore server?

Use `Authorization: Bearer <server-key>` for AbstractCore server authentication. If a request needs a per-request upstream provider key, pass `params.provider_api_key` (or legacy `params.api_key`) in the runtime payload; Runtime converts it to the `X-AbstractCore-Provider-API-Key` header. Current AbstractCore servers reject provider keys in query strings or JSON bodies for security.

Docs: `integrations/abstractcore.md`. Code: `src/abstractruntime/integrations/abstractcore/llm_client.py`.

## Does AbstractRuntime retry effects (LLM/tools)? Is it idempotent?

Retry and idempotency are controlled via `EffectPolicy`:
- idempotency keys are used to reuse prior completed results after restarts
- retry behavior is configurable (e.g. `RetryPolicy`)

Docs: `architecture.md`. Code: `src/abstractruntime/core/policy.py`, `src/abstractruntime/core/runtime.py` (effect execution + reuse).

## Is the ledger tamper-proof?

No. The built-in provenance feature is **tamper-evident** (hash chain), not signature-backed non-forgeability.

Docs: `provenance.md`. Code: `src/abstractruntime/storage/ledger_chain.py`.

## How do I stream progress updates?

If your `LedgerStore` supports subscriptions (or is wrapped with `ObservableLedgerStore`), you can subscribe in-process:
- `Runtime.subscribe_ledger(callback, run_id=...)`

Long-running generated media uses the same ledger stream. Runtime converts provider progress callbacks into `EMIT_EVENT` ledger records named `abstract.progress` with JSON-safe payloads such as `phase`, `step`, `total_steps`, `frame`, `total_frames`, and `progress`.

Docs: `architecture.md`. Code: `src/abstractruntime/core/runtime.py` (`subscribe_ledger`), `src/abstractruntime/storage/observable.py`.

## What is “evidence capture”?

Evidence capture records durable, artifact-backed evidence for selected external-boundary tools:
- `web_search`, `fetch_url`, `execute_command`

It runs best-effort after successful `TOOL_CALLS` and requires an `ArtifactStore`.
Docs: `evidence.md`. Code: `src/abstractruntime/evidence/recorder.py`, `src/abstractruntime/core/runtime.py` (`_maybe_record_tool_evidence`, `list_evidence`, `load_evidence`).

## What are snapshots and are they safe to restore?

Snapshots are named bookmarks of run state. Restoring a snapshot is a host-level operation (load + write back into your RunStore).
Safety depends on whether workflow code/spec has changed since the snapshot was taken.

Docs: `snapshots.md`. Code: `src/abstractruntime/storage/snapshots.py`.

## How do WorkflowBundles (`.flow`) relate to `WorkflowSpec`?

`WorkflowSpec` is an in-memory graph of Python callables (not portable). WorkflowBundles (`.flow`) distribute **VisualFlow JSON** plus a manifest; hosts compile VisualFlow JSON into `WorkflowSpec` using the VisualFlow compiler.

Docs: `workflow-bundles.md`, `architecture.md`. Code: `src/abstractruntime/workflow_bundle/*`, `src/abstractruntime/visualflow_compiler/*`.

## How do I run the MCP worker?

Use the `abstractruntime-mcp-worker` CLI from the base Runtime install and select toolsets explicitly.

Docs: `mcp-worker.md`. Code: `src/abstractruntime/integrations/abstractcore/mcp_worker.py`.

## Where should I look for runnable examples?

- `../examples/README.md` (runnable scripts)
- `manual_testing.md` (smoke tests)

---

## docs/troubleshooting.md

# Troubleshooting

This page is for symptom-oriented fixes. For concepts and limits, see `faq.md`; for setup and examples, see
`getting-started.md`.

## Importing `abstractruntime.integrations.abstractcore` fails

Symptom:
- Importing the AbstractCore integration raises an `ImportError` about the required AbstractCore version or missing
  optional dependencies.

Checks:

```bash
python -m pip show AbstractRuntime abstractcore
python -m pip install -U abstractruntime
```

Fix:
- Install or upgrade the base Runtime package. LLM/tools integration, common
  remote-light multimodal dependencies, and the MCP worker entry point are part
  of the base install.
- The current AbstractCore integration expects `abstractcore>=2.13.38`.

Verify:

```bash
python -c "import abstractruntime.integrations.abstractcore as ac; print(ac.__all__[:5])"
```

## A run is waiting and does not continue

Symptom:
- `Runtime.tick(...)` returns `status=waiting`, and the run stays paused.

Likely causes:
- The run is waiting for `ASK_USER`, `WAIT_EVENT`, passthrough tools, or tool approval.
- `WAIT_UNTIL` needs a driver loop, or a host must call `tick(...)` again after the due time.

Fix:
- For user/event/tool waits, resume with the exact `wait_key` from `state.waiting.wait_key`.
- For time-based waits, use `create_scheduled_runtime()` or another host driver that periodically ticks due runs.

Verify:

```python
state = rt.get_state(run_id)
print(state.status, state.waiting.wait_key if state.waiting else None)
```

## Generated media is missing or too large for run state

Symptom:
- Image, voice, music, or transcription outputs fail, or binary content is not present in `RunState.vars`.

Likely causes:
- Generated binary outputs require a runtime `ArtifactStore`.
- Runtime stores generated bytes by artifact reference instead of embedding raw bytes in checkpoints or ledger records.

Fix:
- Construct the runtime with an artifact store such as `InMemoryArtifactStore` or `FileArtifactStore`.
- Read `artifact_id` / `artifact_ref` from the durable result and load the artifact from the configured store.

Docs:
- `api.md#artifacts-store-by-reference`
- `integrations/abstractcore.md#multimodal-generation`

## Remote media calls use the wrong model or fail with input-media errors

Symptom:
- Remote image/TTS/STT calls do not use the expected media model.
- Remote image generation fails when media is supplied.
- Remote transcription rejects media that is not a local file or artifact-backed temporary file.

Fix:
- Put endpoint-specific routing in the `output` selector, not in the chat model unless you intend to route chat.
- Use `output.task="image_edit"` for image edits with one source image and optional mask.
- Use exactly one audio media item for remote STT, and make sure it resolves to a local path or artifact-backed file.

Docs:
- `integrations/abstractcore.md#multimodal-generation`

## Local media residency returns `model_residency_unsupported`

Symptom:
- A local `MODEL_RESIDENCY` load for `image_generation`, `image_upscale`, `video_generation`, `text_to_video`, `image_to_video`, `tts`, `stt`, or `music_generation` returns `ok=false` with
  `code="model_residency_unsupported"`.

Meaning:
- Runtime is reporting that the current local execution topology cannot truthfully keep that media backend resident for
  later reuse.

Fix:
- Use a configured long-lived AbstractCore server for media residency, then let Runtime relay `/acore/models/*`.
- Keep local media warmup optional (`required=false`) if it is only an optimization.
- Do not infer loaded state from model defaults, catalogs, downloaded weights, or provider names.

Docs:
- `integrations/abstractcore.md#prompt-cache-control-plane-and-durable-blocs`
- `faq.md#why-can-local-media-residency-return-okfalse-without-failing-the-run`

## Prompt-cache behavior is not reused for generated media

Symptom:
- A workflow-level prompt-cache flag creates session cache keys for text/chat calls, but not for image, voice, music, or
  transcription output selectors.

Meaning:
- Runtime only auto-derives session prompt-cache keys for text/chat calls. Non-text output selectors may still carry an
  explicit `prompt_cache_binding`, but Runtime does not invent one.

Fix:
- Use `params.prompt_cache_binding` for durable exact text/chat prefix reuse.
- Treat generated media calls as separate capability executions unless the selected AbstractCore backend documents a
  task-specific cache contract.

Docs:
- `integrations/abstractcore.md#prompt-cache-control-plane-and-durable-blocs`
- `faq.md#where-should-cached-session-or-prompt-cache-state-live`

## MCP worker command is not found

Symptom:
- `abstractruntime-mcp-worker` is not available on the command line.

Fix:

```bash
python -m pip install -U abstractruntime
abstractruntime-mcp-worker --help
```

Docs:
- `mcp-worker.md`

---

## docs/manual_testing.md

# Manual testing

This guide is a small set of **manual smoke tests** you can run to verify the durable runtime loop (start/tick/wait/resume), scheduler resumption, and persistence.

## Prerequisites

From the repo root:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install -e .
```

## Test 1: Zero-config hello world

```python
from abstractruntime import create_scheduled_runtime, StepPlan, WorkflowSpec


def greet(run, ctx):
    name = run.vars.get("name", "World")
    return StepPlan(node_id="greet", complete_output={"message": f"Hello, {name}!"})


workflow = WorkflowSpec(
    workflow_id="hello",
    entry_node="greet",
    nodes={"greet": greet},
)

sr = create_scheduled_runtime()
run_id, state = sr.run(workflow, vars={"name": "Alice"})

print(state.status.value)
print(state.output)

sr.stop()
```

Expected:
- status is `completed`
- output contains `{"message": "Hello, Alice!"}`

---

## Test 2: Ask user (pause + resume)

```python
from abstractruntime import create_scheduled_runtime, Effect, EffectType, StepPlan, WorkflowSpec, RunStatus


def ask_name(run, ctx):
    return StepPlan(
        node_id="ask",
        effect=Effect(
            type=EffectType.ASK_USER,
            payload={"prompt": "What is your name?"},
            result_key="user_input",
        ),
        next_node="greet",
    )


def greet(run, ctx):
    name = run.vars.get("user_input", {}).get("text", "Unknown")
    return StepPlan(node_id="greet", complete_output={"greeting": f"Hello, {name}!"})


workflow = WorkflowSpec(
    workflow_id="ask_and_greet",
    entry_node="ask",
    nodes={"ask": ask_name, "greet": greet},
)

sr = create_scheduled_runtime()
run_id, state = sr.run(workflow)
assert state.status == RunStatus.WAITING
print(state.waiting.prompt)

state = sr.respond(run_id, {"text": "Bob"})
assert state.status == RunStatus.COMPLETED
print(state.output)

sr.stop()
```

Expected:
- first run blocks with `status=waiting` and a `prompt`
- after `respond`, run completes with a greeting

---

## Test 3: Wait until (scheduler auto-resume)

```python
from datetime import datetime, timedelta, timezone
import time

from abstractruntime import create_scheduled_runtime, Effect, EffectType, StepPlan, WorkflowSpec, RunStatus


def schedule_task(run, ctx):
    until = (datetime.now(timezone.utc) + timedelta(seconds=2)).isoformat()
    return StepPlan(
        node_id="schedule",
        effect=Effect(type=EffectType.WAIT_UNTIL, payload={"until": until}),
        next_node="execute",
    )


def execute_task(run, ctx):
    return StepPlan(node_id="execute", complete_output={"ok": True})


workflow = WorkflowSpec(
    workflow_id="scheduled_task",
    entry_node="schedule",
    nodes={"schedule": schedule_task, "execute": execute_task},
)

sr = create_scheduled_runtime(poll_interval_s=0.2)
run_id, state = sr.run(workflow)
assert state.status == RunStatus.WAITING
print("waiting until:", state.waiting.until)

for _ in range(20):
    time.sleep(0.2)
    state = sr.get_state(run_id)
    if state.status == RunStatus.COMPLETED:
        break

print(state.status.value, state.output)
sr.stop()
```

Expected:
- run first blocks with `wait_reason=until`
- within a few seconds, scheduler resumes and the run completes

---

## Test 4: Persistence (survive restart)

```python
import tempfile
from pathlib import Path

from abstractruntime import (
    create_scheduled_runtime,
    Effect,
    EffectType,
    StepPlan,
    WorkflowSpec,
    JsonFileRunStore,
    JsonlLedgerStore,
    RunStatus,
)


def ask(run, ctx):
    return StepPlan(
        node_id="ask",
        effect=Effect(type=EffectType.ASK_USER, payload={"prompt": "Continue?"}, result_key="answer"),
        next_node="done",
    )


def done(run, ctx):
    answer = run.vars.get("answer") or {}
    text = answer.get("text") if isinstance(answer, dict) else None
    return StepPlan(node_id="done", complete_output={"answer": text})


workflow = WorkflowSpec(workflow_id="persistent_wf", entry_node="ask", nodes={"ask": ask, "done": done})
data_dir = Path(tempfile.mkdtemp())

# Session 1: start + block
sr1 = create_scheduled_runtime(run_store=JsonFileRunStore(data_dir), ledger_store=JsonlLedgerStore(data_dir))
run_id, state = sr1.run(workflow)
assert state.status == RunStatus.WAITING
sr1.stop()

# Session 2: “restart”, reload + resume
sr2 = create_scheduled_runtime(
    run_store=JsonFileRunStore(data_dir),
    ledger_store=JsonlLedgerStore(data_dir),
    workflows=[workflow],  # re-register
)
state = sr2.get_state(run_id)
assert state.status == RunStatus.WAITING
state = sr2.respond(run_id, {"text": "yes"})
assert state.status == RunStatus.COMPLETED
print(state.output)
sr2.stop()
```

Expected:
- run id remains valid after a restart
- ledger and checkpoint files exist under `data_dir`

---

## Test 5: Find waiting runs

```python
from abstractruntime import create_scheduled_runtime, Effect, EffectType, StepPlan, WorkflowSpec, WaitReason, RunStatus


def wait_for_event(run, ctx):
    return StepPlan(node_id="wait", effect=Effect(type=EffectType.WAIT_EVENT, payload={"wait_key": f"event_{run.run_id[:8]}"}))


workflow = WorkflowSpec(workflow_id="event_wf", entry_node="wait", nodes={"wait": wait_for_event})

sr = create_scheduled_runtime()
ids = [sr.run(workflow)[0] for _ in range(3)]

waiting = sr.find_waiting_runs()
waiting_events = sr.find_waiting_runs(wait_reason=WaitReason.EVENT)

print("waiting:", len(waiting), "events:", len(waiting_events))
assert all(r.status == RunStatus.WAITING for r in waiting_events)

sr.stop()
```

Expected:
- at least 3 waiting runs are listed
- filtering by `WaitReason.EVENT` works

## Run the automated tests

```bash
python -m pytest -q
```

Expected: the test suite passes.

Some integration tests depend on optional local services or packages (for example a configured Ollama model or `lancedb`). In lean development environments, run the focused unit tests for your change first, then run the full suite in a fully provisioned integration environment before release.

## See also

- `getting-started.md` — first steps
- `../examples/README.md` — runnable examples
- `architecture.md` — where these behaviors come from

---

## docs/limits.md

# Runtime limits (`_limits`)

AbstractRuntime stores runtime-facing limits in a canonical `RunState.vars["_limits"]` dict. This is used for **durable configuration** (persisted in checkpoints) and for **host/agent introspection**.

Implementation pointers:
- `_limits` helpers/namespace constants: `src/abstractruntime/core/vars.py`
- limit config source of truth: `src/abstractruntime/core/config.py` (`RuntimeConfig`)
- limit APIs: `src/abstractruntime/core/runtime.py` (`get_limit_status`, `check_limits`, `update_limits`)

## What `_limits` contains

`Runtime.start(...)` initializes `_limits` from `RuntimeConfig.to_limits_dict()` when missing. (`src/abstractruntime/core/runtime.py`, `src/abstractruntime/core/config.py`)

Shape (keys are stable; values may be `None` when unknown):

```python
run.vars["_limits"] = {
    "max_iterations": 50,
    "current_iteration": 0,
    "max_tokens": 32768,          # context window (fallback when unknown)
    "max_output_tokens": None,    # provider/model dependent
    "max_input_tokens": None,     # optional budget cap for inputs
    "estimated_tokens_used": 0,   # best-effort (see below)
    "max_history_messages": -1,   # -1 = unlimited
    "warn_iterations_pct": 80,
    "warn_tokens_pct": 80,
}
```

Notes (as implemented today):
- `current_iteration` is **not** automatically incremented by the runtime; higher-level loops (agents/workflows) should update it if they want iteration budgeting.
- `estimated_tokens_used` is a **best-effort, last-known** value. The runtime updates it from `LLM_CALL` usage metadata when available (`src/abstractruntime/core/runtime.py`). It is not guaranteed to be tokenizer-accurate and is not accumulated across calls.

## Configuring limits

You can pass a `RuntimeConfig` when constructing a `Runtime`:

```python
from abstractruntime.core import Runtime, RuntimeConfig
from abstractruntime.storage import InMemoryLedgerStore, InMemoryRunStore

rt = Runtime(
    run_store=InMemoryRunStore(),
    ledger_store=InMemoryLedgerStore(),
    config=RuntimeConfig(
        max_iterations=50,
        max_tokens=65536,
        warn_iterations_pct=75,
    ),
)
```

If you use the AbstractCore convenience factories, they also accept `config=` and may populate model capabilities (`src/abstractruntime/integrations/abstractcore/factory.py`).

## Introspection and updates

### `Runtime.get_limit_status(run_id)`

Returns a structured dict for UI/status display. (`src/abstractruntime/core/runtime.py`)

### `Runtime.check_limits(run_state)`

Returns a list of `LimitWarning` objects for limits approaching/exceeded. (`src/abstractruntime/core/models.py`, `src/abstractruntime/core/runtime.py`)

As of v0.4.9, warnings are computed for:
- `iterations` (`current_iteration` vs `max_iterations`)
- `tokens` (`estimated_tokens_used` vs `max_tokens`)

### `Runtime.update_limits(run_id, updates)`

Updates selected keys in `_limits` durably (saved via the configured `RunStore`). Unknown keys are ignored. (`src/abstractruntime/core/runtime.py`)

Example:

```python
rt.update_limits(run_id, {"max_tokens": 131072, "warn_tokens_pct": 85})
```

## See also

- `architecture.md` — where `_limits` fits in the runtime
- `integrations/abstractcore.md` — where token usage metadata typically comes from (`LLM_CALL`)
- `manual_testing.md` — quick manual checks + running tests

---

## docs/integrations/abstractcore.md

# AbstractCore integration

This integration wires AbstractRuntime effects to AbstractCore so workflows can execute:
- `EffectType.LLM_CALL`
- `EffectType.TOOL_CALLS`

Implementation pointers (this repo):
- factories: `src/abstractruntime/integrations/abstractcore/factory.py`
- effect handlers: `src/abstractruntime/integrations/abstractcore/effect_handlers.py`
- tool executors: `src/abstractruntime/integrations/abstractcore/tool_executor.py`
- default toolsets (incl. comms gating): `src/abstractruntime/integrations/abstractcore/default_tools.py`

## Install

```bash
pip install abstractruntime
```

The base install includes AbstractCore 2.13.38 or newer. That is the supported baseline for the current server auth split (`Authorization` for server auth, `X-AbstractCore-Provider-API-Key` for provider overrides), generated-media contracts, image upscaling, capability catalog, prompt-cache control-plane endpoints, durable bloc prompt-cache helpers, bindings and lifecycle operations, task-aware model residency for text/image/video/TTS/STT, current tool catalog, AbstractCore's public output-selector contract, async/sync text-generation output-selector parity, video generation endpoints, the public local vision-cache catalog helper used by Runtime discovery, and vision adapter discovery plus batch/LoRA media controls.

The base install also includes the remote-light media/capability plugins needed
for AbstractCore's multimodal `generate(..., output=...)` path. Local
image/video/voice/music generation still depends on configured AbstractCore
capability backends and hardware profiles:

```bash
pip install "abstractruntime[apple]"
pip install "abstractruntime[gpu]"
```

With `abstractmusic>=0.1.12`, the base music integration includes the lightweight remote ACE Music backend without local model-runtime extras. The MCP worker entrypoint is included in the base Runtime install.

## Execution modes

The factories implement three execution modes (ADR-0002):
- **Local**: in-process AbstractCore providers + local tool execution
- **Remote**: HTTP to an AbstractCore server (`/v1/chat/completions`) + tool passthrough
- **Hybrid**: remote LLM + local tool execution

Local mode currently uses `MultiLocalAbstractCoreLLMClient` as the built-in LLM router. Despite the name, it is not a
local+remote combo client: it routes among multiple in-process local `(provider, model)` clients and keeps them warm in
the current process. Remote model execution is a separate topology exposed through `create_remote_runtime(...)` and
`create_hybrid_runtime(...)`.

Factory functions (exported from `abstractruntime.integrations.abstractcore`):
- `create_local_runtime(...)`
- `create_remote_runtime(...)`
- `create_hybrid_runtime(...)`

Runtime stays explicit at the boundary: Gateway/hosts construct these clients with the Core server URL, Core server auth headers, provider/model defaults, retry policy, tool executor, and artifact store they intend to use. Runtime does not read `ABSTRACTGATEWAY_*` environment variables directly and does not reinterpret Gateway bearer tokens as Core server tokens or provider keys. Gateway-owned config should be consumed by Gateway, then passed to Runtime through explicit run state, effect payloads, constructor arguments, or Runtime-owned environment variables.

## Minimal LLM workflow

```python
from abstractruntime import Effect, EffectType, StepPlan, WorkflowSpec
from abstractruntime.integrations.abstractcore import create_local_runtime


def ask_model(run, ctx):
    return StepPlan(
        node_id="ask_model",
        effect=Effect(
            type=EffectType.LLM_CALL,
            payload={
                "prompt": "Answer in one sentence: what is durable workflow state?",
                "params": {"temperature": 0.0, "max_tokens": 128},
            },
            result_key="llm",
        ),
        next_node="done",
    )


def done(run, ctx):
    llm = run.vars.get("llm") or {}
    return StepPlan(node_id="done", complete_output={"answer": llm.get("content")})


workflow = WorkflowSpec(
    workflow_id="abstractcore_llm_demo",
    entry_node="ask_model",
    nodes={"ask_model": ask_model, "done": done},
)

rt = create_local_runtime(provider="ollama", model="qwen3:4b")
run_id = rt.start(workflow=workflow)
state = rt.tick(workflow=workflow, run_id=run_id)
print(state.output)
```

## `LLM_CALL` payload (recommended shape)

`Effect(type=EffectType.LLM_CALL, payload=...)`

```json
{
  "prompt": "...",
  "text": "optional text alias, useful for TTS",
  "messages": [{"role": "user", "content": "..."}],
  "system_prompt": "...",
  "media": ["path/or/artifact-ref"],
  "output": {"modality": "text|image|video|voice|music", "task": "optional"},
  "tools": [{"name": "...", "description": "...", "parameters": {...}}],
  "params": {
    "temperature": 0.0,
    "max_tokens": 256,
    "base_url": null
  }
}
```

Notes:
- Remote mode supports per-request dynamic routing by forwarding `params.base_url` to the AbstractCore server request body (`src/abstractruntime/integrations/abstractcore/llm_client.py`).
- Remote mode sends per-request provider key overrides from `params.api_key` / `params.provider_api_key` as `X-AbstractCore-Provider-API-Key` headers. Server/master auth should be supplied separately through the client's configured headers, usually `Authorization: Bearer <ABSTRACTCORE_SERVER_API_KEY>`.
- Local mode treats `base_url` and provider API keys as provider-construction concerns. `MultiLocalAbstractCoreLLMClient` can construct a per-call client when a host injects `params.base_url` plus `params.api_key` or `params.provider_api_key` (for example from a Gateway provider endpoint profile), then strips those fields before calling the provider.
- `media` accepts one item or a list. Durable artifact refs such as `{"$artifact": "...", "filename": "speech.wav"}` are materialized to temporary files for AbstractCore and never stored as raw bytes in `RunState`.
- `output` may be top-level or inside `params`; top-level `outputs` is accepted as a runtime alias for AbstractCore's `output`.
- `output.tags`, when present, are merged into the generated artifact metadata. Runtime metadata such as `run_id` and `tags` is used by AbstractRuntime's ArtifactStore boundary and is not forwarded as provider-specific generation kwargs.
- Host-supplied run defaults such as `run.vars["_runtime"]["provider"]` and `run.vars["_runtime"]["model"]` are persisted as JSON-safe routing metadata; provider clients, auth objects, downloaded model handles, and server sessions are not durable runtime state.

## Runtime grounding

AbstractRuntime records per-call grounding as structured response metadata under `metadata.runtime_grounding`. The current fields include local datetime, timezone when detectable, country, source, whether prompt injection occurred, and an optional user identity when supplied by trace metadata or local environment.

For text/chat LLM calls only, the same grounding is rendered into the current user turn as a tagged runtime envelope:

```text
<runtime_metadata>{"country":"FR","local_datetime":"2026-05-13T18:00:00+02:00"}</runtime_metadata>
hello
```

This makes time/location/user context visible to the LLM without mutating the durable human message into a natural-language prefix. If a model echoes the runtime-owned envelope, AbstractRuntime removes that envelope from user-facing response text while preserving `metadata.runtime_grounding` for audit.

Direct media requests, including image generation, TTS, and transcription, do not receive prompt-injected grounding. They still receive trace headers/tags for observability and artifact ownership, but TTS `input` and image prompts remain the literal text supplied by the workflow.

## Multimodal generation

AbstractRuntime forwards AbstractCore's unified `generate(..., output=...)` selector and normalizes multimodal responses into JSON-safe, artifact-backed results.

Generate an image:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "prompt": "A red ceramic mug on a white table.",
        "output": {"modality": "image", "format": "png", "width": 1024, "height": 1024},
    },
    result_key="image_result",
)
```

Generate speech:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "text": "Hello from AbstractRuntime.",
        "output": {"modality": "voice", "voice": "coral", "format": "wav"},
    },
    result_key="speech_result",
)
```

Generate music:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "text": "Warm lo-fi piano with brushed drums.",
        "output": {"modality": "music", "provider": "acemusic", "model": "ace-step", "format": "wav"},
    },
    result_key="music_result",
)
```

Generate video:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "prompt": "Glowing data streams converge into a geometric logo.",
        "output": {
            "modality": "video",
            "task": "text_to_video",
            "provider": "mlx-gen",
            "model": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
            "format": "mp4",
            "num_frames": 41,
            "fps": 24,
            "steps": 10,
        },
    },
    result_key="video_result",
)
```

Image to video:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "prompt": "Add a slow camera orbit.",
        "media": {"$artifact": "source_image_artifact_id", "type": "image", "role": "source"},
        "output": {
            "modality": "video",
            "task": "image_to_video",
            "provider": "mlx-gen",
            "model": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
            "format": "mp4",
        },
    },
    result_key="video_result",
)
```

Transcribe/analyze audio:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "media": {"$artifact": "audio_artifact_id", "filename": "speech.wav"},
        "output": "text",
    },
    result_key="transcript",
)
```

Generated binary media requires a runtime `ArtifactStore` and is stored there. The persisted result contains artifact references:

```json
{
  "outputs": {
    "image": [
      {
        "modality": "image",
        "task": "image_generation",
        "artifact_id": "...",
        "artifact_ref": {"$artifact": "...", "content_type": "image/png"}
      }
    ]
  }
}
```

Media-only normalized results now distinguish orchestration identity from the actual media backend:

- `runtime_provider` / `runtime_model`: the runtime-side orchestration identity, when relevant
- `media_provider` / `media_model`: the actual image/video/voice/music backend identity surfaced from the generated output

For local one-shot subprocess image generation, runtime metadata also records `execution_mode="local_one_shot_subprocess"`.

Long-running generated media may expose provider progress callbacks. Runtime injects a transient `on_progress` callback during `LLM_CALL` execution and persists each callback as an `EMIT_EVENT` ledger record named `abstract.progress`. The callback itself is never stored in the effect payload or run vars.

Remote runtimes support chat media by sending OpenAI-compatible data URL content arrays to AbstractCore Server. They also support image generation (`/v1/images/generations`), image edits (`/v1/images/edits` or `/{provider}/v1/images/edits`), image upscaling (`/v1/images/upscale` or `/{provider}/v1/images/upscale`), text-to-video (`/v1/videos/generations`), image-to-video (`/v1/videos/edits` or `/{provider}/v1/videos/edits`), TTS (`/v1/audio/speech`), music generation (`/v1/audio/music`), and STT (`/v1/audio/transcriptions`) with the same artifact-backed result shape. The Runtime/Core request surface now forwards task-specific media controls including `count`/`n`, `seeds`, ordered `lora_adapters`, and video `flow_shift`. Remote media endpoint calls do not inherit the chat model by default; pass an output-specific `model` only when you want a remote provider/model instead of the server's configured capability default. Remote STT requires exactly one audio media item that resolves to a local file path or artifact-backed temporary file. Remote image edits, image upscaling, and image-to-video require one source image media item resolving to a local path or artifact-backed temporary file. For voice clone/register or reference-guided TTS, use local execution so AbstractCore can use its in-process capability dispatcher. Runtime does not import `abstractmusic` directly; local music support comes through the configured AbstractCore capability stack.

Remote multimodal generation currently supports one `output` selector per `LLM_CALL`. Hybrid runtimes use the same remote LLM/media path as remote mode while executing tools locally. Local runtimes can use AbstractCore's in-process multimodal dispatcher for richer capability plugin behavior.

Local media residency is intentionally explicit when unsupported. `MODEL_RESIDENCY` results for local `image_generation`, `image_upscale`, `video_generation`, `text_to_video`, `image_to_video`, `tts`, `stt`, and `music_generation` return:

- `code="model_residency_unsupported"`
- `requires_long_lived_server=true`
- `config_hint` pointing to `ABSTRACTCORE_SERVER_BASE_URL`

Image/video-generation residency responses also include `execution_mode="local_one_shot_subprocess"` because local generated media can be isolated into one-shot workers unless a long-lived Core server owns the media backend.

When the workflow marks residency as optional (`required=false`), the effect still completes durably but includes `status_hint="warning"` and `degraded=true` so hosts can render the no-op honestly.

Remote auth example:

```python
from abstractruntime.integrations.abstractcore import create_remote_runtime

rt = create_remote_runtime(
    server_base_url="http://127.0.0.1:8000",
    model="openai/gpt-4o-mini",
    headers={"Authorization": "Bearer server-master-key"},
)
```

Then pass a per-request upstream provider key through `params.provider_api_key` only when the AbstractCore server is acting as a provider proxy for that request:

```python
payload = {
    "prompt": "Summarize this in one sentence.",
    "params": {
        "provider_api_key": "sk-provider-key",
        "base_url": "http://127.0.0.1:1234/v1",
    },
}
```

## `TOOL_CALLS` payload

```json
{
  "tool_calls": [
    {
      "name": "tool_name",
      "arguments": {"x": 1},
      "call_id": "optional (provider id)",
      "runtime_call_id": "optional (stable; runtime-generated)"
    }
  ],
  "allowed_tools": ["optional allowlist (order-insensitive)"]
}
```

Notes:
- `runtime_call_id` is generated/normalized by the runtime for durability (`src/abstractruntime/core/runtime.py`).
- In remote/passthrough mode, a host/worker boundary can use `runtime_call_id` as an idempotency key.

## Tool execution modes

Tool execution is controlled by the configured `ToolExecutor` (`src/abstractruntime/integrations/abstractcore/tool_executor.py`):

- **Executed (trusted local)**: use `MappingToolExecutor` (recommended) or `AbstractCoreToolExecutor`.
- **Passthrough (untrusted/server/edge)**: use `PassthroughToolExecutor`.
  - The `TOOL_CALLS` handler returns a durable `WAITING` run state.
  - The host executes the tool calls externally and resumes the run with results (`Runtime.resume(...)` / `Scheduler.resume_event(...)`).
- **Approval-gated local execution**: wrap a trusted executor with `ApprovalToolExecutor`.
  - Safe read-only/default bridge tools can run immediately.
  - Riskier or unknown tools return a durable `approval_required` wait.
  - A thin client can resume with `{"approved": true}` to execute the approved calls in-runtime, or `{"approved": false, "reason": "..."}` to return structured tool errors.

Approval example:

```python
from abstractruntime.integrations.abstractcore import (
    ApprovalToolExecutor,
    MappingToolExecutor,
    ToolApprovalPolicy,
    create_local_runtime,
)


def write_file(*, path: str, content: str):
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)
    return {"path": path, "bytes": len(content.encode("utf-8"))}


tools = ApprovalToolExecutor(
    delegate=MappingToolExecutor({"write_file": write_file}),
    policy=ToolApprovalPolicy(),
)
rt = create_local_runtime(provider="ollama", model="qwen3:4b", tool_executor=tools)
```

## Prompt-cache control plane and durable blocs

AbstractRuntime's AbstractCore integration now exposes a public host-control facade for prompt-cache, durable bloc/KV prompt-cache operations, and model-residency operations:

- `get_abstractcore_host_facade(runtime)`
- `AbstractCoreHostFacade`
- `get_prompt_cache_capabilities(...)`
- `get_prompt_cache_stats(...)`
- `prompt_cache_set(...)`
- `prompt_cache_update(...)`
- `prompt_cache_fork(...)`
- `prompt_cache_clear(...)`
- `prompt_cache_prepare_modules(...)`
- `list_prompt_cache_exports(...)`
- `prompt_cache_export(...)`
- `prompt_cache_import(...)`
- `upsert_text_bloc(...)`
- `get_bloc_record(...)`
- `list_blocs(...)`
- `get_bloc_kv_manifest(...)`
- `ensure_bloc_kv_artifact(...)`
- `load_bloc_kv_artifact(...)`
- `list_bloc_kv_artifacts(...)`
- `delete_bloc_kv_artifact(...)`
- `prune_bloc_kv_artifacts(...)`
- `delete_bloc(...)`
- `get_model_residency_capabilities(...)`
- `list_model_residency(...)`
- `load_model_residency(...)`
- `unload_model_residency(...)`

Behavior by execution mode:

- **Local** (`MultiLocalAbstractCoreLLMClient` / `LocalAbstractCoreLLMClient`): delegates to the in-process AbstractCore provider and normalizes responses into the same JSON-safe shape used by the endpoint.
- **Remote / Hybrid** (`RemoteAbstractCoreLLMClient`): proxies `/acore/prompt_cache/*` and `/acore/models/*` on the configured AbstractCore server.
  - When the remote target is the multi-provider AbstractCore server proxy rather than a direct AbstractEndpoint, callers can forward upstream `base_url` through these prompt-cache methods. Per-request provider key overrides supplied as `api_key` / `provider_api_key` are converted to `X-AbstractCore-Provider-API-Key` headers, not request bodies or query strings.
  - For durable bloc/KV methods, `base_url` takes precedence over local loaded-runtime selectors. Runtime omits `provider`, `model`, and `runtime_id` when `base_url` is supplied so Core takes the upstream endpoint branch cleanly.

Contract notes:

- Capability discovery is explicit: callers can branch on `capabilities.mode` (`none`, `keyed`, `local_control_plane`) and `supports_*` flags.
- Unsupported operations return structured payloads with `supported=false`, `operation`, `code`, and `capabilities`.
- When a provider reports `mode=local_control_plane` (for example MLX, or GGUF models whose llama.cpp chat format has an exact cached renderer), the runtime can maintain a compartmentalized `system | tools | history` cache path automatically.
- When a provider reports `mode=keyed`, the runtime still forwards stable `prompt_cache_key`s but skips module preparation/fork/update orchestration.
- This surface is intentionally host-oriented; the runtime effect handlers still only use prompt caching during LLM execution, but gateway/CLI hosts can now manage prompt caches and durable bloc/KV artifacts through the public facade instead of reaching through to provider internals.
- Automatic per-session prompt-cache keys are enabled by `run.vars["_runtime"]["prompt_cache"]`, `LLM_CALL.params.prompt_cache_key`, or the Runtime-owned `ABSTRACTRUNTIME_PROMPT_CACHE` process default. Gateway-specific prompt-cache env vars should be translated by Gateway into `_runtime.prompt_cache`.
- Durable exact reuse uses `LLM_CALL.params.prompt_cache_binding`. If a binding includes `key`, Runtime adopts it as the effective cache key, rejects mismatches before provider execution, and skips auto-derived session-key injection for that call.
- Automatic prompt-cache key derivation is text/chat-only. Non-text output selectors such as image, voice, music, and transcription may carry an explicit `prompt_cache_binding`, but Runtime does not derive a session cache key for them.
- Local Runtime owns the bloc store root policy:
  - default local root: `~/.abstractruntime/blocs`
  - default file-runtime root: `<base_dir>/blocs`
  - explicit `bloc_root_dir=...` overrides are allowed when hosts need a different root
- The three prompt-cache tracks are distinct:
  - session prompt cache: best-effort volatile reuse
  - durable bloc prompt cache: exact reuse through bloc/KV/binding
  - host-local prompt-cache export/import admin: optional operator tooling
    around live local provider cache state, separate from durable workflow
    memory

Host-side prompt-cache example:

```python
from abstractruntime.integrations.abstractcore import (
    create_local_runtime,
    get_abstractcore_host_facade,
)

rt = create_local_runtime(provider="mlx", model="mlx-community/Qwen3-4B-4bit")
facade = get_abstractcore_host_facade(rt)

caps = facade.get_prompt_cache_capabilities()
if caps.get("capabilities", {}).get("supports_prepare_modules"):
    facade.prompt_cache_prepare_modules(
        namespace="assistant",
        modules=[
            {"module_id": "system", "system_prompt": "You are concise."},
            {"module_id": "tools", "tools": [{"name": "read_file", "parameters": {"type": "object"}}]},
        ],
    )
```

Host-side durable bloc example:

```python
from abstractruntime.integrations.abstractcore import (
    create_local_file_runtime,
    get_abstractcore_host_facade,
)

rt = create_local_file_runtime(
    base_dir="./runtime-data",
    provider="mlx",
    model="mlx-community/Qwen3-4B-4bit",
)
facade = get_abstractcore_host_facade(rt)

record = facade.upsert_text_bloc(
    path="assistant/system.txt",
    content="Long-lived system prompt or memory text",
)
artifact = facade.ensure_bloc_kv_artifact(
    provider="mlx",
    model="mlx-community/Qwen3-4B-4bit",
    sha256=record["sha256"],
)
loaded = facade.load_bloc_kv_artifact(
    provider="mlx",
    model="mlx-community/Qwen3-4B-4bit",
    sha256=record["sha256"],
)

binding = loaded["artifact"]["prompt_cache_binding"]
```

Host-local prompt-cache export/import example:

```python
saved = facade.prompt_cache_export(
    name="orbit-cache",
    key="sess:orbit",
    q8=True,
)
listed = facade.list_prompt_cache_exports()
loaded_cache = facade.prompt_cache_import(
    name="orbit-cache",
    key="loaded:orbit",
    clear_existing=True,
)
```

Host-local export/import contract:

- This surface is **local-only**. Remote and hybrid runtimes return structured
  `prompt_cache_local_only` payloads instead of proxying host filesystem state
  through Core Server.
- Runtime owns the export root policy:
  - default local root: `~/.abstractruntime/prompt_cache_exports`
  - default file-runtime root: `<base_dir>/prompt_cache_exports`
  - explicit `prompt_cache_export_root_dir=...` overrides are allowed when a
    host needs a different local catalog root
- Exports stay partitioned by provider/model under that root, so the same
  logical export name can coexist safely across different local backends.
- This is a **secondary operator/admin feature**, not the primary durable app
  contract. For replay-safe exact reuse inside workflows, prefer
  `prompt_cache_binding` from durable bloc/KV artifacts instead.

Host-side durable bloc lifecycle example:

```python
records = facade.list_blocs()
artifacts = facade.list_bloc_kv_artifacts(bloc_id=record["record"]["bloc_id"])

# Preview a safe delete first.
preview = facade.delete_bloc_kv_artifact(
    bloc_id=record["record"]["bloc_id"],
    artifact_path=artifacts["artifacts"][0]["artifact_path"],
    dry_run=True,
)

# Remove one derived KV artifact but keep the durable text bloc.
facade.delete_bloc_kv_artifact(
    bloc_id=record["record"]["bloc_id"],
    artifact_path=artifacts["artifacts"][0]["artifact_path"],
    clear_loaded=True,
)

# Remove the whole bloc and all derived artifacts under it.
facade.delete_bloc(
    bloc_id=record["record"]["bloc_id"],
    clear_loaded=True,
)
```

Then use the binding in a normal runtime `LLM_CALL`:

```python
Effect(
    type=EffectType.LLM_CALL,
    payload={
        "prompt": "Use the durable cached prefix.",
        "params": {"prompt_cache_binding": binding},
    },
    result_key="llm",
)
```

### Storage semantics

- For **local** and **local-file** runtimes, `upsert_text_bloc(...)` persists one durable text snapshot under the Runtime-owned bloc root. Runtime chooses the root (`~/.abstractruntime/blocs` by default, or `<base_dir>/blocs` for `create_local_file_runtime(...)`), while AbstractCore's `FileBlocStore` defines the on-disk layout under that root.
- Within one bloc root, the durable source of truth is **content-addressed by SHA256**. Re-upserting the same text/file hash reuses or updates the same bloc record; it does not intentionally create several independent bloc copies under that same root.
- Deduplication is therefore **per bloc root**, not global across every Runtime instance. If several runtimes should share one durable bloc store, point them at the same `bloc_root_dir`. Separate roots intentionally isolate storage and can hold separate copies of the same text.
- The durable text bloc and the provider/model cache are different layers:
  - one bloc: durable extracted text plus metadata
  - zero or more derived KV artifacts: one per `(provider, model)` pair, stored under that bloc's `kv/` area
- Derived KV artifacts are **not portable** across providers or models. The same text bloc can legitimately have several provider/model-native artifacts, but each artifact remains tied to one provider/backend/model rendering path.
- `prompt_cache_binding` is a request-time proof that a specific runtime cache key still points at the exact loaded bloc artifact. It is not the durable text itself.
- For **remote** and **hybrid** runtimes using `base_url`, Runtime does not create its own local bloc copy; it proxies the bloc/KV operation to the configured AbstractCore server or upstream endpoint, and that remote side owns the store.

### Lifecycle operations

- Runtime now exposes public host methods for:
  - listing durable bloc records
  - listing provider/model KV artifacts under those blocs
  - deleting one derived KV artifact while keeping the bloc text
  - pruning matching KV artifacts by filter
  - deleting one durable bloc and, by default, its derived KV artifacts
- Safety behavior mirrors the public AbstractCore contract:
  - `dry_run=True` previews the delete/prune result without mutating storage
  - `clear_loaded=True` clears matching live prompt-cache keys before deletion when the relevant provider/model is resident in the current runtime or the remote Core server
  - `force=True` bypasses that safety check and should be treated as an explicit operator choice
- `delete_bloc_kv_artifact(...)` deletes exactly one artifact. If the selector matches several provider/model artifacts, Runtime returns a structured error rather than guessing.
- `delete_bloc(...)` removes the durable text bloc itself. By default it also removes derived KV artifacts under that bloc; pass `delete_kv=False` only if you intentionally want to leave those artifacts behind.

## Host-local comms and Telegram wrappers

Runtime also exposes the remaining Gateway-facing host/operator wrappers for
email and Telegram:

- `get_abstractcore_host_facade(runtime)` now includes:
  - `list_email_accounts(...)`
  - `list_emails(...)`
  - `read_email(...)`
  - `send_email(...)`
- `abstractruntime.integrations.abstractcore.comms_facade` also exposes:
  - `list_email_accounts(...)`
  - `list_emails(...)`
  - `read_email(...)`
  - `send_email(...)`
- `abstractruntime.integrations.abstractcore.telegram_facade` exposes:
  - `TelegramTdlibNotAvailable`
  - `bootstrap_telegram_auth_from_env(...)`
  - `get_global_telegram_client(start=False)`
  - `stop_global_telegram_client()`
  - `send_telegram_message(...)`

Contract notes:

- These are **host-local** wrappers over current public AbstractCore tool
  modules. They do not proxy through the remote AbstractCore server.
- The host facade email methods and the standalone `comms_facade` functions use
  the same Runtime-owned email wrapper layer; choose whichever is more natural
  for the host surface you are building.
- They are intentionally **nondurable**. They do not write Runtime run history
  on their own.
- Direct `send_email(...)` on the host facade and direct
  `telegram_facade.send_telegram_message(...)` are for operator-owned
  host-local flows only. If the outbound send belongs to a workflow/run, prefer
  the durable run facade methods shown below.
- Even for **remote** and **hybrid** runtimes, they still use the current host
  process env/config, local TDLib installation, and the host's own outbound
  network access.
- The Telegram global client is process-wide, not runtime-instance scoped.

Host-side operator example:

```python
from abstractruntime.integrations.abstractcore import (
    create_local_runtime,
    get_abstractcore_host_facade,
)
from abstractruntime.integrations.abstractcore.telegram_facade import (
    TelegramTdlibNotAvailable,
    bootstrap_telegram_auth_from_env,
    send_telegram_message,
)

rt = create_local_runtime(provider="ollama", model="qwen3:4b")
facade = get_abstractcore_host_facade(rt)

accounts = facade.list_email_accounts()
sent = facade.send_email(
    ["ops@example.com"],
    "Runtime status",
    body_text="All green.",
)

try:
    bootstrap = bootstrap_telegram_auth_from_env(timeout_s=30)
except TelegramTdlibNotAvailable:
    bootstrap = {"success": False, "error": "TDLib is not installed on this host."}

notify = send_telegram_message(chat_id=123456, text="Runtime check complete.")
```

## Discovery snapshots

AbstractRuntime's AbstractCore integration also exposes a public host discovery facade for snapshot/query reads:

- `get_abstractcore_discovery_facade(runtime)`
- `AbstractCoreDiscoveryFacade`
- `list_providers(...)`
- `list_provider_models(...)`
- `get_model_capabilities(...)`
- `get_voice_catalog(...)`
- `list_tts_models(...)`
- `list_stt_models(...)`
- `list_music_providers(...)`
- `list_music_models(...)`
- `list_vision_provider_models(...)`
- `list_cached_vision_models(...)`

Behavior by execution mode:

- **Local** (`MultiLocalAbstractCoreLLMClient` / `LocalAbstractCoreLLMClient`): uses public AbstractCore registries,
  capability facades, and local vision cache inspection to return JSON-safe snapshot payloads.
- **Remote / Hybrid** (`RemoteAbstractCoreLLMClient`): proxies `/providers`, `/v1/models`, `/v1/audio/*`, and
  `/v1/vision/*` on the configured AbstractCore server. Per-request provider key overrides supplied as `api_key` /
  `provider_api_key` become `X-AbstractCore-Provider-API-Key` headers.

`list_provider_models(provider, ...)` accepts the legacy `input_type` and
`output_type` filters plus Core's precise `capability_route` filter. Local mode
normalizes route filters before calling AbstractCore's provider registry; remote
mode forwards them to `/v1/models?capability_route=...`:

```python
models = facade.list_provider_models(
    "lmstudio",
    capability_route=["input.image", "output.text"],
)
embeddings = facade.list_provider_models("lmstudio", capability_route="embedding.text")
```

Contract notes:

- This surface is query-oriented. It does not create durable Runtime history on its own.
- Hosts should still ask Runtime for these reads instead of rebuilding Core catalog logic or importing Core server
  helpers directly.
- Model capability lookup is static metadata, not a live server probe. Replay should treat it as a recorded snapshot,
  not as a query to re-run.
- `list_cached_vision_models(...)` may still depend on the current local machine state. It is a Runtime-owned snapshot
  query, not durable run truth.
- Remote discovery methods accept `timeout_s=...` through facade kwargs. Local discovery remains synchronous helper
  code; async hosts should offload it to a worker thread if they do not want to block their event loop.

Host-side discovery example:

```python
from abstractruntime.integrations.abstractcore import (
    create_remote_runtime,
    get_abstractcore_discovery_facade,
)

rt = create_remote_runtime(
    server_base_url="http://127.0.0.1:8000",
    model="openai/gpt-4o-mini",
    headers={"Authorization": "Bearer server-master-key"},
)
facade = get_abstractcore_discovery_facade(rt)

providers = facade.list_providers(include_models=False)
voices = facade.get_voice_catalog(provider="openai", providers_only=True)
music = facade.list_music_providers(task="text_to_music")
vision = facade.list_vision_provider_models(task="text_to_image", providers_only=True)
upscalers = facade.list_vision_provider_models(task="image_upscale")
adapters = facade.list_vision_adapters(
    task="text_to_video",
    model="AbstractFramework/wan2.2-t2v-a14b-diffusers-8bit",
)
```

## Durable run-scoped media and comms execution

Hosts sometimes need to trigger image/TTS/music/STT work or outbound comms sends for an existing run. That work should still execute through Runtime so the child run ledger, artifact ownership, and replay surface remain Runtime-authored.

Public durable entry points:

- `get_abstractcore_run_facade(runtime)`
- `AbstractCoreRunFacade`
- `execute_llm_call(...)`
- `execute_tool_calls(...)`
- `resume_tool_calls(...)`
- `generate_image(...)`
- `edit_image(...)`
- `upscale_image(...)`
- `generate_video(...)`
- `image_to_video(...)`
- `generate_voice(...)`
- `generate_music(...)`
- `transcribe_audio(...)`
- `send_email(...)`
- `send_telegram_message(...)`

These helpers create child runs under an existing parent run and execute the real `LLM_CALL` or `TOOL_CALLS` through Runtime rather than doing external work in host/controller code.

Example:

```python
from abstractruntime.integrations.abstractcore import (
    create_local_runtime,
    get_abstractcore_run_facade,
)

rt = create_local_runtime(provider="mlx", model="qwen-chat")
facade = get_abstractcore_run_facade(rt)

child = facade.generate_image(
    "existing-parent-run-id",
    prompt="A red mug on a white table.",
    output={
        "provider": "mlx-gen",
        "model": "AbstractFramework/qwen-image-2512-8bit",
        "format": "png",
        "count": 2,
        "seeds": [101, 102],
        "lora_adapters": [
            {"id": "pixel-art", "scale": 0.7},
            {"id": "cool-grade", "scale": 0.2},
        ],
    },
)

assert child.status.value == "completed"
result = child.output["result"]
print(child.run_id, result["media_model"], result["outputs"]["image"][0]["artifact_id"])
```

Image upscaling uses the same durable child-run boundary and Core-owned `image_upscale` selector:

```python
child = facade.upscale_image(
    "existing-parent-run-id",
    media={"$artifact": "source-image-artifact-id", "type": "image"},
    output={
        "provider": "mlx-gen",
        "format": "png",
        "scale": 2,
        "resolution": 1024,
    },
)

print(child.run_id, child.output["result"]["outputs"]["image"][0]["artifact_id"])
```

For video, use the same child-run boundary:

```python
child = facade.generate_video(
    "existing-parent-run-id",
    prompt="Glowing data streams converge into a geometric logo.",
    output={
        "provider": "mlx-gen",
        "model": "AbstractFramework/wan2.2-t2v-a14b-diffusers-8bit",
        "format": "mp4",
        "num_frames": 41,
        "count": 2,
        "seeds": [401, 402],
        "flow_shift": 3.0,
        "lora_adapters": [{"id": "documentary-motion", "scale": 0.6}],
    },
)

print(child.run_id, child.output["result"]["outputs"]["video"][0]["artifact_id"])
```

Outbound comms sends that belong to a run should use the same durable child-run surface:

```python
email_child = facade.send_email(
    "existing-parent-run-id",
    to=["ops@example.com"],
    subject="Workflow alert",
    body_text="The workflow completed.",
)

telegram_child = facade.send_telegram_message(
    "existing-parent-run-id",
    chat_id=123456,
    text="Workflow completed.",
)
```

Contract notes for durable comms sends:

- Runtime records the send request and the send outcome in the child run ledger.
- Replay should show the recorded result; it should **not** resend the external email or Telegram message.
- Local and hybrid runtimes usually execute those sends immediately when the configured tool executor can run them.
- Remote runtimes may still enter a durable tool wait if the configured tool executor is passthrough/delegated or approval-gated. That wait/resume path is still Runtime-authored truth.
- To resume a waiting durable comms/tool child run through the same public boundary, use `get_abstractcore_run_facade(runtime).resume_tool_calls(child_run_id, payload=...)`.

## Attachment registration limits

When local `read_file` tool outputs are captured as session attachments, Runtime bounds the file bytes it stores. The limit is resolved in this order:

- `TOOL_CALLS.payload.max_attachment_bytes`
- `run.vars["_runtime"]["max_attachment_bytes"]`
- `ABSTRACTRUNTIME_MAX_ATTACHMENT_BYTES`
- the default of 25 MiB

Gateway-specific attachment env vars should be translated by Gateway into one of the explicit Runtime inputs above.

## Default toolsets (incl. comms)

`default_tools.get_default_toolsets()` provides a host-side convenience catalog of common tools:
- file/web/system tools
- optional comms tools behind env-var gating (`docs/tools-comms.md`)

This is useful when building a `MappingToolExecutor` quickly.

## See also

- `../architecture.md` — effect handler boundaries and durability invariants
- `../tools-comms.md` — enabling email/WhatsApp/Telegram tools
- `../adr/0002_execution_modes_local_remote_hybrid.md` — rationale for local/remote/hybrid

---

## docs/artifacts.md

# Runtime artifacts

AbstractRuntime stores large payloads as artifacts so run state, ledger records,
and workflow outputs remain JSON-safe. Artifacts are the durable record for
files, generated media, tool evidence, exported history bundles, and other
payloads that should be referenced by id rather than embedded inline.

The implementation lives in `src/abstractruntime/storage/artifacts.py`.

## File-like vocabulary boundary

Runtime artifacts are not the same thing as live filesystem paths:

- `Artifact`: a Runtime-owned durable payload safe to persist, search, reuse,
  and pass between runs by reference.
- `Workspace File` / `Workspace Folder`: a server-side path capability under
  Gateway/runtime workspace policy. These are path values, not durable payloads.
- `Local File` / `Local Folder`: a client-side intake source. In hosted/browser
  mode they should be uploaded and normalized into artifacts before durable
  execution.

Gateway/Flow product copy may say `Server File` / `Server Folder` when users
choose an origin, but Runtime stays anchored on artifact refs versus
workspace-scoped paths.

## Artifact identity

An artifact has a stable `artifact_id`, a `blob_id` for content deduplication
when the backend supports it, and a `run_id` scope. JSON state and Gateway APIs
pass artifacts with refs such as:

```json
{
  "$artifact": "a7050ebc5c8330...",
  "artifact_id": "a7050ebc5c8330...",
  "run_id": "9e19bd6a-ba07-4c2e-86c6-94ec7ca0a373",
  "content_type": "audio/wav",
  "size_bytes": 5293012
}
```

The ref is a pointer, not authorization. Hosts such as AbstractGateway are
responsible for deciding which principal may list or read an artifact.

## Stored metadata

Runtime stores two metadata layers:

- `tags`: string fields for compatibility and simple lookups.
- `metadata`: structured JSON for producer-specific details.
- `descriptor`: a Runtime-owned `ArtifactDescriptor` that normalizes the fields
  Gateway and Observer should use.

The descriptor separates display format from semantic meaning:

- `render_kind`: how the artifact should be rendered, such as `image`, `audio`,
  `video`, `markdown`, `html`, `json`, `text`, or `document`.
- `semantic_kind`: what the artifact represents, such as `voice`, `music`,
  `sound`, `transcript`, `evidence`, `workflow_snapshot`, or `image`.
- `classification_source`: whether the classification came from the producer,
  runtime tags, MIME inference, or legacy fallback.

The descriptor can also carry `session_id`, `workflow_id`, `node_id`, `turn_id`,
`ledger_cursor`, `producer`, `generation`, `media`, `source_refs`, `links`,
`security`, and action metadata. Producer metadata may be sparse; consumers
should show missing fields as unavailable rather than guessing.

## Generated media provenance

When generated media is produced through the Runtime AbstractCore integration
with output selectors, Runtime stores descriptor and metadata alongside the
bytes. Current generated outputs include image, video, voice/TTS, music, sound
or audio outputs, and transcription-style text outputs where the host route
stores a transcript artifact.

Generated-media descriptors record available producer facts:

- package/capability route, provider, model, backend, and runtime provider/model
  when they differ;
- prompt or TTS text, requested format, output index, negative prompt, and
  redacted generation parameters;
- source artifact refs for edit, image-to-video, cloned/reference voice, or
  other source-media flows when provided;
- measured media facts such as duration, sample rate, dimensions, channels, or
  frame counts when the store can inspect the bytes.

Runtime redacts obvious secret fields and bounds large metadata values. It does
not store raw provider requests as indexed descriptor fields.

Gateway or package producers that store artifacts outside the main Runtime
generated-media path should use `build_artifact_descriptor_payload(...)` from
`abstractruntime.storage.artifacts`. That helper applies the Runtime descriptor
schema, bounded secret-key redaction, and prompt/text sensitivity labels without
making Gateway invent a parallel descriptor contract.

## Catalog and search

`InMemoryArtifactStore` and `FileArtifactStore` support:

- `store(...)`, `load(...)`, and `get_metadata(...)`;
- `update_metadata(...)` for descriptor or structured metadata enrichment;
- `search(...)` for bounded pages;
- `count(...)`, `facet_counts(...)`, and `stats(...)` for exact totals and
  filter chips;
- `record_access(...)` for explicit metadata/content/preview/download/export
  counters.

`FileArtifactStore` maintains a repairable SQLite catalog for descriptor fields,
time filters, exact counts, byte totals, and facets. The catalog is an index of
Runtime-owned artifact metadata; the payload bytes and metadata files remain the
source of truth.

Plain `load(...)` and `get_metadata(...)` are side-effect free. UI and HTTP
layers that want access statistics must call `record_access(...)` or use Gateway
content routes that label the action.

## Gateway and Observer

AbstractGateway exposes Runtime artifacts through bounded HTTP APIs. Search
responses include `artifact_envelope_v1`, which projects Runtime descriptors,
media facts, access stats, and action links while preserving legacy row fields.
Gateway only forwards descriptor action links that are relative Gateway/UI
links; arbitrary external provider URLs should be represented as trace
availability or Gateway-owned trace records instead.

AbstractObserver renders Gateway envelopes. It should not infer canonical
artifact meaning from filenames or raw content except as a visible legacy
fallback. Use the Runtime tab for artifact inventory and the Observe tab for the
workflow/ledger narrative.

## Retrieval boundaries

Artifact search answers questions about stored files and media by metadata,
scope, type, time, producer, and links back to runs. It is not a semantic memory
search system.

- Use the ledger for "what happened in this workflow?"
- Use artifact search for "what files/media exist and how were they produced?"
- Use AbstractMemory/KG retrieval for "what knowledge or relationships were
  learned?"
- Use Gateway audit/provider links for system-level request traces when the
  envelope reports them.

## Limits

Legacy artifacts may only have MIME type and tags. Runtime projects them with
fallback descriptor fields so clients can still list and preview them, but
producer-level prompt/model/source provenance is only available for artifacts
created through descriptor-aware paths.

---

## docs/mcp-worker.md

# MCP worker (`abstractruntime-mcp-worker`)

AbstractRuntime ships an MCP worker that exposes AbstractCore toolsets over MCP (JSON-RPC) via:
- stdio (default)
- HTTP (optional)

Entry point:
- CLI script: `abstractruntime-mcp-worker` (`pyproject.toml`)
- implementation: `src/abstractruntime/integrations/abstractcore/mcp_worker.py`

## Install

```bash
pip install abstractruntime
```

## Run (stdio)

Choose toolsets explicitly (comma-separated):

```bash
abstractruntime-mcp-worker --toolsets files,web,system
```

Toolsets come from `get_default_toolsets()` (`src/abstractruntime/integrations/abstractcore/default_tools.py`). If comms tools are enabled, you can also expose `comms` (`docs/tools-comms.md`).

## Run (HTTP)

```bash
abstractruntime-mcp-worker --transport http --toolsets files,system --host 127.0.0.1 --port 8765
```

For anything beyond localhost, enable auth:

```bash
export ABSTRACT_WORKER_TOKEN="..."
abstractruntime-mcp-worker --transport http --toolsets files,system --http-require-auth
```

Optional origin allowlist (when clients send an `Origin` header):

```bash
abstractruntime-mcp-worker --transport http --toolsets files --http-allow-origin http://localhost:3000
```

## Security notes

- Exposing `system` tools can execute commands; treat the worker as privileged.
- Prefer stdio transport over an authenticated channel (e.g., SSH) when possible.

## See also

- `integrations/abstractcore.md` — tool executors and default toolsets

---

## docs/tools-comms.md

# Communication tools (`comms` toolset)

AbstractRuntime’s AbstractCore integration can expose an optional `comms` toolset (email, WhatsApp, Telegram). These tools are executed as **durable tool calls** via `EffectType.TOOL_CALLS`:
- tool requests/results are recorded in the **ledger** (`src/abstractruntime/core/models.py`)
- execution is controlled by the configured `ToolExecutor` (`src/abstractruntime/integrations/abstractcore/tool_executor.py`)

This document covers what is implemented in this repo: **toolset gating + wiring**. Provider credentials/config are defined by **AbstractCore tools**.

Implementation pointers (this repo):
- toolset gating: `src/abstractruntime/integrations/abstractcore/default_tools.py`
- tool execution: `src/abstractruntime/integrations/abstractcore/tool_executor.py`

## Enable (opt-in)

The `comms` toolset is disabled by default. Enable it via env vars (checked by `default_tools.comms_tools_enabled()`):

- `ABSTRACT_ENABLE_COMMS_TOOLS=1` (enable email + WhatsApp + Telegram)
- `ABSTRACT_ENABLE_EMAIL_TOOLS=1` (email only)
- `ABSTRACT_ENABLE_WHATSAPP_TOOLS=1` (WhatsApp only)
- `ABSTRACT_ENABLE_TELEGRAM_TOOLS=1` (Telegram only)

## Discover what gets enabled

```bash
python - <<'PY'
from abstractruntime.integrations.abstractcore.default_tools import list_default_tool_specs
comms = [s for s in list_default_tool_specs() if s.get("toolset") == "comms"]
print([s.get("name") for s in comms])
PY
```

## Wire into a runtime (local tool execution)

```python
import os

from abstractruntime.integrations.abstractcore import MappingToolExecutor, create_local_runtime
from abstractruntime.integrations.abstractcore.default_tools import get_default_tools

os.environ["ABSTRACT_ENABLE_COMMS_TOOLS"] = "1"

tool_executor = MappingToolExecutor.from_tools(get_default_tools())
rt = create_local_runtime(provider="ollama", model="qwen3:4b", tool_executor=tool_executor)
```

Notes:
- Install Runtime with `pip install abstractruntime`; AbstractCore tool integration and the MCP worker entry point are part of the base remote-light install.
- In untrusted deployments, prefer passthrough tools so a host/worker boundary approves and executes tool calls (`PassthroughToolExecutor` in `src/abstractruntime/integrations/abstractcore/tool_executor.py`).
- For local bridge-owned delivery flows, `ApprovalToolExecutor` can auto-run the Telegram send tools while requiring approval for email, WhatsApp, unknown tools, and write/command-style tools by default.
- Separate from the durable `TOOL_CALLS` path, Runtime also exposes **host wrappers** for operator-owned email and Telegram surfaces:
  - email helpers on `get_abstractcore_host_facade(runtime)` and `abstractruntime.integrations.abstractcore.comms_facade`
  - Telegram lifecycle/send wrappers in `abstractruntime.integrations.abstractcore.telegram_facade`
  - read/bootstrap helpers stay host-local and do not create run history by themselves
  - if an outbound send belongs to a run, prefer the durable run facade:
    `get_abstractcore_run_facade(runtime).send_email(...)` /
    `send_telegram_message(...)`
  - if that durable child run pauses for approval or passthrough execution, resume it via
    `get_abstractcore_run_facade(runtime).resume_tool_calls(...)`

## Credentials/config (provided by AbstractCore)

The actual comms tools live in AbstractCore:
- email + WhatsApp: `abstractcore.tools.comms_tools`
- Telegram: `abstractcore.tools.telegram_tools`

AbstractRuntime does **not** store secrets in run state. Secrets should be supplied as environment variables in the **process that executes the tool calls**.

Practical starting points (provided by AbstractCore; see `pyproject.toml` for the minimum supported version):
- Email:
  - `ABSTRACT_EMAIL_ACCOUNTS_CONFIG=/path/to/emails.yaml` (YAML/JSON config), or `ABSTRACT_EMAIL_{IMAP,SMTP}_*` env vars
  - Passwords are resolved indirectly via `*_PASSWORD_ENV_VAR` (default: `EMAIL_PASSWORD`)
  - Repo template (this repo): `emails.config.example.yaml` (static examples: OVH + Gmail)
- WhatsApp (Twilio):
  - defaults use `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN`
- Telegram:
  - transport selection via `ABSTRACT_TELEGRAM_TRANSPORT` (`tdlib` default, or `bot_api`)
  - bot token default env var: `ABSTRACT_TELEGRAM_BOT_TOKEN`

## Security and privacy notes

- Tool calls and results are durable: message bodies, recipients, and response metadata may be persisted in the ledger and/or checkpoint vars.
- Keep secrets out of tool arguments; prefer env-var resolution. Even when a tool accepts `*_env_var` parameters, those should be **names**, not secret values.
- Treat run storage and ledgers as sensitive when enabling comms tools.

## See also

- `integrations/abstractcore.md` — AbstractCore wiring (`LLM_CALL`, `TOOL_CALLS`)
- `provenance.md` — tamper-evident ledger

---

## docs/evidence.md

# Evidence capture

AbstractRuntime can record **provenance-first evidence** for selected “external boundary” tools (web + process execution). Evidence is stored durably as:
- a small index entry in `RunState.vars["_runtime"]["memory_spans"]`
- an artifact-backed payload (so checkpoints stay JSON-safe and bounded)

Implementation pointers:
- recorder: `src/abstractruntime/evidence/recorder.py`
- capture hook: `Runtime._maybe_record_tool_evidence(...)` (`src/abstractruntime/core/runtime.py`)
- retrieval helpers: `Runtime.list_evidence(...)` / `Runtime.load_evidence(...)` (`src/abstractruntime/core/runtime.py`)

## When evidence is recorded

Evidence capture runs best-effort after a successful `EffectType.TOOL_CALLS` step:
- only for tool names in `DEFAULT_EVIDENCE_TOOL_NAMES` (`web_search`, `fetch_url`, `execute_command`)
- only when an `ArtifactStore` is configured on the runtime (`Runtime(..., artifact_store=...)`)

If evidence capture fails, the runtime records a warning under `vars["_runtime"]["evidence_warnings"]` and continues execution.

## How to inspect evidence

```python
evidence = rt.list_evidence(run_id)
for e in evidence:
    print(e.get("tool_name"), e.get("created_at"), e.get("evidence_id"))

payload = rt.load_evidence(evidence_id="...")  # loads from ArtifactStore
```

## Storage and privacy

- Evidence payloads can include fetched page text or command stdout/stderr; treat artifacts and ledgers as sensitive.
- Secrets should never be passed as tool arguments (arguments are ledger-recorded). Prefer env-var resolution in tool implementations.

## See also

- `provenance.md` — tamper-evident ledger chain
- `architecture.md` — where evidence fits (runtime-owned, artifact-backed)

---

## docs/snapshots.md

# Snapshots (bookmarks)

A **snapshot** is a named, searchable checkpoint of a run state.

Motivation:
- debugging (“return to a known-good state”)
- observability (“inspect state at time T”)
- manual experimentation (“branch from snapshot later”)

Implementation: `src/abstractruntime/storage/snapshots.py`

## Data model

A snapshot stores:
- `snapshot_id`, `run_id`, optional `step_id`
- `name`, `description`, `tags`
- timestamps
- `run_state` (as a JSON dict)

## Stores

Included stores:
- `InMemorySnapshotStore` (tests/dev)
- `JsonSnapshotStore` (file-per-snapshot)

Search (MVP):
- filter by `run_id`
- filter by single `tag`
- substring match in `name` / `description`

## Restore semantics

Restoring a snapshot is a **host-level** operation:
1. load a snapshot from `SnapshotStore`
2. write `snapshot.run_state` back into your configured `RunStore`

Compatibility note:
- snapshot restore cannot guarantee safety if the workflow spec/node code has changed since the snapshot was taken.

## See also

- `architecture.md` — how snapshots fit with RunStore/LedgerStore/ArtifactStore

---

## docs/provenance.md

# Provenance (tamper-evident ledger)

AbstractRuntime’s ledger is an append-only journal of `StepRecord` entries. For audit/debug workflows, you can add **tamper-evidence** via a hash chain:
- each record carries `prev_hash` + `record_hash`
- modifications/reordering become detectable when you verify the chain

Implementation pointers:
- model fields: `src/abstractruntime/core/models.py` (`StepRecord.prev_hash`, `StepRecord.record_hash`, `StepRecord.signature`)
- hash-chain decorator + verifier: `src/abstractruntime/storage/ledger_chain.py`

## What is implemented (v0.4.9)

- `HashChainedLedgerStore(inner_store)` — wraps any `LedgerStore` to compute hashes on append
- `verify_ledger_chain(records)` — validates the chain and returns a verification report

Example:

```python
from abstractruntime import Runtime, WorkflowSpec
from abstractruntime.storage import InMemoryLedgerStore, InMemoryRunStore
from abstractruntime.storage.ledger_chain import HashChainedLedgerStore, verify_ledger_chain

ledger = HashChainedLedgerStore(InMemoryLedgerStore())
rt = Runtime(run_store=InMemoryRunStore(), ledger_store=ledger)

# ... run workflows ...

records = rt.get_ledger(run_id="...")  # list[dict]
report = verify_ledger_chain(records)
print(report.get("ok"), report.get("errors"))
```

## What is intentionally not implemented (yet)

- cryptographic signatures (non-forgeability)
- key management / delegation / revocation

Those belong in an optional extra (e.g., `abstractruntime[crypto]`) once the design is finalized.

## See also

- `architecture.md` — ledger as the source of truth
- `evidence` capture: `src/abstractruntime/evidence/recorder.py` (stores external-boundary evidence as artifacts + index)

---

## docs/workflow-bundles.md

# WorkflowBundles (`.flow`)

A **WorkflowBundle** is a portable distribution unit for VisualFlow JSON workflows:
- bundle format: zip file with `manifest.json`, `flows/*.json`, optional `assets/*`
- portability comes from shipping **VisualFlow JSON**, not `WorkflowSpec` (which contains Python callables)

Implementation pointers:
- manifest model: `src/abstractruntime/workflow_bundle/models.py`
- pack/unpack helpers: `src/abstractruntime/workflow_bundle/packer.py`, `src/abstractruntime/workflow_bundle/reader.py`
- on-disk registry: `src/abstractruntime/workflow_bundle/registry.py`
- compiler: `src/abstractruntime/visualflow_compiler/*`

The compiler also handles current VisualFlow authoring conveniences such as multi-entry execution fan-in. When a node has multiple incoming `exec-in` routes and per-route input overrides, the compiler lowers them into internal `join_exec` and `path_mux` nodes so the bundle remains portable and the runtime behavior stays explicit.

## Bundle layout

Minimal bundle:

```
manifest.json
flows/<flow_id>.json
```

Optional:

```
assets/<name>
```

## Packing a bundle

```python
from abstractruntime.workflow_bundle import pack_workflow_bundle

pack_workflow_bundle(
    root_flow_json="flows/root.json",
    out_path="out/my_bundle.flow",
    bundle_id="my_bundle",
    bundle_version="0.1.0",
)
```

`pack_workflow_bundle(...)` is stdlib-only and validates that referenced subflows exist in `flows_dir` (defaults to the root file’s directory).

## Reading a bundle

```python
from abstractruntime.workflow_bundle import open_workflow_bundle

b = open_workflow_bundle("out/my_bundle.flow")
print(b.manifest.bundle_id, b.manifest.bundle_version)
print([ep.flow_id for ep in b.manifest.entrypoints])
```

## Registry (installed bundles)

`WorkflowBundleRegistry` is a host-side convenience layer for storing and resolving `.flow` bundles from a directory:
- default directory resolution: `default_workflow_bundles_dir()` (`src/abstractruntime/workflow_bundle/registry.py`)
- resolve `bundle_id[@version]` and entrypoints (`resolve_bundle`, `resolve_entrypoint`)

Default directory resolution checks `ABSTRACTFRAMEWORK_WORKFLOWS_DIR`, then AbstractFlow authoring env names, then `./flows/bundles/`, then `~/.abstractframework/workflows/`. Hosts with Gateway-specific flow settings should pass `bundles_dir` explicitly or translate them to the shared framework env name before constructing the registry.

## VisualFlow multi-entry fan-in

Visual authoring tools may connect more than one execution edge into the same target `exec-in` pin. For example, a first prompt can enter a node from `on_flow_start`, while a later loop can re-enter the same node with a different prompt produced by the previous turn.

Store two metadata fields on the target node:
- `entryRoutes`: ordered execution entries. Each route has a stable `key`, `sourceNodeId`, and `sourceHandle`.
- `inputRouteOverrides`: per-input route overrides. Shape: `pinId -> routeKey -> {sourceNodeId, sourceHandle}`.

Minimal target-node fragment:

```json
{
  "id": "ask",
  "type": "ask_user",
  "data": {
    "pinDefaults": {"prompt": "start"},
    "entryRoutes": [
      {"key": "start::exec-out", "sourceNodeId": "start", "sourceHandle": "exec-out"},
      {"key": "ask::exec-out", "sourceNodeId": "ask", "sourceHandle": "exec-out"}
    ],
    "inputRouteOverrides": {
      "prompt": {
        "ask::exec-out": {"sourceNodeId": "ask", "sourceHandle": "response"}
      }
    }
  }
}
```

Compiler behavior:
- incoming exec edges are rerouted through an internal `join_exec` node
- overridden pins are routed through internal `path_mux` nodes
- the selected route is persisted in run state, so pause/resume and file-store restarts keep the same input selection
- stale metadata is rejected when `entryRoutes` no longer matches the incoming exec edges

Authoring guidance:
- use the default route key `${sourceNodeId}::${sourceHandle}` unless your editor needs a custom stable key
- keep route keys unique per target node
- use one normal data edge or `pinDefaults` for the fallback value, then add `inputRouteOverrides` only for routes that need a different value

## See also

- `architecture.md` — VisualFlow → WorkflowSpec compilation path

---

## CONTRIBUTING.md

# Contributing to AbstractRuntime

Thanks for your interest in contributing!

AbstractRuntime is a **durable workflow runtime** (interrupt → checkpoint → resume) with an append-only execution ledger.

## Quick start (dev setup)

Prereqs: **Python 3.10+**.

Recommended (workspace checkout): develop inside the [AbstractFramework](https://github.com/lpalbou/AbstractFramework) workspace.
The test bootstrap (`tests/conftest.py`) will auto-wire sibling projects on `sys.path` (e.g., `abstractcore/`, `abstractmemory/`, `abstractsemantics/`, `abstractflow/`).

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip

# Full dev install (runtime + docs/test tooling)
python -m pip install -e ".[test,docs]"

python -m pytest -q
```

If you cloned **only** this repo (without the AbstractFramework workspace), make sure the sibling packages above are importable (install them or clone them next to this repo) before running the full test suite.

## Repo map (source of truth)

- Public exports: `src/abstractruntime/__init__.py` (keep this consistent with `docs/api.md`)
- Core kernel (durable semantics): `src/abstractruntime/core/`
- Durability backends: `src/abstractruntime/storage/`
- Driver loop (in-process): `src/abstractruntime/scheduler/`
- Runtime integrations: `src/abstractruntime/integrations/`
- Tests: `tests/`

Docs entrypoints:
- `README.md` → `docs/getting-started.md`
- Docs index: `docs/README.md`
- Architecture: `docs/architecture.md`

## Change guidelines

### Code

- Preserve durability invariants: values stored in `RunState.vars` must stay JSON-serializable (`src/abstractruntime/core/models.py`).
- Add/adjust tests for new behavior (see `tests/`).
- If you touch effect semantics, update `docs/architecture.md` and ensure handlers and models stay aligned.

### Documentation

Docs should be **user-facing**, **actionable**, and anchored to code (prefer referencing `src/...` paths for claims).

When behavior changes, update:
- `docs/api.md` (public API surface + imports)
- `docs/getting-started.md` (onboarding examples)
- `docs/architecture.md` (semantics/invariants)
- `CHANGELOG.md` (user-visible changes)

## Releases

- Bump `version` in `pyproject.toml`
- Add a dated section to `CHANGELOG.md` (Keep a Changelog format)

---

## CODE_OF_CONDUCT.md

# Code of Conduct

## Our Standard

This project is maintained as a professional software collaboration. Contributors, maintainers, and users are expected
to keep discussions respectful, technically focused, and welcoming to people with different backgrounds and experience
levels.

Examples of expected behavior:

- Use clear, constructive language when giving feedback.
- Assume good faith while still asking for evidence and reproducible details.
- Keep disagreements focused on the code, docs, design, or release process.
- Respect privacy and do not publish private contact details, credentials, logs, or user data.

Examples of unacceptable behavior:

- Harassment, threats, insults, or discriminatory language.
- Sustained off-topic disruption of issues, pull requests, or discussions.
- Publishing private information without explicit permission.
- Pressuring maintainers or contributors to bypass safety, security, or release checks.

## Reporting

Report conduct concerns privately to the maintainer contact listed in the package metadata or through the repository
owner's GitHub profile. Include the relevant links, screenshots, or context when possible.

Maintainers may remove comments, close threads, block accounts, or restrict repository access when needed to protect the
project and its contributors.

---

## SECURITY.md

# Security Policy

## Reporting a vulnerability

Please report security issues **privately**.

Preferred channel:
- Use **GitHub Security Advisories** / the repository’s “Report a vulnerability” feature (private).

Include as much of the following as you can:
- affected versions (from `pyproject.toml` / `CHANGELOG.md`)
- impact and realistic attack scenario
- minimal reproduction steps or proof-of-concept
- environment details (OS, Python version, storage backend used)

## Coordinated disclosure

- Do not open public issues/PRs for security vulnerabilities.
- Avoid data exfiltration, service disruption, or destructive testing; keep verification to the minimum needed.

## Non-security bugs

If you are unsure whether an issue is security-related, prefer reporting it privately first.

---

## ACKNOWLEDGMENTS.md

# Acknowledgments

AbstractRuntime is designed to pair with the wider Abstract ecosystem and the open-source Python tooling community.

This project depends on (and is shaped by) the following libraries.
The canonical dependency list lives in `pyproject.toml`.

## Runtime dependencies (core install)

- **abstractsemantics** (`>=0.0.3`) — structured schema registry support (declared in `pyproject.toml`, used in `src/abstractruntime/integrations/abstractmemory/effect_handlers.py` and VisualFlow execution wiring).
- **AbstractMemory** (`>=0.2.6`) — TripleStore models and store contract used by Runtime's `MEMORY_KG_*` effects. Durable/vector backend dependencies such as LanceDB remain selected by hosts.

## Runtime integrations
- **abstractcore** (`>=2.13.31`) — LLM, tools, media, and capability integration used by the base `abstractruntime` install (declared in `pyproject.toml`, implementation under `src/abstractruntime/integrations/abstractcore/*`, docs: `docs/integrations/abstractcore.md`).
  - The AbstractCore integration uses **httpx** for remote mode (`src/abstractruntime/integrations/abstractcore/llm_client.py`) and **pydantic** for structured validation (`src/abstractruntime/integrations/abstractcore/effect_handlers.py`). These are provided by AbstractCore’s dependency set.
- **abstractcore[tools]** (`>=2.13.31`) — toolchain extra used by the base Runtime and `abstractruntime-mcp-worker` entry point (declared in `pyproject.toml`) and intended to include HTML parsing dependencies (see comments in `pyproject.toml`).
- **RestrictedPython** (optional) — used for sandboxed execution of VisualFlow “Code” nodes when available (`src/abstractruntime/visualflow_compiler/visual/code_executor.py`).

## Build & test tooling

- **hatchling** — build backend (`pyproject.toml` `[build-system]`).
- **pytest** — test runner (`pytest.ini`, `tests/`).

And thanks to everyone who reports bugs, discusses design tradeoffs, and contributes improvements.

See also: `LICENSE`, `CONTRIBUTING.md`.

---

## CHANGELOG.md

# Changelog

All notable changes to AbstractRuntime will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.29] - 2026-06-14

### Changed
- Raised the AbstractCore dependency floor to `abstractcore>=2.13.38`, so Runtime's base and hardware install profiles depend on the released Core utility surface and synchronized Voice-backed capability floor.

## [0.4.28] - 2026-06-06

### Added
- VisualFlow `read_pdf` and `write_pdf` document nodes. `read_pdf` extracts PDF text/metadata with `pypdf`; `write_pdf` renders text or Markdown-style content to real PDF bytes with `reportlab` while keeping run state JSON-safe.
- Runtime discovery now exposes installed compatible vision adapters through `list_vision_adapters(...)`.
- Runtime's local/remote image and video media helpers now preserve task-specific batch generation controls (`count` / `n`, `seeds`) and ordered `lora_adapters`, and VisualFlow media nodes now lower those fields for image generation, image edit, text-to-video, and image-to-video.

### Changed
- Runtime's base dependency path no longer selects AbstractCore's media extra or direct PyMuPDF/PyMuPDF4LLM/PyMuPDF-layout packages for VisualFlow PDF support.
- Raised the AbstractCore dependency floor to `abstractcore>=2.13.37`, matching the released Core/Vision adapter, batch-generation, and media-parameter contract used by Runtime's base and hardware profiles.
- Forwarded newer Core/Vision controls such as `guidance_2`, `flow_shift`, and image-upscaler parameters through generated-media execution.
- The per-turn `<runtime_metadata>` prompt envelope is now temporal-only by default (`local_datetime` plus a country-free `display`). Full grounding remains in result metadata (`runtime_grounding`); operators can opt fields back into the prompt with `ABSTRACTRUNTIME_GROUNDING_PROMPT_FIELDS` (comma-separated subset of `local_datetime,timezone,country,user,display`).
- When the runtime injects a `<runtime_metadata>` envelope into the user turn, it now also appends a stable "RUNTIME GROUNDING" contract to the system prompt explaining that the envelope is machine context, not a user language/locale preference.

### Fixed
- Markdown-to-PDF rendering now handles ATX heading levels 1 through 6, preventing deeper headings such as `####` from appearing as literal paragraph text in generated PDFs.
- VisualFlow LLM Call and Agent structured outputs now expose the parsed object on the `data` output while preserving the existing textual `response` output.
- VisualFlow LLM Call nodes now preserve inline `resp_schema` / `response_schema` constraints when provider and model are left on Auto, so Gateway/Core default routing still receives a structured-output `response_model`.
- VisualFlow `answer_user` lowering now always emits a string `message` payload, preventing connected-but-null message inputs from creating invalid `ANSWER_USER` effects.
- Structured-output field descriptions from JSON Schema now survive Runtime's Pydantic response-model conversion, so providers receive the same guidance authored in Flow.
- Local subprocess media execution now supports task-compatible image edit and image upscaling inputs under the same durable image/video contract as in-process Runtime media calls.
- Runtime now ships its own workspace-path and file-filter helper modules instead of importing unreleased AbstractCore internals, so published installs and release CI use the same supported dependency surface.

## [0.4.27] - 2026-06-03

### Changed
- Raised the AbstractCore dependency floor to `abstractcore>=2.13.32` so Runtime hosts inherit provider endpoint profiles, route-specific multimodal defaults, and updated audio-understanding model metadata.
- Updated AbstractCore discovery integration to expose Gateway/Core capability defaults, provider endpoint profiles, and route-specific media catalogs to thin clients.

### Fixed
- LLM and generated-media execution now resolves Gateway/Core default provider and model selections when VisualFlow nodes leave provider/model on Auto.
- Media artifact resolution and VisualFlow generated-media calls now preserve uploaded artifacts and progress callbacks across Runtime/AbstractCore boundaries.

## [0.4.26] - 2026-05-31

### Changed
- Moved AbstractCore remote/tool/media capability integration and the MCP worker dependency set into the base `pip install abstractruntime` profile. Runtime now exposes only the base, `abstractruntime[apple]`, and `abstractruntime[gpu]` user install profiles for functionality vs. local-inferencer selection; the `abstractcore`, `multimodal`, `mcp-worker`, `all-apple`, and `all-gpu` extras are no longer part of the supported install surface.
- Raised the AbstractCore dependency floor to `abstractcore>=2.13.31` so Runtime installs inherit the latest remote-light media and Wan A14B vision contracts.

### Fixed
- Local text-to-video and image-to-video media-only calls now run in an isolated subprocess, preserving progress callbacks while preventing native MLX/Metal video failures from killing the Gateway/Runtime parent process.

## [0.4.25] - 2026-05-29

### Added
- File-backed artifact stores now expose `content_path(...)` for hosts that need a stable local path while in-memory stores continue to return `None`.

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.30` (and matching `multimodal`, `mcp-worker`, and hardware-profile cascade extras), aligning Runtime with the latest Core media/plugin floors and image-to-image residency truth.

### Fixed
- Artifact-backed media resolution now preserves image roles such as `source` and `mask`, keeping image-edit and image-to-video requests wired correctly through AbstractCore.
- Model-residency discovery now treats `image_to_image` as its own vision task and deduplicates shared loaded-model records when task is omitted.

## [0.4.24] - 2026-05-26

### Added
- Runtime now surfaces AbstractCore/AbstractVision video generation through the existing generated-media boundary:
  - `LLM_CALL` output selectors for `{"modality":"video","task":"text_to_video"}` and `{"modality":"video","task":"image_to_video"}`
  - remote Core Server routing for `/v1/videos/generations` and `/v1/videos/edits`
  - durable run-facade helpers `generate_video(...)` and `image_to_video(...)`
  - VisualFlow node lowering for `generate_video` / `text_to_video` and `image_to_video`
- Provider progress callbacks are converted into JSON-safe `abstract.progress` ledger events during `LLM_CALL` execution without persisting Python callback objects.

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.29` (and matching `multimodal`, `mcp-worker`, and hardware-profile cascade extras), aligning Runtime with Core video endpoints, video residency tasks, and AbstractVision 0.3.16 progress-capable generation.
- Runtime docs and AI-readable `llms.txt` / `llms-full.txt` now document text-to-video, image-to-video, and generated-media progress events.

## [0.4.23] - 2026-05-26

### Added
- Run lifecycle helpers and execution metric surfaces for VisualFlow execution and Gateway run retention workflows.
- Storage deletion primitives for durable run cleanup across the Runtime storage backends.

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.28` (and matching `multimodal`, `mcp-worker`, and hardware-profile cascade extras), aligning Runtime with the latest Core capability defaults, MLX-Gen catalog, and OmniVoice discovery contracts.

### Fixed
- Effect invocation tracing now records generated-media and code-node execution details consistently across local and Gateway-hosted runs.

## [0.4.22] - 2026-05-23

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.27` (and matching `multimodal`, `mcp-worker`, and hardware-profile cascade extras), aligning Runtime with the latest Core capability plugin floors and server contracts.

### Fixed
- Remote and VisualFlow music generation now fail closed on legacy `backend` / `music_backend` selectors and require `provider` / `music_provider` as the backend selector, matching AbstractCore Server `/v1/audio/music` validation.
- VisualFlow `generate_music` lowering now preserves boolean `structure_prompt` values (including explicit `False`) in the pending output selector, keeping the Flow/Gateway/Core contract consistent.

## [0.4.21] - 2026-05-22

### Added
- Public model-residency capability discovery on the AbstractCore host facade so hosts can branch on task support before showing warmup controls.
- Durable run-facade support for image edits through `edit_image(...)`.
- First-class VisualFlow lowering for `edit_image` / `image_to_image` and `generate_music` media nodes.
- A focused troubleshooting guide and repository code of conduct in the core documentation set.

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.25`, matching the released Core validation for task-aware text/image/TTS/STT residency.
- Remote Runtime media execution now routes image edits through AbstractCore Server `/v1/images/edits` or provider-scoped `/{provider}/v1/images/edits`.
- Runtime no longer auto-derives session prompt-cache keys for non-text generated-media or transcription output selectors; explicit `prompt_cache_binding` remains supported.
- Local and remote model-residency responses now fail closed unless Core-owned residency truth verifies the loaded state.
- Runtime docs, backlog, ADR links, and AI-readable `llms.txt` / `llms-full.txt` now reflect the Core-owned residency boundary and current media node support.

### Fixed
- Artifact-backed media resolution now preserves image/audio role metadata without failing when content type metadata is absent.

## [0.4.20] - 2026-05-21

### Added
- Runtime now exposes a public host-local prompt-cache export/import admin surface on `get_abstractcore_host_facade(...)`:
  - `list_prompt_cache_exports(...)`
  - `prompt_cache_export(...)`
  - `prompt_cache_import(...)`

### Changed
- Local Runtime now owns the prompt-cache export root/catalog policy:
  - `~/.abstractruntime/prompt_cache_exports` by default
  - `<base_dir>/prompt_cache_exports` for `create_local_file_runtime(...)`
  - exact provider/model partitioning with Runtime-managed metadata sidecars
- Remote and hybrid runtimes now fail honestly for prompt-cache export/import admin with a structured local-only response instead of implying server-side support.
- Runtime docs and AI-readable `llms.txt` / `llms-full.txt` now document the secondary host-local export/import contract distinctly from the primary durable bloc/binding prompt-cache path.

## [0.4.19] - 2026-05-21

### Added
- Runtime now ships the missed standalone email comms wrapper/export layer for host-local operator surfaces:
  - `abstractruntime.integrations.abstractcore.comms_facade`
  - package-level email helper exports from `abstractruntime.integrations.abstractcore`

### Changed
- Host-facade email helpers now delegate through Runtime's own comms facade instead of importing `abstractcore.tools.comms_tools` directly in the facade method body.
- Runtime docs and AI-readable `llms.txt` / `llms-full.txt` now describe the standalone email comms facade/export layer alongside the existing host facade, Telegram wrappers, and durable run-owned comms sends.

## [0.4.18] - 2026-05-21

### Added
- Runtime now exposes the remaining Gateway-facing comms/Telegram package boundary through public Runtime wrappers:
  - host-local email helpers on `get_abstractcore_host_facade(...)`
  - host-local Telegram TDLib/bootstrap/global-client wrappers in `abstractruntime.integrations.abstractcore.telegram_facade`
- Outbound email and Telegram sends can now execute as durable Runtime-authored child runs through `get_abstractcore_run_facade(...)`:
  - `send_email(...)`
  - `send_telegram_message(...)`
  - `resume_tool_calls(...)` for approval-gated or passthrough tool waits

### Changed
- Runtime docs and AI-readable `llms.txt` / `llms-full.txt` now distinguish clearly between:
  - host-local operator comms helpers
  - durable run-owned outbound comms execution and replay semantics
- Outbound comms replay now follows the Runtime-owned truth model: recorded send requests and outcomes are replayed as data, not re-executed as external sends.

## [0.4.17] - 2026-05-21

### Added
- Runtime now surfaces AbstractCore-backed music through the same durable boundary as other generated artifacts:
  - host discovery snapshot methods for music providers/models
  - durable run-scoped `generate_music(...)`
  - artifact-backed normalized music outputs for local and remote Runtime paths

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.24`.
- The `multimodal` extra now installs `abstractcore[remote,vision,voice,audio,music]>=2.13.24`, which includes AbstractMusic's lightweight remote ACE backend path via `abstractmusic>=0.1.4`.
- Runtime docs and AI-readable `llms.txt` / `llms-full.txt` now describe the shipped music boundary and the current `0030` Gateway cleanup scope more accurately.

## [0.4.16] - 2026-05-21

### Added
- Public durable AbstractCore bloc lifecycle operations on `get_abstractcore_host_facade(...)` across local, remote, and hybrid runtimes:
  - `list_blocs(...)`
  - `list_bloc_kv_artifacts(...)`
  - `delete_bloc_kv_artifact(...)`
  - `prune_bloc_kv_artifacts(...)`
  - `delete_bloc(...)`

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.23`.
- Runtime's public docs and AI-readable `llms.txt` / `llms-full.txt` now describe the shipped durable bloc prompt-cache lifecycle boundary, including per-model KV artifacts and explicit cleanup controls.

## [0.4.15] - 2026-05-20

### Added
- Public AbstractCore Runtime facades for:
  - discovery/catalog snapshot queries (`get_abstractcore_discovery_facade(...)`)
  - prompt-cache and model-residency host control operations (`get_abstractcore_host_facade(...)`)
  - durable run-scoped child-run execution for image, TTS, STT, and direct LLM calls (`get_abstractcore_run_facade(...)`)
- `EffectType.MODEL_RESIDENCY` with AbstractCore-backed load/list/unload handling and VisualFlow lowering support for runtime-owned model residency control.

### Changed
- Minimum optional AbstractCore dependency floor is now `abstractcore>=2.13.20`.
- Local cached-vision discovery now depends on AbstractCore's public `get_local_vision_cache_catalog()` helper instead of private server internals.
- Local media residency no-op/unsupported responses now complete truthfully with warning/degraded metadata, and media-only results distinguish runtime orchestration identity from the actual media backend identity.
- Documentation was refreshed for the current Runtime/Core boundary, including root docs, ADR/backlog references, and AI-readable `llms.txt` / `llms-full.txt`.

## [0.4.14] - 2026-05-19

### Fixed
- Runtime extras that pull AbstractCore provider/tool dependencies now declare current compatible OpenAI/httpx/anyio bounds directly, preventing Python 3.10 pip installs from backtracking through the full OpenAI 1.x history.

## [0.4.13] - 2026-05-19

### Fixed
- The `multimodal` extra now uses AbstractCore's current remote/vision/voice/audio abstraction extras and declares the media document dependencies directly, avoiding Core's older narrow media constraints when combined with `[all-apple]` or `[all-gpu]`.
- Apple/GPU Runtime profile extras now bound setuptools to a modern version compatible with Torch's `<82` constraint, preventing resolver backtracking into broken legacy setuptools releases.

## [0.4.12] - 2026-05-19

### Fixed
- Remote AbstractCore transcription now uses provider-scoped audio routes such as `/{provider}/v1/audio/transcriptions` when an STT provider is selected.
- VisualFlow generated media nodes now keep LLM `provider`/`model` routing separate from image, TTS, and STT provider/model pins.

### Changed
- Minimum AbstractCore optional dependency floor is now `abstractcore>=2.13.15`.

## [0.4.11] - 2026-05-13

### Fixed
- AbstractCore effect-handler media materialization now preserves artifact `content_type`, media `type`, artifact ids, and safe filename extensions instead of dropping artifact-backed media to bare paths. This keeps generated WAV artifacts valid for downstream transcription nodes.
- Added explicit MIME extension aliases for common generated media types such as `audio/wav`, `image/png`, and `video/mp4` so platform MIME table differences do not create extensionless temp files.

### Changed
- Minimum AbstractCore optional dependency floor is now `abstractcore>=2.13.14`.


## [0.4.10] - 2026-05-12

### Fixed
- Generated media VisualFlow nodes now keep media model selection in the output spec and reserve LLM `provider`/`model` routing for explicit `runtime_provider`/`runtime_model` overrides.
- Legacy `provider`/`model` pins on image, TTS, and STT media nodes remain accepted as media selector fallbacks for existing flows.

### Changed
- Minimum AbstractCore optional dependency floor is now `abstractcore>=2.13.13` so generated media and audio catalog contracts stay aligned.

## [0.4.9] - 2026-05-09

### Changed
- Added `AbstractMemory>=0.2.6` as a base dependency so Runtime's
  `MEMORY_KG_*` effect contract always has the AbstractMemory TripleStore
  models available.

### Notes
- Runtime still does not depend on `AbstractMemory[lancedb]`. Hosts such as
  AbstractGateway choose the durable/vector memory backend, path, embeddings,
  and readiness policy.

## [0.4.8] - 2026-05-08

### Changed
- Minimum `abstractcore` optional dependency increased to `>=2.13.12`, and the
  semantics floor increased to `abstractsemantics>=0.0.3`.
- Added explicit hardware-profile cascade extras:
  `abstractruntime[apple]`, `abstractruntime[gpu]`,
  `abstractruntime[all-apple]`, and `abstractruntime[all-gpu]`.

### Notes
- Runtime still owns durable execution, not local model engines. These extras
  delegate to the matching AbstractCore profile so Gateway and root aggregate
  installs can compose a single profile vocabulary.

## [0.4.7] - 2026-05-08

### Changed
- Minimum `abstractcore` optional dependency increased to `>=2.13.11` for the `abstractcore`, `multimodal`, and `mcp-worker` extras so Runtime aligns with the current Core server-auth, provider-key, generated-media, and capability-catalog contracts.
- AbstractCore integration imports now fail fast when a stale local AbstractCore install is older than the 2.13.11 Gateway/Core deployment baseline.
- Documentation now makes the Gateway handoff explicit: hosts choose Runtime plus the Core/capability/memory profile, pass Core server URLs/auth headers deliberately, and keep provider clients, auth objects, model handles, and sessions out of durable runtime state.
- Runtime no longer reads Gateway-owned environment variables directly. Prompt-cache defaults use explicit Runtime state or `ABSTRACTRUNTIME_PROMPT_CACHE`, read-file attachment registration limits use explicit Runtime state/payload values or `ABSTRACTRUNTIME_MAX_ATTACHMENT_BYTES`, and workflow bundle registries use shared/framework or explicit directories.

### Testing
- Added packaging boundary coverage proving Runtime exposes no fake hardware profile extras (`apple`, `gpu`, `all-apple`, `all-gpu`) and keeps the Core floors aligned.
- Added import-boundary coverage proving the runtime kernel and package root do not import optional Core/Vision/Voice/Memory/Music stacks.
- Added a remote client regression test proving Gateway auth/provider-key environment variables are not inherited as AbstractCore server auth or provider-key headers.
- Added regression tests proving Gateway env vars alone do not enable prompt-cache keys, shrink attachment registration limits, or select workflow bundle registry directories.

## [0.4.6] - 2026-05-07

### Changed
- Minimum `abstractcore` optional dependency increased to `>=2.13.10` so Runtime picks up AbstractCore's async/sync text-generation output-selector parity in addition to the public output-selector contract.

### Fixed
- AbstractCore output-selector imports now fail fast when an older local AbstractCore install exposes the helper module but does not include the 2.13.10 async parity fix.

## [0.4.5] - 2026-05-07

### Changed
- Minimum `abstractcore` optional dependency increased to `>=2.13.9` so Runtime can use AbstractCore's public output-selector contract instead of mirroring provider-private multimodal selector logic.
- Runtime's AbstractCore output-spec adapter now delegates selector detection, normalization, generated-media detection, non-chat dispatch detection, and runtime metadata stripping to `abstractcore.core.output_specs`.

### Fixed
- Explicit `voice_clone` output specs no longer require a Runtime `ArtifactStore` before dispatch because AbstractCore exposes them as generated resources rather than binary media outputs.

## [0.4.4] - 2026-05-07

### Added
- **AbstractCore multimodal generation integration**:
  - `LLM_CALL` now forwards AbstractCore's unified `generate(..., output=...)` selector for image generation, TTS/voice output, and audio transcription
  - generated binary outputs are normalized into JSON-safe runtime results with ArtifactStore-backed refs instead of inline bytes
  - local runtimes can use AbstractCore capability plugins such as AbstractVision and AbstractVoice through the same runtime effect shape
  - remote runtimes support AbstractCore Server image generation, speech, and transcription endpoints, plus OpenAI-compatible chat media content arrays
- **Multimodal packaging extra**:
  - new `abstractruntime[multimodal]` extra installs `abstractcore[media,openai,vision,voice,audio]>=2.13.8`
- **VisualFlow LLM media selectors**:
  - LLM nodes lowered from VisualFlow can request generated media through `output` / `outputs` from node config or input data

### Changed
- Minimum `abstractcore` optional dependency increased to `>=2.13.8` for the unified multimodal response types.
- `LLM_CALL` accepts top-level `text`, top-level `output`, and top-level `outputs` as a runtime alias for AbstractCore `output`.
- `LLM_CALL.media` accepts one media item or a list; artifact refs are materialized to provider-ready temporary files before model calls.
- Remote AbstractCore clients now preserve existing OpenAI-style content arrays when adding media attachments.
- Remote AbstractCore clients now resolve ArtifactStore-backed media refs for direct client use, matching the runtime effect-handler path.
- VisualFlow LLM pending-call lowering now carries `output` / `outputs` selectors into runtime LLM effects.
- VisualFlow LLM result syncing now projects generated media artifacts into node outputs such as `outputs`, `resources`, `artifact_ref`, `artifact_id`, and `meta.output_mode`.

### Fixed
- Runtime artifact metadata (`run_id`, tags, artifact ids) is kept out of AbstractCore provider/capability kwargs while still being applied to stored generated media artifacts.
- Generated binary media now fails closed without an ArtifactStore instead of embedding base64 bytes in durable state.
- Remote image/TTS/STT calls no longer reuse the chat model unless an output-specific media model is supplied.
- Remote media inputs now either convert to a provider-ready content item or fail before dispatch; unsupported remote image edits, voice reference inputs, and non-file STT inputs are rejected explicitly.
- Turn-grounding injection now preserves structured multimodal message content arrays instead of stringifying them.
- Session-scoped prompt-cache key derivation now uses the effective AbstractCore client provider/model identity when an `LLM_CALL` payload omits explicit provider/model overrides.

### Documentation
- Documented multimodal `LLM_CALL` payloads, artifact-backed response shape, remote endpoint coverage, cached-session/prompt-cache boundaries, and the `abstractruntime[multimodal]` extra in the AbstractCore integration guide, API reference, architecture guide, FAQ, README, getting-started guide, docs index, and AI-ready `llms*.txt` files.
- Added a planned workspace/media access policy item covering default workspace-only access, explicit user allow/deny paths, and a conscious full-machine access mode for long-running agency deployments.

### Testing
- Added focused coverage for multimodal response normalization, artifact-backed generated media, media-only transcription calls, remote image/TTS/STT endpoints, remote media guardrails, remote chat media content arrays, content-array prompt extraction, direct remote artifact-ref media resolution, text-alias routing, provider-request redaction, and runtime metadata/tag boundaries.
- Added coverage for effective prompt-cache key identity and VisualFlow LLM media selector/result projection.

## [0.4.3] - 2026-05-06

### Added
- **AbstractCore prompt-cache control plane**:
  - local, multi-local, and remote LLM clients expose `get_prompt_cache_capabilities`, `get_prompt_cache_stats`, `prompt_cache_set`, `prompt_cache_update`, `prompt_cache_fork`, `prompt_cache_clear`, and `prompt_cache_prepare_modules`
  - local clients can maintain compartmentalized `system | tools | history` prompt-cache modules when providers support `local_control_plane`
  - remote clients proxy `/acore/prompt_cache/*` endpoints for gateway/CLI hosts
- **Artifact-backed media for AbstractCore LLM calls**:
  - local and remote AbstractCore clients can resolve runtime artifact refs into provider-ready media inputs
  - AbstractCore runtime factories now pass the runtime artifact store into LLM clients
- **Durable tool approval execution**:
  - `ToolApprovalPolicy` and `ApprovalToolExecutor` support safe auto-approval, durable approval waits, and approved re-execution
  - runtime factories expose the configured tool executor for approval-style `TOOL_CALLS` resumes
- **VisualFlow multi-entry lowering**:
  - authoring graphs with multiple incoming `exec-in` routes can be lowered into internal `join_exec` and `path_mux` nodes
  - per-entry input overrides survive pause/resume and file-store restart scenarios

### Changed
- AbstractCore remote provider-key overrides now use `X-AbstractCore-Provider-API-Key` headers instead of body/query `api_key` fields rejected by current AbstractCore servers.
- AbstractCore LLM clients keep per-turn grounding out of stable system prompts, coalesce leading system messages, strip internal tool-activity system messages, and propagate trace metadata headers.
- AbstractCore runtime factories expose the underlying LLM client for host-side control-plane operations and continue to honor AbstractCore timeout/config defaults.
- Default runtime iteration budget increased from 25 to 50.
- Minimum AbstractCore optional dependency increased to `>=2.13.5` so the documented prompt-cache control plane, hardened server auth, provider-key header routing, Telegram tools, and current model/provider behavior are available by default.
- Documentation: align version references with `pyproject.toml` (0.4.3), document AbstractCore prompt-cache operations, update remote provider-key guidance, and add concrete VisualFlow multi-entry authoring metadata.
- CI/release automation now builds the package and docs on normal CI and exposes a manual-only guarded release path for PyPI, GitHub Releases, and the docs site.

### Fixed
- VisualFlow While nodes again route `condition=true` to Loop and `condition=false` to Done/parent/complete after the execution-handle tracking refactor.
- Tool approval resumes now execute approved calls in-runtime when configured, return structured tool errors when denied or unavailable, and append completion ledger records for ledger-only replay clients.
- JSONL ledger listing now recovers concatenated JSON records defensively.
- `TOOL_CALLS` now emits durable warnings for missing or duplicate tool call ids.
- Optional VisualFlow fixture tests now skip cleanly when assessment fixtures are absent.

### Testing
- Added focused coverage for prompt-cache module preparation/rebuilds, remote prompt-cache proxying, artifact-backed media, tool approval waits/resumes, JSONL ledger recovery, remote provider-key headers, VisualFlow multi-entry prompt overrides, direct effect re-entry, same-predecessor route handles, stale route metadata, join-only fan-in, and While routing regressions.

## [0.4.2] - 2026-02-08

### Changed
- **Dependencies**:
  - bump minimum `abstractcore` / `abstractcore[tools]` to `>=2.11.8` (`pyproject.toml`)
  - bump minimum `abstractsemantics` to `>=0.0.2` (`pyproject.toml`)

## [0.4.1] - 2026-02-04

### Added
- **Durable prompt metadata for EVENT waits**:
  - `WAIT_EVENT` effects may include optional `prompt`, `choices`, and `allow_free_text` fields.
  - The runtime persists these fields onto `WaitState` so hosts (including remote/thin clients) can render a durable ask+wait UX without relying on in-process callbacks.
- **Rendering utilities** (`abstractruntime.rendering`):
  - `stringify_json(...)` + `JsonStringifyMode` to render JSON/JSON-ish values into strings with `none|beautify|minified` modes.
  - `render_agent_trace_markdown(...)` to render runtime-owned `node_traces` scratchpads into a complete, review-friendly Markdown timeline.
- **Documentation refresh**:
  - clearer entrypoints: `README.md` → `docs/getting-started.md`
  - new reference docs: `docs/api.md`, `docs/faq.md`, `docs/architecture.md`
  - maintainer-facing orientation: `llms.txt`, `llms-full.txt`
  - new repo policies: `CONTRIBUTING.md`, `SECURITY.md`, `ACKNOWLEDGMENTS.md`

### Fixed
- Normalize AbstractCore tool specs for skim tools so `paths` is always an array parameter (improves JSON schema consistency for tool callers).

## [0.4.0] - 2025-01-06

### Added

- **Active Memory System** (`abstractruntime.memory.active_memory`): Complete MemAct agent memory module
  - Runtime-owned `ACTIVE_MEMORY_DELTA` effect for structured Active Memory updates (used by agents via `active_memory_delta` tool)
  - JSON-safe durable storage in `run.vars["_runtime"]["active_memory"]`
  - Memory modules: MY PERSONA, RELATIONSHIPS, MEMORY BLUEPRINTS, CURRENT TASKS, CURRENT CONTEXT, CRITICAL INSIGHTS, REFERENCES, HISTORY
  - Active Memory v9 format with natural-language markdown rendering (not YAML) to reduce syntax contamination
  - All components render into system prompt by default (prevents user-role pollution on native-tool providers)

- **MCP Worker** (`abstractruntime-mcp-worker`): Standalone stdio-based MCP server for AbstractRuntime tools
  - Exposes AbstractRuntime's default toolsets as MCP tools via stdio transport
  - Human-friendly logging to stderr with ANSI color support
  - Security: allowlist-based command execution safety (`TOOL_WAIT` effect for dangerous commands)
  - New optional dependency: `abstractruntime[mcp-worker]` (includes `abstractcore[tools]`)
  - Entry point: `abstractruntime-mcp-worker` CLI script

- **Evidence Capture System** (`abstractruntime.evidence.recorder`): Always-on provenance-first evidence recording
  - Automatically records evidence for external-boundary tools: `web_search`, `fetch_url`, `execute_command`
  - Evidence stored as artifact-backed records indexed as `kind="evidence"` in `RunState.vars["_runtime"]["memory_spans"]`
  - Runtime helpers: `Runtime.list_evidence(run_id)` and `Runtime.load_evidence(evidence_id)`
  - Keeps RunState JSON-safe by storing large payloads in ArtifactStore with refs

- **Ledger Subscriptions**: Real-time step append events via `Runtime.subscribe_ledger()`
  - `create_local_runtime`, `create_remote_runtime`, `create_hybrid_runtime` now wrap LedgerStore with `ObservableLedgerStore` by default
  - Hosts can receive real-time notifications when steps are appended to ledger

- **Durable Custom Events (Signals)**:
  - `EMIT_EVENT` effect to dispatch events and resume matching `WAIT_EVENT` runs
  - Extended `WAIT_EVENT` to accept `{scope, name}` payloads (runtime computes stable `wait_key`)
  - `Scheduler.emit_event(...)` host API for external event delivery (session-scoped by default)

- **Orchestrator-Owned Timeouts** (AbstractCore integration):
  - Default **LLM timeout**: 7200s per `LLM_CALL` (not per-workflow), enforced by `create_*_runtime` factories
  - Default **tool execution timeout**: 7200s per tool call (not per-workflow), enforced by ToolExecutor implementations

- **Tool Executor Enhancements** (`MappingToolExecutor`):
  - **Argument canonicalization**: Maps common parameter name variations (e.g., `file_path`/`filepath`/`path`) to canonical names
  - **Filename aliases**: Supports `target_file`, `file_path`, `filepath`, `path` as aliases for file operations
  - **Error output detection**: Detects structured error responses (`{"success": false, ...}`) from tools
  - **Argument sanitization**: Cleans and validates tool call arguments
  - **Timeout support**: Per-tool execution timeouts with configurable limits

- **Memory Query Enhancements** (`MEMORY_QUERY` effect):
  - Tag filters with **AND/OR** modes (`tags_mode=all|any`) and **multi-value** keys (`tags.person=["alice","bob"]`)
  - Metadata filters for **authors** (`created_by`) and **locations** (`location`, `tags.location`)
  - Span records now capture `created_by` for `conversation_span`, `active_memory_span`, `memory_note` when `actor_id` available
  - `MEMORY_NOTE` accepts optional `location` field
  - `MEMORY_NOTE` supports `keep_in_context=true` flag to immediately rehydrate stored note into `context.messages`

- **Package Dependencies**:
  - New optional dependency: `abstractruntime[abstractcore]` (enables `abstractruntime.integrations.abstractcore.*`)
  - New optional dependency: `abstractruntime[mcp-worker]` (includes `abstractcore[tools]>=2.6.8`)

### Changed

- **LLM Client Enhancements**:
  - Tool call parsing refactored for better robustness and error handling
  - Streaming support with timing metrics (TTFT, generation time)
  - Response normalization preserves JSON-safe `raw_response` for debugging
  - Always attaches exact provider request payload under `result.metadata._provider_request` for every `LLM_CALL` step

- **Runtime Core** (902 lines changed):
  - Enhanced resume handling for paused/cancelled runs
  - Improved subworkflow execution with async+wait support
  - Better observable ledger integration

### Fixed

- **Cancellation is Terminal**: `Runtime.tick()` now treats `RunStatus.CANCELLED` as terminal and will not progress cancelled runs
- **Control-Plane Safety**: `Runtime.tick()` stops without overwriting externally persisted pause/cancel state (used by AbstractFlow Web)
- **Atomic Run Checkpoints**: `JsonFileRunStore.save()` writes via temp file + atomic rename to prevent partial/corrupt JSON under concurrent writes
- **START_SUBWORKFLOW async+wait**: Support for `async=true` + `wait=true` to start child run without blocking parent tick, while keeping parent in durable SUBWORKFLOW wait
- **ArtifactStore Run-Scoped Addressing**: Artifact IDs namespaced to run when `run_id` provided (prevents cross-run collisions, preserves purge-by-run semantics)
- **AbstractCore Integration Imports**: `LocalAbstractCoreLLMClient` imports `create_llm` robustly in monorepo namespace-package layouts
- **Token Limit Metadata**: `_limits.max_output_tokens` falls back to model capabilities when not configured (runtime surfaces explicit per-step output budget)
- **Token-Cap Normalization Boundary**: Removed local `max_tokens → max_output_tokens` aliasing from AbstractRuntime's AbstractCore client (AbstractCore providers own this mapping)

### Testing

- **25 new/modified test files** covering:
  - Active Memory functionality
  - MCP worker (logging, security, stdio communication)
  - Evidence recorder
  - Memory query rich filters
  - Tool executor (canonicalization, filename aliases, timeouts, error detection)
  - LLM client tool call parsing
  - Runtime configuration and subworkflow handling
  - Packaging extras validation

### Statistics

- **33 commits** improving memory systems, MCP integration, evidence capture, and tool execution
- **45 files changed**: 5,788 insertions, 286 deletions
- **6,074 total lines changed** across the codebase
- **3 new modules**: `active_memory.py`, `evidence/recorder.py`, `mcp_worker.py`

## [0.2.0] - 2025-12-17

### Added

#### Core Runtime Features
- **Durable Workflow Execution**: Start/tick/resume semantics for long-running workflows that survive process restarts
- **WorkflowSpec**: Graph-based workflow definitions with node handlers keyed by ID
- **RunState**: Durable state management (`current_node`, `vars`, `waiting`, `status`)
- **Effect System**: Side-effect requests including `LLM_CALL`, `TOOL_CALLS`, `ASK_USER`, `WAIT_EVENT`, `WAIT_UNTIL`, `START_SUBWORKFLOW`
- **StepPlan**: Node execution plans that define effects and state transitions
- **Explicit Waiting States**: First-class support for pausing execution (`WaitReason`, `WaitState`)

#### Scheduler & Automation
- **Built-in Scheduler**: Zero-config background scheduler with polling thread for automatic run resumption
- **WorkflowRegistry**: Mapping from workflow_id to WorkflowSpec for dynamic workflow resolution
- **ScheduledRuntime**: High-level wrapper combining Runtime + Scheduler with simplified API
- **create_scheduled_runtime()**: Factory function for zero-config scheduler creation
- **Event Ingestion**: Support for external event delivery via `scheduler.resume_event()`
- **Scheduler Stats**: Built-in statistics tracking and callback support

#### Storage & Persistence
- **Append-only Ledger**: Execution journal with `StepRecord` entries for audit/debug/provenance
- **InMemoryRunStore**: In-memory run state storage for development and testing
- **InMemoryLedgerStore**: In-memory ledger storage for development and testing
- **JsonFileRunStore**: File-based persistent run state storage (one file per run)
- **JsonlLedgerStore**: JSONL-based persistent ledger storage
- **QueryableRunStore**: Interface for listing and filtering runs by status, workflow_id, actor_id, and time range
- **Artifacts System**: Storage for large payloads (documents, images, tool outputs) to avoid bloating checkpoints
  - `ArtifactStore` interface with in-memory and file-based implementations
  - `ArtifactRef` type for referencing stored artifacts
  - Helper functions: `artifact_ref()`, `is_artifact_ref()`, `get_artifact_id()`, `resolve_artifact()`, `compute_artifact_id()`

#### Snapshots & Bookmarks
- **Snapshot System**: Named, searchable checkpoints of run state for debugging and experimentation
- **SnapshotStore**: Storage interface for snapshots with metadata (name, description, tags, timestamps)
- **InMemorySnapshotStore**: In-memory snapshot storage for development
- **JsonSnapshotStore**: File-based snapshot storage (one file per snapshot)
- **Snapshot Search**: Filter by run_id, tag, or substring match in name/description

#### Provenance & Accountability
- **Hash-Chained Ledger**: Tamper-evident ledger with `prev_hash` and `record_hash` for each step
- **HashChainedLedgerStore**: Decorator for adding hash chain verification to any ledger store
- **verify_ledger_chain()**: Verification function that detects modifications or reordering of ledger records
- **Actor Identity**: `ActorFingerprint` for attribution of workflow execution to specific actors
- **actor_id tracking**: Support for actor_id in both RunState and StepRecord for accountability

#### AbstractCore Integration
- **LLM_CALL Effect Handler**: Execute LLM calls via AbstractCore providers
- **TOOL_CALLS Effect Handler**: Execute tool calls with support for multiple execution modes
- **Three Execution Modes**:
  - **Local**: In-process AbstractCore providers with local tool execution
  - **Remote**: HTTP to AbstractCore server (`/v1/chat/completions`) with tool passthrough
  - **Hybrid**: Remote LLM calls with local tool execution
- **Convenience Factories**: `create_local_runtime()`, `create_remote_runtime()`, `create_hybrid_runtime()`
- **Tool Execution Modes**:
  - Executed mode (trusted local) with results
  - Passthrough mode (untrusted/server) with waiting semantics
- **Layered Coupling**: AbstractCore integration as opt-in module to keep kernel dependency-light

#### Effect Policies & Reliability
- **EffectPolicy Protocol**: Configurable retry and idempotency policies for effects
- **DefaultEffectPolicy**: Default implementation with no retries
- **RetryPolicy**: Configurable retry behavior with max_attempts and backoff
- **NoRetryPolicy**: Explicit no-retry policy
- **compute_idempotency_key()**: Ledger-based deduplication to prevent duplicate side effects after crashes

#### Examples & Documentation
- **7 Runnable Examples**:
  - `01_hello_world.py`: Minimal workflow demonstration
  - `02_ask_user.py`: Pause/resume with user input
  - `03_wait_until.py`: Scheduled resumption with time-based waiting
  - `04_multi_step.py`: Branching workflow with conditional logic
  - `05_persistence.py`: File-based storage demonstration
  - `06_llm_integration.py`: AbstractCore LLM call integration
  - `07_react_agent.py`: Full ReAct agent implementation with tools
- **Comprehensive Documentation**:
  - Architecture Decision Records (ADRs) for key design choices
  - Integration guides for AbstractCore
  - Detailed documentation for snapshots and provenance
  - Limits and constraints documentation
  - ROADMAP with prioritized next steps

### Technical Details

#### Architecture
- **Layered Design**: Clear separation between kernel, storage, integrations, and identity
- **Dependency-Light Kernel**: Core runtime remains stable with minimal dependencies
- **Graph-Based Execution**: All workflows represented as state machines/graphs for visualization and composition
- **JSON-Serializable State**: All run state and vars must be JSON-serializable for persistence

#### Testing
- Run the test suite with `python -m pytest -q` (see `docs/manual_testing.md`).

#### Compatibility
- **Python 3.10+**: Supports Python 3.10, 3.11, 3.12, and 3.13

### Known Limitations

- Snapshot restore does not guarantee safety if workflow spec or node code has changed
- Subworkflow support (`START_SUBWORKFLOW`) is implemented but undergoing refinement
- Cryptographic signatures (non-forgeability) not yet implemented - current hash chain provides tamper-evidence only
- Remote tool worker service not yet implemented

### Design Decisions

- **Kernel stays dependency-light**: Enables portability, stability, and clear integration boundaries
- **AbstractCore integration is opt-in**: Layered coupling prevents kernel breakage when AbstractCore changes
- **Hash chain before signatures**: Provides immediate value without key management complexity
- **Built-in scheduler (not external)**: Zero-config UX for simple cases
- **Graph representation for all workflows**: Enables visualization, checkpointing, and composition

### Notes

AbstractRuntime is the durable execution substrate designed to pair with AbstractCore, AbstractAgent, and AbstractFlow. It enables workflows to interrupt, checkpoint, and resume across process restarts, making it suitable for long-running agent workflows that need to wait for user input, scheduled events, or external job completion.

## [0.0.1] - Initial Development

Initial development version with basic proof-of-concept features.

[Unreleased]: https://github.com/lpalbou/abstractruntime/compare/v0.4.29...HEAD
[0.4.29]: https://github.com/lpalbou/abstractruntime/compare/v0.4.28...v0.4.29
[0.4.28]: https://github.com/lpalbou/abstractruntime/compare/v0.4.27...v0.4.28
[0.4.27]: https://github.com/lpalbou/abstractruntime/compare/v0.4.26...v0.4.27
[0.4.26]: https://github.com/lpalbou/abstractruntime/compare/v0.4.25...v0.4.26
[0.4.25]: https://github.com/lpalbou/abstractruntime/compare/v0.4.24...v0.4.25
[0.4.24]: https://github.com/lpalbou/abstractruntime/compare/v0.4.23...v0.4.24
[0.4.23]: https://github.com/lpalbou/abstractruntime/compare/v0.4.22...v0.4.23
[0.4.22]: https://github.com/lpalbou/abstractruntime/compare/v0.4.21...v0.4.22
[0.4.21]: https://github.com/lpalbou/abstractruntime/compare/v0.4.20...v0.4.21
[0.4.20]: https://github.com/lpalbou/abstractruntime/compare/v0.4.19...v0.4.20
[0.4.19]: https://github.com/lpalbou/abstractruntime/compare/v0.4.18...v0.4.19
[0.4.18]: https://github.com/lpalbou/abstractruntime/compare/v0.4.17...v0.4.18
[0.4.17]: https://github.com/lpalbou/abstractruntime/compare/v0.4.16...v0.4.17
[0.4.16]: https://github.com/lpalbou/abstractruntime/compare/v0.4.15...v0.4.16
[0.4.15]: https://github.com/lpalbou/abstractruntime/compare/v0.4.14...v0.4.15
[0.4.14]: https://github.com/lpalbou/abstractruntime/compare/v0.4.13...v0.4.14
[0.4.13]: https://github.com/lpalbou/abstractruntime/compare/v0.4.12...v0.4.13
[0.4.12]: https://github.com/lpalbou/abstractruntime/compare/v0.4.11...v0.4.12
[0.4.11]: https://github.com/lpalbou/abstractruntime/compare/v0.4.10...v0.4.11
[0.4.10]: https://github.com/lpalbou/abstractruntime/compare/v0.4.9...v0.4.10
[0.4.9]: https://github.com/lpalbou/abstractruntime/compare/v0.4.8...v0.4.9
[0.4.8]: https://github.com/lpalbou/abstractruntime/compare/v0.4.7...v0.4.8
[0.4.7]: https://github.com/lpalbou/abstractruntime/compare/v0.4.6...v0.4.7
[0.4.6]: https://github.com/lpalbou/abstractruntime/compare/v0.4.5...v0.4.6
[0.4.5]: https://github.com/lpalbou/abstractruntime/compare/v0.4.4...v0.4.5
[0.4.4]: https://github.com/lpalbou/abstractruntime/releases/tag/v0.4.4
[0.4.3]: https://github.com/lpalbou/abstractruntime/releases/tag/v0.4.3
[0.4.2]: https://github.com/lpalbou/abstractruntime/releases/tag/v0.4.2
[0.4.1]: https://github.com/lpalbou/abstractruntime/releases/tag/v0.4.1
[0.4.0]: https://github.com/lpalbou/abstractruntime/releases/tag/v0.4.0
[0.0.1]: https://github.com/lpalbou/abstractruntime/releases/tag/v0.0.1

---

## ROADMAP.md

# AbstractRuntime Roadmap

## Current status (v0.4.2)

AbstractRuntime provides a durable workflow kernel plus optional integrations:
- durable execution: `Runtime.start/tick/resume`, explicit `WaitState` (`src/abstractruntime/core/runtime.py`)
- append-only ledger (`StepRecord`) + persistent stores (JSON/JSONL, SQLite) (`src/abstractruntime/storage/*`)
- built-in scheduler (`Scheduler`, `ScheduledRuntime`) (`src/abstractruntime/scheduler/*`)
- snapshots/bookmarks (`src/abstractruntime/storage/snapshots.py`)
- tamper-evident hash-chained ledger (`src/abstractruntime/storage/ledger_chain.py`)
- artifacts + offloading for large payloads (`src/abstractruntime/storage/artifacts.py`, `src/abstractruntime/storage/offloading.py`)
- retries/idempotency hooks (`src/abstractruntime/core/policy.py`)
- VisualFlow compiler + WorkflowBundles (`src/abstractruntime/visualflow_compiler/*`, `src/abstractruntime/workflow_bundle/*`)
- AbstractCore integration for `LLM_CALL` / `TOOL_CALLS` (`docs/integrations/abstractcore.md`)

## Near-term priorities

These are tracked in `docs/backlog/planned/`:

1. **Signatures and keys** — non-forgeable provenance (beyond tamper-evidence)
   `docs/backlog/planned/008_signatures_and_keys.md`

2. **Remote tool worker executor** — first-class worker boundary for tool execution
   `docs/backlog/planned/014_remote_tool_worker_executor.md`

3. **Limit warnings + observability events** — surface `_limits` warnings durably/streaming
   `docs/backlog/planned/017_limit_warnings_and_observability.md`

4. **Agent integration improvements** — reduce friction for external agent loops building on runtime
   `docs/backlog/planned/015_agent_integration_improvements.md`

## Longer-term (not scheduled)

- distributed scheduling primitives (beyond in-process polling)
- workflow versioning/migration patterns for long-lived runs and snapshot restore
- stronger reproducibility contracts for replays (workflow snapshotting + run history bundles)

---

## docs/adr/README.md

# Architectural Decision Records (ADRs)

ADRs document significant architectural decisions made during AbstractRuntime development. They explain *why* certain approaches were chosen, not *what* was built (that's in the backlog).

## Why ADRs Matter

When you ask "why is it designed this way?", the answer is in an ADR. ADRs are:
- **Immutable**: Once accepted, they are not edited (only superseded by new ADRs)
- **Historical**: They capture the context and constraints at decision time
- **Educational**: They help new contributors understand the architecture

## Index

| ID | Title | Status | Date | Summary |
|----|-------|--------|------|---------|
| 0001 | [Layered Coupling with AbstractCore](docs/adr/0001_layered_coupling_with_abstractcore.md) | Accepted | 2025-12-11 | Kernel stays dependency-light; AbstractCore integration is opt-in |
| 0002 | [Execution Modes](docs/adr/0002_execution_modes_local_remote_hybrid.md) | Accepted | 2025-12-11 | Support local, remote, and hybrid execution topologies |
| 0003 | [Provenance Hash Chain](docs/adr/0003_provenance_tamper_evident_hash_chain.md) | Accepted | 2025-12-11 | Tamper-evident ledger first; cryptographic signatures deferred |
| 0004 | [Runtime Owns Run-Scoped Media Execution Truth](docs/adr/0004_runtime_owns_run_scoped_media_execution_truth.md) | Accepted | 2026-05-20 | Hosts must route run-scoped media execution through Runtime |
| 0005 | [Runtime Owns AbstractCore Host Discovery Queries](docs/adr/0005_runtime_owns_abstractcore_host_discovery_queries.md) | Accepted | 2026-05-20 | Hosts should ask Runtime for Core discovery/catalog snapshots |
| 0006 | [Runtime Owns Durable AbstractCore Bloc Prompt-Cache Control](docs/adr/0006_runtime_owns_durable_abstractcore_bloc_prompt_cache.md) | Accepted | 2026-05-20 | Hosts should use Runtime for durable bloc/KV controls and binding-aware execution |
| 0007 | [Runtime Relays Core-Owned Model Residency Truth](docs/adr/0007_runtime_relays_core_owned_model_residency_truth.md) | Accepted | 2026-05-21 | Runtime reports loaded state only from AbstractCore residency truth |

## Relationship to Backlog

ADRs explain *why*. Backlog items explain *what* and *how*.

| ADR | Related Implementation |
|-----|------------------------|
| 0001 | `backlog/completed/005_abstractcore_integration.md` |
| 0002 | `backlog/completed/005_abstractcore_integration.md` |
| 0003 | `backlog/completed/007_provenance_hash_chain.md`, `backlog/planned/008_signatures_and_keys.md` |
| 0004 | `backlog/completed/023_truthful_local_media_residency_boundaries.md`, `backlog/completed/024_runtime_owned_run_scoped_media_execution.md` |
| 0005 | `backlog/completed/026_runtime_host_discovery_facade_for_core_catalogs.md` |
| 0006 | `backlog/completed/027_runtime_durable_bloc_prompt_cache_facade.md` |
| 0007 | `backlog/completed/0035_model_residency_provider_truth_for_local_http_clients.md`, `backlog/proposed/0036_local_media_residency_bridge_to_core_residency.md` |

## Adding New ADRs

When making a significant architectural decision:
1. Create `docs/adr/NNNN_short_title.md`
2. Use the template: Status, Context, Decision, Consequences
3. Set status to "Accepted" once the decision is final
4. If superseding an old ADR, update the old one's status to "Superseded by NNNN"

---

## docs/backlog/README.md

# Backlog (maintainers)

This README currently serves as the backlog overview for this repository.

This folder contains a structured backlog used during development. Items are grouped as:
- `completed/` — implemented work items (what shipped, with implementation pointers)
- `planned/` — committed future work items that still match current runtime reality
- `proposed/` — uncommitted ideas and follow-on risks worth preserving
- `deprecated/` — historical backlog notes (superseded)

If you are new to the project, start with `../README.md` and `../architecture.md` instead.

## Counts

- Planned: 5
- Proposed: 3
- Completed: 33
- Deprecated: 13
- Recurrent: 0

## Next recommended work

1. `planned/018_workspace_access_policy_for_media_and_tools.md`
   Keep workspace and tool policy explicit while Gateway extracts its local
   workspace helpers.
2. `planned/014_remote_tool_worker_executor.md`
   The public ToolExecutor path is still the larger follow-on after the current
   Gateway boundary cleanup.
3. `proposed/0036_local_media_residency_bridge_to_core_residency.md`
   Local Runtime media residency should relay live Core-owned truth for image/TTS/STT or stay unsupported when no
   Core-owned local media residency facade is wired.
4. `proposed/0031_runtime_tool_spec_adapters_for_gateway_and_mcp.md`
   The comms/Telegram boundary cleanup is now complete; the remaining lower
   pressure follow-up is a Runtime-owned tool-spec surface if Gateway or the
   MCP worker still need one after adoption.
5. `proposed/0038_core_server_pool_residency_affinity.md`
   Future-only topology item for multiple AbstractCore servers, server identity, and residency routing affinity.

## Completed

| ID | Item |
|----|------|
| 001 | `completed/001_runtime_kernel.md` |
| 002 | `completed/002_persistence_and_ledger.md` |
| 003 | `completed/003_wait_primitives.md` |
| 004 | `completed/004_scheduler_driver.md` |
| 005 | `completed/005_abstractcore_integration.md` |
| 006 | `completed/006_snapshots_bookmarks.md` |
| 007 | `completed/007_provenance_hash_chain.md` |
| 009 | `completed/009_artifact_store.md` |
| 010 | `completed/010_examples_and_composition.md` |
| 011 | `completed/011_subworkflow_support.md` |
| 012 | `completed/012_run_store_query_and_scheduler_support.md` |
| 013 | `completed/013_effect_retries_and_idempotency.md` |
| 016 | `completed/016_runtime_aware_parameters.md` |
| 019 | `completed/019_runtime_host_facade_for_core_operator_surfaces.md` |
| 020 | `completed/020_runtime_gateway_install_boundary.md` |
| 021 | `completed/021_runtime_gateway_env_namespace_cleanup.md` |
| 022 | `completed/022_model_residency_control_plane.md` |
| 023 | `completed/023_truthful_local_media_residency_boundaries.md` |
| 024 | `completed/024_runtime_owned_run_scoped_media_execution.md` |
| 026 | `completed/026_runtime_host_discovery_facade_for_core_catalogs.md` |
| 027 | `completed/027_runtime_durable_bloc_prompt_cache_facade.md` |
| 028 | `completed/028_runtime_bloc_kv_lifecycle_and_pruning.md` |
| 029 | `completed/029_runtime_music_generation_and_discovery_via_abstractcore.md` |
| 0030 | `completed/0030_runtime_host_facades_for_comms_telegram_and_tool_specs.md` |
| 0032 | `completed/0032_runtime_durable_outbound_comms_truth.md` |
| 0033 | `completed/0033_runtime_host_local_prompt_cache_export_import_surface.md` |
| 0035 | `completed/0035_model_residency_provider_truth_for_local_http_clients.md` |
| 0037 | `completed/0037_visualflow_generate_music_node_compiler_parity.md` |
| 0039 | `completed/0039_runtime_music_structure_prompt_bool_contract.md` |
| 0040 | `completed/0040_task_agnostic_local_residency_listing.md` |
| 0041 | `completed/0041_runtime_hardware_extras_avoid_nonpermissive_document_stacks.md` |
| 0042 | `completed/0042_core_vision_upscale_and_parameter_surface.md` |
| 0043 | `completed/0043_runtime_vision_adapter_and_batch_surface.md` |

## Planned

| ID | Item |
|----|------|
| 008 | `planned/008_signatures_and_keys.md` |
| 014 | `planned/014_remote_tool_worker_executor.md` |
| 017 | `planned/017_limit_warnings_and_observability.md` |
| 018 | `planned/018_workspace_access_policy_for_media_and_tools.md` |
| 025 | `planned/025_runtime_retention_and_purge_contract.md` |

## Proposed

| ID | Item |
|----|------|
| 0031 | `proposed/0031_runtime_tool_spec_adapters_for_gateway_and_mcp.md` |
| 0036 | `proposed/0036_local_media_residency_bridge_to_core_residency.md` |
| 0038 | `proposed/0038_core_server_pool_residency_affinity.md` |

## Deprecated

See `deprecated/DEPRECATED_README.md` for context on the deprecated backlog set.
Recent deprecation:
- `deprecated/0034_agent_runtime_convenience_constructor.md`

---

## examples/README.md

# AbstractRuntime Examples

Runnable examples demonstrating AbstractRuntime capabilities.

## Quick Start

```bash
cd examples
python 01_hello_world.py
```

## Examples

| Example | Description | Dependencies |
|---------|-------------|--------------|
| 01_hello_world.py | Minimal workflow with zero-config | None |
| 02_ask_user.py | Pause for user input, resume with response | None |
| 03_wait_until.py | Schedule a task for later | None |
| 04_multi_step.py | Multi-node workflow with branching | None |
| 05_persistence.py | File-based storage, survive restart | None |
| 06_llm_integration.py | LLM call with AbstractCore | abstractcore, ollama |
| 07_react_agent.py | Full ReAct agent with tools | abstractcore, abstractagent, ollama |

## Requirements

Examples 1-5 only require abstractruntime:
```bash
pip install abstractruntime
```

Examples 6-7 use Runtime's base AbstractCore integration:
```bash
pip install abstractruntime
# Example 07 also requires AbstractAgent (separate package/repo).
# Also requires Ollama running locally with qwen3:4b-instruct-2507-q4_K_M
```

## Running Examples

Each example is self-contained. Run directly:

```bash
python 01_hello_world.py
```

For interactive examples (02, 05), follow the prompts.
