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:
from abstractruntime import Effect, EffectType, Runtime, StepPlan, WorkflowSpec
Storage helpers (common stores):
from abstractruntime.storage import (
InMemoryLedgerStore,
InMemoryRunStore,
JsonFileRunStore,
JsonlLedgerStore,
)
Scheduler convenience wrapper:
from abstractruntime import create_scheduled_runtime
AbstractCore integration (included in the base abstractruntime install):
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, orcomplete_output.Effect/EffectType: durable side-effect request protocol (the runtime mediates execution).RunState/RunStatus: durable checkpoint for a run, persisted by aRunStore.WaitState/WaitReason: durable pause metadata forWAIT_*/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=..., parent_run_id=..., run_id=...) -> run_id- creates and persists a new
RunState run_idstarts the run under an id you choose ([A-Za-z0-9_-]+). The run is created only if that id is free; starting it again with the same identity (workflow, session, parent,vars._meta.occurrence,vars._meta.creation_digest) returns the existing run untouched, and a different identity raisesRunIdentityConflict(aValueError,reason_code = "identity_conflict"). The store must supportcreate_if_absent(otherwiseNotImplementedError).START_SUBWORKFLOWaccepts the same explicit id aspayload.run_id, andpayload.resolve_vars(a list of top-level var names) resolves the artifact refs in those vars for the child only, strictly (a missing artifact fails the start), so the parent's records keep the refs- a root run that names a session is checked against the session's attribution: in a discussion session it gets the discussion's workspace setup (its own writable workspace and the read-only mount) and provenance, and when the attribution cannot be read (an invalid discussion root, or a store without a run index) the start raises
SessionAttributionError(see automations.md) Runtime.tick(workflow, run_id, max_steps=..., step_gate=None) -> RunState- executes node handlers and effects until the run becomes
WAITING,COMPLETED,FAILED, orCANCELLED - optional
step_gate()is consulted at every step boundary; when it returnsFalsethe tick returns the persisted state and the run staysRUNNING(a later tick continues where it stopped) Runtime.resume(workflow, run_id, wait_key, payload, max_steps=...) -> RunState- validates the
wait_key, writespayloadtoWaitState.result_key(if set), and continues fromWaitState.resume_to_node - a wait is resumed at most once: a resume of a run that is no longer waiting, or that waits on another key, raises
StaleResumeError(aValueError), for example when another caller resumed it first - the check and the commit run under a per-run lock, and tools approved with
{"approved": true}execute while that lock is held, so a long tool delays a competing resume of the same run, which is then refused run_mutation_lock(run_id)(package root): the per-run, per-process, re-entrant lock thattickholds for the whole tick andresumefor its commit; take it around your own read-modify-save of a run so a tick cannot overwrite your changeRuntime.get_state(run_id) -> RunStateandRuntime.get_ledger(run_id) -> list[dict]- host-facing read APIs for checkpoints and the append-only ledger
Runtime.cancel_run(run_id, reason=None, cancelled_by="api") -> RunState- persists
CANCELLED, then signals every effect of that run (and its in-flight descendants) executing in this process; the running attempt is recorded ascancelled(StepStatus.CANCELLED, never retried) withcancelled_byandreason, and no further effect starts Runtime.set_default_provider_model(provider=..., model=...)- re-points the default provider/model seeded into new runs; pair it with the pooled client's
set_default_provider_model(...)so both agree Runtime.set_live_delta_sink(sink)wheresink(event: dict) -> None, orNoneto remove it- receives live token deltas (
llm.delta,llm.delta_end) for LLM calls of runs started with_runtime.stream: true(a boolean;startrefuses any other value); nothing is written to the ledger. See Live token streaming
Effect cancellation helpers (src/abstractruntime/core/effect_cancellation.py):
- inflight_effects(run_ids=None) lists executing effects (run, step, provider, model, elapsed time)
- request_model_effects_cancel(provider, model, cancelled_by="model_eject") stops the effects using a model before it is unloaded; local unload_model_residency calls it for you
- kill_inflight_effect(step_id, killed_by=...) is an in-process hard stop for a call that ignores its cancel event (it cannot interrupt a thread blocked inside one native call)
Tool scope: an explicit allowed_tools list in run.vars["_runtime"], in a child run's _runtime, or in a TOOL_CALLS payload is a ceiling intersected across the run tree (src/abstractruntime/core/tool_scope.py). Approval policy can remove a prompt but can never grant a tool outside it; a missing key means unrestricted and an empty list denies every tool.
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
Run index and creation (all built-in run stores, including the offloading wrapper):
- create_if_absent(run) -> (run, created): creates a run only if its id is free and never overwrites an existing one; store_supports_create_if_absent(store) / require_create_if_absent(store) check a store first. Process-crash safe; power-loss durability is not claimed
- list_run_index(status=, workflow_id=, session_id=, root_only=, limit=, oldest_first=, automation_id=, role=, session_kind=): lightweight rows carrying automation_id, role, occurrence_index, session_kind and workspace_root (the run's top-level vars["workspace_root"] as stored, stripped; None when absent); the three attribution filters take a value, a comma-separated string or a list; root_only=True returns turn roots (parent-less runs except automation controllers, plus automation occurrences)
- session_kinds(session_id) -> frozenset and latest_occurrence_row(automation_id): indexed lookups used by session attribution and automation listings
- JsonFileRunStore.warm_session_index() builds the session and children indexes at host startup. Store objects and processes sharing one JSON run folder see each other's created and deleted runs through the creation journal .runs_created.log; v1 supports one writer process per store
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.
SnapshotStoreinterface +InMemorySnapshotStore,JsonSnapshotStoreSnapshotmodel (a named bookmark of run state)
Docs: snapshots.md.
Effect policies (retries + idempotency)¶
Implementation: src/abstractruntime/core/policy.py.
EffectPolicyprotocol and implementations:DefaultEffectPolicy,RetryPolicy,NoRetryPolicycompute_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; write_docx
renders Markdown-style content to real .docx bytes with the standard
library; 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/DOCX 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.
Automations¶
Implementation: src/abstractruntime/automations/*, src/abstractruntime/triggers/*, src/abstractruntime/automation_queries.py.
Deep dive: automations.md.
from abstractruntime.automations import (
apply_automation_command, # pause / resume / run_now / revise / stop_current / archive / unarchive
create_automation, # -> (automation_id, revision)
drive_automation, # standalone run loop
get_automation,
list_attention,
list_occurrences,
pending_waits,
register_controller_bundle,
start_discussion,
)
from abstractruntime.automation_queries import latest_occurrence, list_automations
from abstractruntime.triggers import get_trigger_adapter, trigger_sources
create_automation(runtime, request, *, now=None, actor_id=None): creates the controller root run (the automation) with a deterministic id; raisesAutomationError(invalid_definition,unsupported_feature,unknown_trigger_source,identity_conflict)apply_automation_command(runtime, *, automation_id, command_id, type, payload=None, actor=None, expected_revision=None): the only writer of automation state; returns{status: "applied" | "rejected", error?, duplicate}; idempotent percommand_idrecord_automation_command_result(...): records a host-side failure of a command as a rejected result- reads:
get_automation(run_store, id),list_occurrences(runtime, id, cursor=, limit=),list_attention(ledger_store, id, after_seq=, cursor=, limit=),pending_waits(run_store, id, limit=)with typed waits (ask_user,tool_approval,event) andANSWER_PAYLOADS;list_automations(run_store, status=, cursor=, limit=),automation_summary(run),latest_occurrence(run_store, id) start_discussion(runtime, *, automation_id, occurrence_index, request_id, prompt, workspace_root, actor_id=None): a separate conversation forked at any occurrence N, seeded with the automation's whole timeline 1..N, working in its own writableworkspace_rootwith the automation's workspace mounted read-only- controller bundle:
register_controller_bundle(registry),controller_workflow_spec(),controller_bundle_path();CONTROLLER_WORKFLOW_IDisabstractframework.automation-controller@1.0.0:controller - trigger sources:
trigger_sources(),get_trigger_adapter(id, version), built-insschedule@1,manual@1andemail.received@1, third-party sources through theabstractruntime.trigger_sourcesentry-point group - definition v2:
policy.email_allowed_recipients(default["self"]),policy.untrusted_input_tools(default[]) andnotify.channels(default["console"]); attention items carrychannels
Email¶
Implementation: src/abstractruntime/email/*, src/abstractruntime/triggers/email_received.py. Deep dive: email.md.
from abstractruntime.email import (
EmailBinding, bind_email_account, strip_client_email_keys, binding_of, # run-scoped binding
JsonFileEventInbox, InMemoryEventInbox, # durable event inbox
EmailInboxFeeder, PollReport, email_event_id, # mailbox -> inbox
email_trigger_consumers, wake_email_automations, prune_email_inbox, # watcher helpers
EventInboxRetention, # inbox retention (90 days / 10,000 events)
EMAIL_USE_AGENT_TOOL, EMAIL_USE_ACTION, email_use_for_workflow, # who is sending
email_action_target, register_email_action_workflow, validate_email_action, render_email_template,
)
Runtime.set_email_context_resolver(fn):fn(binding, *, use) -> EmailContext | None(useis"agent_tool"or"action";fn(binding)also accepted), per runtime, memory onlyRuntime.set_email_binding(binding | None)/Runtime.email_binding: the account occurrences are bound toRuntime.set_occurrence_input_resolver(fn | None)/Runtime.occurrence_input_resolver:fn(definition, input_data) -> input_data, called at each automation occurrence's admission (its run start) and frozen with the occurrence's inputs (a replayed dispatch stays identical); a non-dict answer fails the admission. The gateway re-resolves an automation's "Use my default" workspaces here. None (default): the definition's inputs as storedRuntime.set_event_inbox(inbox)/Runtime.event_inbox: required byemail.received@1EmailInboxFeeder(inbox, account_ref=...).poll(ctx, *, now=None, force=False) -> PollReportand.status()- toolsets:
get_default_toolsets(..., email_enabled=True)(alsolist_default_tool_specs,build_default_tool_map,list_tool_catalog); no env flag;list_tool_catalog(email_enabled=False, email_off_reason="not_connected" | "admin_disabled" | "not_available" | "agent_tools_off") - mail library for hosts:
abstractruntime.integrations.abstractcore.email_facadere-exportsabstractcore.comms.email(its__all__plus thelegacymodule;GATEWAY_NAMESlists the names hosts use) - approval: the
send_email_recipient@v2refiner (tool-approval.md) adopt_legacy_schedule_projection(run): read-only summary of a legacy gatewayscheduled:*root- read-only mounts:
_runtime.workspace_read_only_paths(absolute folders; file write tools and VisualFlow writers refused inside, reads and command/code tools allowed); helpersread_only_paths(vars),path_is_read_only(vars, path)andREAD_ONLY_PATHS_KEYinabstractruntime.utils.workspace_paths - writable exceptions inside read-only roots:
workspace_writable_paths(top-level or_runtime; absolute folders). A path is read-only when a read-only root contains it and no writable path inside that root contains it, so the more specific rule wins (a host sets/read-only plus the run's folder and its read & write folders for a read-only default). Child runs and VisualFlow file nodes get the parent's exceptions exactly; a child never adds one. Helpers:writable_paths(vars),is_read_only_target(path, read_only_roots, writable_roots),WRITABLE_PATHS_KEY. - read-only workspaces: run vars
workspace_read_only: true(or_runtime.workspace_read_only: true);abstractruntime.integrations.abstractcore.tool_effects.TOOL_EFFECT_CLASSESclassifies every exposable tool asread,write,exec,delegate,commsormemory-write
Sessions and history¶
Implementation: src/abstractruntime/session_turns.py, src/abstractruntime/session_history.py, src/abstractruntime/core/run_attribution.py.
select_session_turns(run_store, session_id, *, include_occurrences=True, until_ms=None, automation_id=None, through_occurrence=None, include_drafts=False, limit=50)(package root): a session's turns, oldest first. Turns are parent-less runs except automation controllers, plus automation occurrences (a retried occurrence counts once, as its newest attempt); child runs, runtime-internal runs, legacy scheduled wrappers and draft-test runs are left outsession_chat_messages(run_store=, ledger_store=, artifact_store=, session_id=, max_tokens=HISTORY_REPLAY_MAX_TOKENS, until_ms=None, exclude_run_ids=None, automation_id=None, through_occurrence=None, strict=False)(package root): the session's completed turns as user/assistant message pairs; in a discussion session the discussion's seed comes first. Returns aReplayedHistory(a list of messages) whose.reportsays what was replayed and dropped.strict=TrueraisesSessionHistoryError(reason_code = "history_unavailable") instead of returning a partial history.max_messages,max_total_charsandmax_chars_per_message(keyword-only, defaultNone) are the caps retired in 0.7.0: they are accepted so hosts built against 0.6 (AbstractGateway 0.6.0) still get their history, and ignored. Passing any of them logs one warning and names them inreport["ignored_inputs"]- The history window:
HISTORY_REPLAY_MAX_TOKENS = 50_000(package root). Replay keeps the most recent turns that fit 50,000 estimated tokens, newest first, as whole messages. There is no message-count cap and no character cap, and no message is ever cut. The fold stops at the first older turn that does not fit, so the window has no gaps. A total of exactly 50,000 tokens fits. One exception: when the newest turn alone is larger than the window, it is kept whole (oversize_turn_kept: true), because cutting it would lose content and dropping it would replay nothing. The model can use the rest of its context window; if a turn is too large for the model, the provider reports the error. Tokens are counted withabstractruntime.memory.token_budget.estimate_message_tokens: for a content-part list it counts the text parts' text plusMEDIA_PART_TOKEN_ESTIMATE(512) for each media part (MEDIA_PART_TYPES: image, audio, file), never the part's base64; any other part (a tool result, a thinking block) counts by its text (0.7.1).max_tokensmust be a positive int ReplayedHistory.report:{policy: "most_recent_whole_turns", max_tokens, token_estimator, replayed_messages, replayed_tokens, dropped_messages, dropped_tokens, dropped_counts_complete, oversize_turn_kept}. Replay stops reading once the window is full, so in a long sessiondropped_counts_completeisfalseand the dropped counts cover only the turns it read. When turns were dropped, the oldest replayed message starts with a[#TRUNCATION: N earlier message(s) ... were dropped from replay by the history window ...]line (ADR-0026). Hosts record the report in the run asvars._runtime.session_historyfold_history_window(pairs, *, max_tokens=HISTORY_REPLAY_MAX_TOKENS)(package root): the window itself, applied to chronological turns (lists of whole messages); returns(kept, report)announce_dropped(messages, report, *, stamp_metadata=True)(package root): writes the window's[#TRUNCATION: ...]notice at the start of the oldest kept message, in place, whenreportsays turns were dropped. Use it after your ownfold_history_windowcall (for example on client-sent history) so the notice matches session replay.stamp_metadata=Truealso addsmetadata.replay_truncatedandmetadata.history_windowto that message; passFalsefor plain role/content messages. When the message's content is a content-part list ([{"type": "text", ...}, {"type": "image_url", ...}]), the notice is added as one{"type": "text"}part and every other part is left as it is, so images stay images (0.7.1); a stamped head keeps its<runtime_metadata>envelope first. Content of any other type is left untouched and a warning is logged._announce_droppedis kept as an alias of the pre-0.7.0 private namewindow_transcript(messages, *, max_tokens=HISTORY_REPLAY_MAX_TOKENS, current_turn_start=None)(package root): the same window over a transcript you already hold (a turn is a user message and the messages after it).current_turn_startis the index of the message that opened the turn in progress: from there on everything is one turn, the newest, always kept whole (oversize_turn_keptwhen it alone exceeds the window), whatever user-role messages a loop adds inside it; an index out of range raisesValueError. Returns aReplayedHistoryof copies with the notice written byannounce_dropped(..., stamp_metadata=False). The entity chat driver and the entity visit workflow use it for their prompts. The visit records the report invars._runtime.session_history; the chat driver puts it onTurnReport.history_window. On the react visit arm, BRIDGE sets_runtime.history_window_tokens(50,000) and_runtime.history_window_turn_start(the visitor's message) and the AbstractAgent react loop (0.3.17 or newer) sendswindow_transcript(context.messages)on each call and records the report; the stored transcript stays whole. The report carrieswindow_applied: true; with an older AbstractAgent that records no window the turn still completes, a warning is logged and the report is{"window_applied": false, "reason": "agent_too_old", ...}(the whole transcript was sent). When the oldest kept message carries a<runtime_metadata>envelope, the notice is written after itsession_attribution(run_store, session_id)(abstractruntime.core.run_attribution):Noneor{"kind": "chat" | "automation" | "occurrence" | "discussion", ...}; a discussion adds its validated root, automation, occurrence and workspace. RaisesSessionAttributionErrorwhen the lookup cannot be completedis_draft_lifecycle(run_lifecycle)(package root): whether a run's lifecycle value marks a draft test run
History bundles (export_run_history_bundle) and session replay use the same turn selection, so automation occurrences appear as turns (kind occurrence, with automation_id and occurrence_index) in both.
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.
When available, it also includes resolved_actions: bounded capability/action summaries derived
from Core route resolution and persisted from LLM_CALL results.
Runtime-owned integrations¶
AbstractCore (LLM + tools)¶
Requires: pip install abstractruntime (AbstractCore 2.24.0 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)
- email facade: email_facade (AbstractCore's mail library for hosts; see Email)
- 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); inline_run_active(run_id) is true while the facade executes a child run in this process (its workflow exists only inside that call), so a host runner that scans RUNNING runs skips it
- 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)
- the host's OpenAI credential for AbstractVoice's openai engines, voice_openai_api_key, travels two ways. For discovery, pass it to get_voice_catalog, list_tts_models or list_stt_models; the local clients hand it to the voice plugin's config, and a remote client never sends it (a remote AbstractCore server uses its own voice configuration). For execution, put it in create_local_runtime(llm_kwargs={"voice_openai_api_key": ...}). Constructor kwargs become the provider's config, which is also the capability-plugin config. Providers read only the keys they name, so a voice_* key changes no text request (tests tests/test_voice_openai_api_key_path.py)
- 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, get_memory_snapshot, list_session_prompt_caches, clear_session_prompt_caches, lock_model_residency, unlock_model_residency, get_context_estimate); the last six are optional in the client contract, and the facade returns {"ok": false, "supported": false, ...} when the configured client does not implement one
- switching the default model (set_default_provider_model on the pooled client) unloads the previous in-process model unless an owner in the process still uses it; list_model_residency diagnostics report pending_ejects and last_switch_ejects, and unload_model_residency(runtime_id=...) accepts any listed local:<task>:<provider>:<model> id, including local:embedding:<provider>:<model> rows (see integrations/abstractcore.md#unloading-and-switching-models)
- lock_model_residency, unlock_model_residency, and get_context_estimate accept an optional payload mapping and/or keyword arguments (keyword arguments win on conflicts); a lock requires provider-verified residency — locking a non-resident pair refuses with a structured model_not_resident payload (load with lock: true instead; remotely the Core server's HTTP 409 refusal is converted to the same payload), while unlock never requires residency so a locked-but-evicted pair is always releasable; a locked model refuses unload_model_residency with a structured model_locked payload — locally as a client-side per-(provider, model) lock, remotely by converting the Core server's HTTP 409 refusal — and unloads only with force=true, releasing the lock only after the unload succeeds
- the MODEL_RESIDENCY effect supports list_loaded, load, unload, lock, and unlock operations with the same soft-fail semantics; on unload, force is forwarded only when authored in the effect payload
- local list_model_residency merges AbstractCore's host-wide loaded-model sweep into text-generation listings (sweep-only rows carry source: "provider_server" and no task label), and residency rows normalize provider size extras to size_bytes / size_vram_bytes while keeping the originals; local text rows also carry registry-declared modalities (omitted on a registry miss), host identity (host_id / host_name), and runtime-owned lock truth (locked / lockable, with pinned a truthful alias of locked — never the default-identity flag, which default alone carries), while remote listings keep the Core server's row identity
- 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 (list_email_accounts, list_emails, read_email, send_email) use the process's own AbstractCore account (single-user installs)
- 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.
Execution controls and cancellation:
- params.thinking (bool or reasoning level) and params.speculation (False, True, or a Core speculation object such as {"mode": "native_mtp", "num_draft_tokens": 2}) are forwarded locally and remotely; run.vars["_runtime"]["speculation"] sets a run-wide preference inherited by subworkflows, Agent loops and delegated calls, and False stays Off across every boundary (see integrations/abstractcore.md#execution-controls-and-local-concurrency)
- get_abstractcore_discovery_facade(...).get_execution_capabilities(model_name=None, provider=None) asks the actual execution host (local or remote) what it supports, without loading a model
- the LLM_CALL handler hands the effect's cancel event to AbstractCore (generate(..., cancel_event=...)); remote clients close the request to the AbstractCore server on Stop, which the server treats as a cancel
- every LLM_CALL is offered a progress callback; providers that report text phases (prefill / generate / complete) produce abstract.progress ledger events with kind: "llm"
- abstractruntime.turn_grounding.stamp_user_turn_grounding(messages, grounding=...) writes the grounding envelope once into the stored user turn, so the prompt sent to the model stays a byte prefix of the next turn's prompt
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
- with the pooled local client (MultiLocalAbstractCoreLLMClient), a request whose outputs are all non-text (image, video, voice, music, transcription) and that carries no tools never builds the text client: it runs on AbstractCore's capability host (CapabilityHostProvider, no text model), and so does streaming TTS. A text default that cannot be built (an engine not installed) therefore never blocks speech or image generation
- media-only normalized results expose runtime_provider / runtime_model separately from media_provider / media_model
- local image/video generation runs in a one-shot subprocess (execution_mode="local_one_shot_subprocess") unless every requested media model is resident (explicitly loaded with load_model_residency), in which case it runs in-process on the loaded pipeline (execution_mode="resident_in_process", resident_load_ids)
- optional local media residency failures complete with status_hint="warning" and degraded=true; local media warmup that no capability plugin can serve (image_generation, image_upscale, video_generation, text_to_video, image_to_video, tts, stt, music_generation) reports requires_long_lived_server=true, and 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
- when Runtime injects the derived session key, the client stamps session attribution (session_id, run_id, workflow_id, node_id, namespace) into the cache entry's metadata after each generate; caller-supplied prompt_cache_keys and binding keys are never stamped, so session-scoped clearing cannot destroy caches shared across sessions
- get_abstractcore_host_facade(...) exposes get_memory_snapshot(), list_session_prompt_caches(session_id=None), and clear_session_prompt_caches(session_id); session caches persist until a host clears them, the owning model is unloaded, or the provider evicts them — completing a run does not clear them
- 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
Workspace scope:
- run vars workspace_root, workspace_access_mode, workspace_allowed_paths, workspace_ignored_paths, and the host-only workspace_builtin_deny_prefixes / workspace_builtin_allow scope file and shell tools; child runs inherit them (see integrations/abstractcore.md#workspace-scoped-tools)
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(...)andabstractruntime.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 insrc/abstractruntime/memory/__init__.py)
See also¶
../README.md— install + quick startgetting-started.md— first durable workflowarchitecture.md— component map + durability invariantsfaq.md— common questions and gotchasintegrations/abstractcore.md—LLM_CALL/TOOL_CALLSwiring
Host ASGI serving¶
abstractruntime.integrations.abstractcore.server_facade.serve_core_request(scope, receive, send, *, token: str, allow_unauthenticated: bool = False) delegates host-admitted ASGI requests with request-scoped Core authentication. See host-managed serving for network and credential responsibilities.
Structured-output checks¶
abstractruntime.integrations.abstractcore.structured_facade.parse_response_format(raw) -> (kind, schema, name) checks an OpenAI response_format and raises ResponseFormatError (AbstractCore's own class, with .param) for an unusable one. Both are also exported flat from abstractruntime.integrations.abstractcore. Requires AbstractCore 2.25.0 or newer. See structured-output checks for hosts.