Skip to main content

europacore

The agent and harness core for building agent-based applications. It includes:

  • AgentHarness: a multi-turn, tool-calling loop with:
    • guardrails
    • checkpoint/resume and human-in-the-loop approval
    • streaming
    • provider-agnostic retry
    • context compaction
    • sub-agent delegation
    • opt-in failure-spend governance (failure_spend_governance=True): a fan-out cap, a circuit breaker for identically failing calls, and a failure budget that steers a failing run to answer early (failure_arm_after_distinct_calls=2 arms it only once two different calls have failed, for tools that report ordinary misses as errors)
  • SingleShotAgent: one prompt in, one schema-validated output, in a single forced LLM call.
  • ToolAgent: the base class for the tools the harness dispatches.
  • Built-in tools for planning, skills and delegation, each enabled by a flag. enable_planning=True registers todo_write and, by default, nudges the model to plan before substantial work; planning_mode="auto" keeps the tool and drops the nudge, so a one-line question costs no extra round-trip.
  • An LLM layer: a Chat-Completions-shaped LLM contract with Amazon Bedrock (Claude) and OpenAI (Responses API) providers, plus a (name, version) prompt registry.
  • Optional durable state and runtimes:
    • Postgres checkpointer and run journal
    • a Temporal-backed autonomous multi-agent runtime
    • sandbox providers (local subprocess, AWS Lambda MicroVMs)

Moving code from the library's predecessor? See docs/migration.md.

API reference and compatibility

docs/api.md lists every public name with its signature and summary; anything not listed there is internal. docs/deprecations.md says what a release may change and how renamed names are phased out.

Install

Python 3.11+.

pip install -e ../europacore                      # core only
pip install -e '../europacore[bedrock,postgres]'  # with extras

As a Poetry path dependency:

europacore = { path = "../europacore", develop = true, extras = ["bedrock", "postgres"] }

The core needs pydantic, pyyaml, jinja2, pybars3 and tiktoken. Everything else is an extra, and is imported only when you use it:

Extra Enables
bedrock europacore.llm.bedrock: BedrockClaudeModel, BedrockEmbeddingModel. A call carrying a cancel token (every harness call) goes over converse_stream, even when not streaming, so a cancel stops it: grant bedrock:InvokeModelWithResponseStream as well as bedrock:InvokeModel; without it the model warns once and falls back to converse, whose cancelled calls run to the end. BedrockClaudeModel(performance_config={"latency": "optimized"}) asks for latency-optimised inference, kept only on the model/region pairs Bedrock accepts it for (LATENCY_OPTIMIZED_SUPPORT); BedrockConfig.get_performance_config(model_id, region) is the opt-in lookup for a model factory.
openai europacore.llm.openai: embedding and transcription models
openai-responses from europacore.llm.openai.OpenAIModel import OpenAIModel: chat models on the OpenAI Responses API, over requests/requests-sse (no openai SDK). Calls the harness drives make a single attempt, so the harness is the only retry layer; external_retry=True does the same for direct calls. A non-streaming call carrying a cancel token (every harness call) is sent over the event stream and returns only the final response, so a cancel closes the connection.
bedrock-openai from europacore.llm.bedrock.BedrockOpenAIModel import BedrockOpenAIModel: GPT models on Bedrock's OpenAI-compatible endpoint, minting short-term Bedrock API keys from AWS credentials
postgres PostgresHarnessCheckpointer, PostgresHarnessJournal, PostgresAutonomousRunStore
temporal europacore.autonomous.temporal.TemporalAutonomousCoordinator
sandbox from europacore.sandbox.LambdaMicroVmProvider import LambdaMicroVmProvider
browser in-process Playwright browser ops for the local sandbox
promptstore remote prompt sets (template_source other than "local")
telemetry PromptStore tracing of runs, model calls and tools (no-op without it)
costs USD cost estimates in HarnessResult for models beyond the bundled catalog (via litellm prices)
mcp europacore.agents.tools.mcp_tools.McpToolset: an MCP server's tools as harness tools (MCP SDK 2.x)
otel OpenTelemetry spans and metrics (GenAI semantic conventions) for runs, model calls and tools
server europacore.server: sessions, runs and resumable SSE streams over HTTP (FastAPI, uvicorn), and the europacore serve command
credentials cryptography, for the default encryptor of stored tool credentials (europacore.credentials.FernetEncryptor)

poetry install --all-extras installs all of them. There's no all extra, because Poetry 2.0 rejects self-referencing extras and a duplicated list makes the resolver explode.

Quickstart

An agent that runs offline on a core-only install (examples/minimal_agent.py):

from europacore.agents import AgentHarness, function_tool
from europacore.testing import ScriptedLLM, text, tool_call

@function_tool
def add(a: float, b: float) -> float:
    """Add two numbers."""
    return a + b

llm = ScriptedLLM([tool_call("add", {"a": 2, "b": 40}), text("2 + 40 = 42")])   # plays the model
agent = AgentHarness(llm, tools=[add], system_prompt="You are a careful calculator.")
print(agent(question="What is 2 + 40?").text)

To go live, pass a real model instead of ScriptedLLM, such as create_claude_haiku_4_5() below.

With Bedrock, a prompt template and a structured answer

This needs the bedrock extra and AWS credentials.

pip install -e '../europacore[bedrock]'
export EUROPACORE_TEMPLATE_DIRS=./templates   # where calculator/v1.0.0/calculator.yaml lives
from pydantic import BaseModel
from europacore.agents import AgentHarness, InMemoryHarnessCheckpointer, ToolAgent, ToolResult
from europacore.llm.bedrock.bedrock_config import create_claude_haiku_4_5


class AddInput(BaseModel):
    a: float
    b: float


class AddTool(ToolAgent):
    description = "Add two numbers."
    input_schema = AddInput

    def __init__(self, **kwargs):
        super().__init__(name="add", **kwargs)

    def execute(self, input: AddInput) -> ToolResult:
        return ToolResult(ok=True, content=str(input.a + input.b))


class Answer(BaseModel):
    answer: str


agent = AgentHarness(
    llm=create_claude_haiku_4_5(),
    tools=[AddTool()],
    output_schema=Answer,                    # the model must finish by calling output_formatter
    template=("calculator", "1.0.0"),        # <template dir>/calculator/v1.0.0/calculator.yaml
    checkpointer=InMemoryHarnessCheckpointer(),
)

result = agent(question="What is 2 + 40?", session_id="s1")
print(result.output)                         # {'answer': '...'}

for event in agent.stream(question="And 3 + 4?", session_id="s2"):
    ...                                      # HarnessEvent(type=...), then the HarnessResult

A function becomes a tool with @function_tool: its parameters become the input schema, the docstring's first paragraph the description and its Args: lines the field descriptions. The recovery metadata a ToolAgent declares as class attributes are keyword arguments:

from europacore.agents import function_tool

@function_tool(retry_safety="read_only", side_effect="read_only")
def add(a: float, b: float) -> float:
    """Add two numbers.

    Args:
        a: The first addend.
        b: The second addend.
    """
    return a + b

agent = AgentHarness(create_claude_haiku_4_5(), tools=[add], system_prompt="You are a careful calculator.")

A parameter named context receives the per-call context described next. The defaults are the conservative ToolAgent ones, so declare a read-only tool as such: a non-idempotent tool in flight at a crash waits for a human instead of running again.

An MCP server's tools join a harness through McpToolset (the mcp extra):

from mcp import StdioServerParameters
from europacore.agents.tools.mcp_tools import McpToolset

with McpToolset(StdioServerParameters(command="github-mcp-server", args=["stdio"]), name="github") as github:
    agent = AgentHarness(llm, tools=github.tools(), system_prompt="...",
                         extra_guardrails=[PermissionGuardrail([("mcp:github:create_*", "ask")])])
  • Session. The toolset keeps one session open until it is closed. server is anything the MCP SDK's Client accepts: stdio parameters, a streamable-HTTP URL, or an in-process server.
  • Permissions. Each call is gated as mcp:<name>:<tool> as well as by its tool name.
  • Selection. tool_filter and prefix select and rename tools.
  • Annotations. A server's annotations are trusted for recovery metadata only with trust_annotations=True.

A tool that needs the per-call context (the cancel event, its call id, the run's remaining budget) sets wants_context = True and defines execute(self, input, context). A tool that forwards to other tools, such as an MCP gateway, can override start_event_data(args) to name the operation on its tool-call-start event, and gated_operation(args) so that PermissionGuardrail rules and approvals see the remote operation rather than the gateway.

Without an output_schema, the run ends on the model's first reply with no tool calls, and that reply is the answer. No output_formatter tool is declared, so a one-shot question costs one model call, not two:

agent = AgentHarness(create_claude_haiku_4_5(), system_prompt="Answer in one sentence.")
print(agent(question="What is the capital of France?").text)   # output == {"text": "..."}

When a cap stops such a run, the forced final answer is also asked for as text. soft_force_terminal_at_step and guardrails that override on_terminal work on the output_formatter call, so they need an output_schema; passing them without one raises at construction.

A template that cannot be found raises at construction, listing every directory searched. For a quick agent or a test, pass system_prompt="..." instead of template=; the user turn is then the question argument. The template file is the one in examples/templates/calculator.

examples/template_agent.py is the same agent with a scripted model, so it runs offline on a core-only install. europacore.testing provides that model (ScriptedLLM) and response builders (tool_call, final, text) for testing your own agents. A script item may also be an exception to raise, or a callable (request) -> response that answers from what the model was sent. Run it with poetry run python examples/template_agent.py.

To test against real model behaviour offline, record a run once and replay it in CI:

from europacore.testing import RecordingLLM, ReplayLLM, frozen_clock, sequential_ids

with frozen_clock(datetime(2030, 1, 1)), sequential_ids():   # a stable prompt date and ids
    run_agent(RecordingLLM(create_claude_haiku_4_5(), "tests/cassettes/triage.jsonl"))   # live, once

with frozen_clock(datetime(2030, 1, 1)), sequential_ids():
    run_agent(ReplayLLM("tests/cassettes/triage.jsonl"))   # offline
  • What a cassette holds. It is JSON lines: the recorded model's attributes, then each completed call's request, response and streamed chunks, redacted for credentials. Failed attempts are not recorded.

  • Strict replay. ReplayLLM serves the calls in order and raises CassetteMismatch when a request differs from the recorded one, showing both keys and each side's last message (strict=False skips the check), so a change in what the harness sends fails the test.

  • Evaluation. europacore.evaluation scores an agent against an eval set (JSON). Each case declares the tool calls it expects (exact, in_order or any_order), an output to contain, and rubric statements a judge model scores:

    from europacore.evaluation import EvalSet, RubricJudge, evaluate
    
    def test_triage():
        evaluate(build_agent, EvalSet.load("evals/triage.json"), judge=RubricJudge(judge_model)).assert_passed()
    

    A failing case's summary shows the tools it called, its answer and the rubrics it missed. runs= averages repeated runs, and threshold= sets the bar per metric.

  • Reproducible runs. frozen_clock and sequential_ids fix every timestamp and generated id europacore records: run, turn, event and pause ids, checkpoint times and the prompt's current_date. They are process-wide for the with block.

A missing optional dependency raises MissingExtraError (a ModuleNotFoundError) naming the extra to install.

Constructor reference

AgentHarness(llm, output_schema=None, tools=(), *, ...): the model (llm), the terminal output schema (output_schema, optional: without one the final text reply is the answer) and the tool list (tools) are positional; everything else is keyword-only. Grouped by what it controls; the defaults are the ones that apply when the argument is omitted.

The same arguments also come in groups, one per heading below, from europacore.agents.options, and AgentHarness takes them directly:

from europacore.agents import AgentHarness
from europacore.agents.options import Limits, ModelCall, Prompt

agent = AgentHarness(
    llm,
    tools=[...],
    prompt=Prompt(system_prompt="You triage incidents."),
    limits=Limits(max_steps=40, max_cost_usd=2.0),
    model_call=ModelCall(stream_tokens=True),
)
Keyword Group Heading
prompt Prompt Prompt and identity
model_call ModelCall Model call
limits Limits Limits and cost
tool_dispatch ToolDispatch Tools and dispatch
guardrails Guardrails Guardrails and loop detection
compaction Compaction Compaction
durability Durability Durability
sandbox Sandbox Sandbox and permissions
collaborators Collaborators Replacing a collaborator

Flat and grouped arguments mix: a group's fields left at their defaults leave the flat arguments alone, and an argument set both flat and in a group raises TypeError. The built-in features (Capabilities) keep their flat arguments and their own option types; build_harness(llm, ..., features=[PlanningOptions(...)]) takes those alongside the groups. A test keeps each group's defaults equal to the constructor's.

Prompt and identity

Argument Default
template / system_prompt none Exactly one: a (name, version) template or an inline system prompt (see Prompt templates)
template_source "local" Where template is looked up
extra_system_prompt none Appended to the system prompt
name, description, input_schema none How the harness presents itself when used as another agent's tool
prompt_registry, token_checker europacore.configure() defaults See Application defaults

Model call

Argument Default
llm_params none Request parameters sent with each call (never assigned onto a shared model). {"thinking": "off" | "low" | "medium" | "high"} sets reasoning depth on any provider: Bedrock Claude sends adaptive thinking with output_config.effort (off sends no thinking config; a 5.x model, which thinks by default, is sent {"type": "disabled"}, except Fable 5 and Mythos 5, which reject it and get effort low), OpenAI sends reasoning.effort (off is none) unless reasoning is set
callback_handler a new CallbackHandler Receives harness spans and this harness's model calls. Tools get it per call in their context (callbacks.CALLBACK_HANDLER_CONTEXT_KEY); it is never assigned onto a tool
callbacks none Callbacks registered on that handler at construction
stream_tokens False Yield assistant-delta/thinking-delta events while the model streams
stream_idle_timeout 300.0 s A stream silent this long is retried as stalled
llm_max_attempts, llm_retry_base_delay, llm_retry_max_delay, llm_retry_max_total 5, 1.0, 120.0, 300.0 s The single retry authority: bounded backoff on transient errors, never past max_seconds
enable_recoverable_pause True Pause for an operator on a fixable model error (bad model id, expired credentials) instead of failing
finalize_max_attempts 2 Attempts at a forced final answer

Limits and cost

Argument Default
max_steps 25 Model calls per turn
max_seconds 300.0 Wall clock per turn, including tool waits and retries
max_total_tokens EUROPACORE_MAX_TOTAL_TOKENS, else max(500k, 2.5 × window) Tokens per turn, including tool and compaction calls
max_session_tokens none Tokens across a session
max_cost_usd none USD per turn; needs prices (the costs extra or cost_estimator), warns at construction otherwise
cost_estimator bundled catalog, then litellm price map (model, usage) -> USD or None
soft_force_terminal_at_step none From this step, require the terminal tool

Tools and dispatch

Argument Default
parallel_tool_calls True Run a turn's calls concurrently (gated per tool); off keeps every call on the calling thread, except tools that declare timeout_seconds, which run alone on a worker so they can be timed out
tool_cancel_grace_seconds 5.0 How long running tools get to observe a cancel
overlap_tool_dispatch EUROPACORE_OVERLAP_TOOL_DISPATCH, else off Start read-only tools while the response still streams
tool_result_max_chars 8000 Larger results are stored behind a handle and previewed
tool_result_store, tool_result_store_max_entries, tool_result_store_max_chars in memory, 64, 1,500,000 Where those results live, read back with read_tool_result
large_prompt_threshold_chars, large_prompt_preview_chars 25000, 25000 The same for oversized user messages
image_spool, max_inline_images none, 4 Durable store for attached images; images sent inline per request
credentials none The tool credentials of the person the harness acts for (CredentialService.for_owner(workspace_id, user_id)); see Tool credentials

Guardrails and loop detection

Argument Default
extra_guardrails none Appended to the built-in chain each run
stuck_detection, stuck_exempt_tools True, none Repeated-call and repeated-result detection; tools whose repetition is the job
failure_spend_governance, failure_patterns, failure_arm_after_distinct_calls off, built-in table, 1 Opt-in failure budget and circuit breaker (guardrails/spend.py)
context_fragments none Context blocks composed into the prompt each turn

Compaction

Argument Default
history_compaction_threshold EUROPACORE_COMPACTION_THRESHOLD, else 0.65 Fraction of the window that triggers compaction (clamped to leave one step of headroom)
compaction_llm europacore.configure(compaction_llm_factory=) Summarises dropped turns; none means a structural digest

Capabilities

Argument Default
enable_planning, planning_mode off, "plan" The todo_write tool; "auto" registers it without the prompt nudge
enable_skills, skills off, none Loadable skills: a dict of LoadableSkill. skills= without enable_skills=True raises ValueError (so does subagents= without enable_delegation=True)
enable_delegation, subagents, max_delegation_depth, temporal_delegation off, none, 2, off Sub-agent delegation, optionally through Temporal. A child never gets more tokens, time or cost than its parent has left; under Temporal, cap the child you build with RemainingBudget.from_payload(pause["budget"]).cap(child)
scratchpad_max_keys, scratchpad_value_max_chars none Deprecated and ignored (harness.scratchpad had no reader); removed in 0.3

Durability

Argument Default
checkpointer none Session state; needed for session_id, pause and resume. A session_id without one is stateless, and the harness logs a warning once
journal the checkpointer's, else none Event journal: leases, replay, uncertain tool outcomes
lease_heartbeat_interval_seconds 30.0 How often an open run renews its lease
auto_retry_safe_tool_calls False When a run resumes in place after a crash, run the read-only and idempotent tool calls it left without an outcome again, instead of telling the model their outcome is unknown

Sandbox and permissions

Argument Default
permission_profile derived from the tools Filesystem roots, network domains and capabilities tools may use
sandbox_backend, require_sandbox_backend none, False Where sandboxed tools run; whether running without one is an error
sandbox_escalation, sandbox_escalation_reasons off, filesystem root + network domain Pause for a human on a widenable denial instead of failing the call

Replacing a collaborator (each default is behaviour-preserving, and tests/test_harness_seams.py shows a minimal replacement)

Argument Contract
llm_invoker harness_llm.LLMInvoker: the model call and its retries
token_estimator harness_compaction.TokenEstimatorProtocol
compaction_policy harness_compaction.CompactionPolicy
message_builder harness_host.MessageBuilderProtocol: prompt and image assembly
checkpoint_writer harness_checkpoint.CheckpointWriter: durable save and load

Events

stream() yields HarnessEvents, then the HarnessResult. A durable journal stores each event as it is yielded, so the names are a public contract: they are listed in europacore.agents.HarnessEventType, and a test fails when the library emits a name missing from it. The loop's own events are kebab-case; an event a guardrail's Decision names is snake_case and carries the decision's payload as data (with the guardrail's name under guardrail when the chain adds it). A custom guardrail may emit any name.

Type When
agent-loop-start A run starts
agent-loop-end A run ends with an answer or an error (data.output on success)
agent-loop-paused A run pauses for a decision; the result carries the pause payload
agent-loop-resume resume() continues a paused run
agent-recovery-safe-retry A restarted run found tool calls whose outcome was lost and that may run again (data.outcomes): read-only or idempotent calls, and calls whose tool's reconcile said their effect did not land. They run again with auto_retry_safe_tool_calls; otherwise the model is told the outcome is unknown (or, for an effect that did not land, that the call did not run)
agent-tool-reconciled A restarted run asked a non-idempotent tool's reconcile about a call whose outcome was lost, and the effect had landed (data.outcome); the call is answered with the result reconcile returned
agent-duplicate-request The call's request_id was already answered: the earlier run's result follows, and nothing runs (data.request_id, data.run_id)
agent-step-start, agent-step-end Each model step; a forced final call has data.final_attempt
agent-compaction History was compacted to fit the context window
assistant-delta, thinking-delta Streamed text and reasoning chunks (stream_tokens=True)
assistant-message, thinking The model's complete text and reasoning for a step
token-usage The step's model usage
tool-call-start, tool-call-end Each tool call (name is the tool)
tool-call-progress A running tool's report (report_progress), e.g. the latest output of exec_command; live only, never journaled
response-images The model returned images (data.count)
response-empty-retry An empty reply was reissued once with a nudge
response-truncated A reply hit the output token limit
steering-injected Steering messages were added to the history
cap_hit A cap was reached (data.code): max_steps and the token caps force a final answer, others (max_seconds, cost) fail the run
no_tool_call The model replied without a tool call and was nudged
terminal_validation_failed The output_formatter call was rejected (data.reason: json, schema with data.errors, or missing_candidate)
stuck_same_tool_overuse, stuck_repeated_results Loop detection fired
tool_call_budget, tool_circuit_break, failure_budget A spend guardrail refused a call or forced a final answer
permission_denied, approval_required, policy_escalation_required PermissionGuardrail/RequiresApprovalGuardrail denied or paused a call
credential_required A tool needs a credential the person has not connected; a needs_credential pause follows (see Tool credentials)
subagent_approval_required, temporal_delegation_requested A delegated child needs approval, or is deferred to Temporal
temporal_step_boundary The run pauses between steps so a Temporal activity can checkpoint
insufficient_info The sufficiency guardrail ended the run asking for more information
tool_whitelist_block A tool outside the allowlist was refused
authorization_check_failed A failure_mode="closed" guardrail raised, failing the run
message_size_truncated, message_size_nudge MessageSizeBudgetGuardrail truncated or flagged an oversized message
agent-waiting-children An autonomous run waits on its children (kebab-case for historical reasons)

Models by name, and failover

create_model(name, **kwargs) (europacore.llm) builds a model from its name, whichever provider serves it. Keyword arguments go to the provider's constructor, and each provider still needs its extra.

  • claude-* names that BedrockConfig knows, and Bedrock anthropic. ids, use Bedrock.
  • gpt-* and o* names use OpenAI's Responses API.
  • openai. ids use GPT on Bedrock.
  • A prefix such as bedrock/ or openai/ picks the provider explicitly.
  • register_model_provider(name, pattern, factory) adds your own provider.

FallbackLLM(primary, secondary, ...) sends each call to the first model and, on a transient or recoverable error, to the next:

llm = FallbackLLM(create_model("claude-opus-4-7"), create_model("gpt-6-luna"))
  • What fails over. A malformed request is not retried elsewhere, and a stream fails over only before its first chunk.
  • Cancels. A cancelled call never fails over.
  • Hung streams. first_chunk_timeout= (seconds) fails a streamed call over when a model sends no first chunk in time; it stops models whose provider honours the request's CancelToken (Bedrock and OpenAI do, for non-streaming calls too: they go over the streaming transport). After the harness's stream_idle_timeout finds a model stalled before its first chunk, calls start at the next model for stall_cooldown seconds (default 60; 0 turns it off).
  • Reported attributes. It reports the smallest context window of its models. served_by names the model that answered the last call.

Hooks

For a one-off change to what the model or a tool sees, wrap instead of writing a guardrail:

from europacore.agents.guardrails import ToolHooks
from europacore.llm import ModelHooks

llm = ModelHooks(model, before=lambda request: cache.get(request_key(request)), after=store_in_cache)
agent = AgentHarness(llm, tools=..., extra_guardrails=[ToolHooks(before=fill_defaults, after=redact_result)])
  • ModelHooks(llm, before=, after=) wraps a model. before(request) may edit the request (its messages are a deep copy, so the run's history is untouched) and returns None to call the model, or a response to use instead. after(request, response) returns None or a replacement.
  • ToolHooks(before=, after=) is a guardrail. before(name, args) may edit args and returns None to run the tool, or a value that becomes its result. after(name, args, result) returns None or a replacement result.
  • ToolHooks failures. after sees the full result before it is truncated and stored, and also a failed call's error result (after_errors=False skips those). A hook that raises fails closed by default: before blocks the call and after withholds the result. failure_mode="open" logs the error and carries on instead.

Guardrails remain the way to steer the loop itself: pause, finish, fail or force a final answer.

Prompt templates

A template is a YAML file at <dir>/<name>/v<version>/<name>.yaml:

templateEngine: handlebars        # or jinja2
messages:
  - type: system
    text: You are a careful calculator.
  - type: user
    text: "{{question}}"

Keyword arguments passed to the harness call (question=, repo_name=, …) become template variables.

Handlebars templates have these helpers: list renders an English list ("a, b, and c"), json pretty-prints a value, eq is an inline comparison ({{#if (eq kind 'sql')}}), if_eq is the block form ({{#if_eq kind 'sql'}}...{{else}}...{{/if_eq}}), and object_list renders a list of objects as a numbered key: value outline.

Directories are searched in this order:

  1. PromptRegistry(template_dirs=[...])
  2. EUROPACORE_TEMPLATE_DIRS (separated by os.pathsep)
  3. the library's own templates (europacore/prompts/templates, which holds guardian_approval)

The first match wins, so an app can override a library template by shipping one with the same name and version.

Application defaults: europacore.configure()

The core never imports application code. Anything that is an application choice is either passed to the constructor or registered once at start-up:

import europacore
from europacore.llm.PromptRegistry import PromptRegistry

europacore.configure(
    prompt_registry_factory=lambda cb: PromptRegistry(cb, template_dirs=["my_app/templates"]),
    compaction_llm_factory=cheap_model,    # summarises dropped turns during compaction
    adjudicator_llm_factory=cheap_model,   # GuardianGuardrail's llm_adjudicator()
    telemetry_attr_prefix="myapp",         # span attrs <prefix>.run.id / <prefix>.turn.id
)
Hook Default when unset Per-instance override
prompt_registry_factory(callback_handler) local-YAML PromptRegistry (shared) AgentHarness(prompt_registry=)
token_checker_factory(model, context_window) tiktoken TokenChecker (cached per model) AgentHarness(token_checker=)
compaction_llm_factory() none; compaction uses a structural digest AgentHarness(compaction_llm=)
adjudicator_llm_factory() none; raises if needed llm_adjudicator(llm=)
telemetry_attr_prefix "europacore" —
persistence.postgres.set_default_connection_factory(f) EUROPACORE_DATABASE_URL / DATABASE_URL Postgres*(connection_factory=)

Telemetry

With the telemetry extra installed and PROMPTSTORE_URL / PROMPTSTORE_TELEMETRY_KEY set, a run emits a span tree: harness.run, then loop.iteration, then model.call and tool.call. A delegated sub-agent's run nests under its subagent.spawn span, so a delegation is one trace. The run span carries the turn's prompt-cache totals as cache_read_tokens and cache_write_tokens. Everything no-ops when the extra or the variables are absent.

A tool that drives its own model can put that call on the trace without a span handle in its signature. The dispatcher publishes the tool's span as an ambient parent for the duration of the call:

from europacore.agents import telemetry

def execute(self, input):
    with telemetry.sub_model_span("query-expansion", model=model_id) as span:
        response = self.llm(request)
        telemetry.record_model_usage(span, response["usage"], model_id)

telemetry.instrument_stream(stream, "summariser", model=model_id) does the same for a response generator, opening the span on first consumption. Both are inert outside a harness run. The compaction summariser's calls appear the same way, as compaction-summary under the run span.

OpenTelemetry

With the otel extra installed, the same run also emits OpenTelemetry spans and metrics, following the GenAI semantic conventions. Configure a TracerProvider and MeterProvider with an exporter (OTLP, console, ...) in the application to receive them; without one the API's no-op defaults cost nothing.

  • Spans. invoke_agent {harness} per run and per delegated sub-agent, chat {model} per model call, execute_tool {tool} per tool call, and loop.iteration per step. A delegation is one trace. They carry gen_ai.operation.name, gen_ai.agent.name, gen_ai.tool.name, gen_ai.provider.name, gen_ai.request.model and gen_ai.usage.*. Other attributes use the telemetry_attr_prefix.
  • Metrics. gen_ai.client.token.usage (by gen_ai.token.type) and gen_ai.client.operation.duration.
  • Content. Message content is recorded only with OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true, redacted for credentials and emails.
  • Off switch. EUROPACORE_OTEL=0 turns OpenTelemetry off. Promptstore and OpenTelemetry are independent: either, both or neither.

Security defaults and data retention

Know these before exposing an agent to untrusted input (each trust boundary, its control and its test are listed in docs/threat-model.md):

  • Tool authorisation is open by default. Without a permission_profile, a harness may use every capability its tools declare. PermissionGuardrail is opt-in and allows anything no rule names. For exec_command, pass exec_allowlist=default_exec_allowlist() (the spec option include_exec_defaults does this): a command then runs unasked only when it is an argv (a list cmd, no shell=True, not sh -c), so no shell reads its words, has no path-named program, and every program in it is on a read-only allowlist (inspection commands, read-only git, the common test runners); anything else asks a human or the guardian, and deny rules still apply. Flags that make an allowlisted program run another program or write a chosen path (find -exec, sort -o, git --output) ask too, in any abbreviated or bundled form. Each test runner (go test, pytest, cargo test, npm/pnpm/yarn test) has a flag allowlist instead (RunnerFlags): any flag not on it asks (go test -ldflags/-coverprofile, pytest --html, cargo test -Z, npm test -- --outputFile), and abbreviations are not matched; uniq, tree and printf are not on the list. Every shell line asks, however plain. For a tool that cannot send an argv, default_exec_allowlist(shell_lines=True) (spec option exec_shell_lines) lets a plain line (literal characters only: no expansion, substitution, glob, redirection, subshell or #) run unasked too, which is the weaker check earlier fixes kept patching. The core exec_command (sandbox_tools) sends an argv. Extend the list with .extended(commands=[...], runner_flags={prefix: spec.with_flags(...)}) or explicit ("exec:<command>", "allow") rules. The allowlist decides when to ask; the sandbox profile is the boundary, and the allowlisted test runners execute repository code that only the sandbox contains. Pass the same allowlist to GuardianGuardrail.
  • Sandboxing is your choice. A tool that declares sandbox_required still runs in-process, with a warning, unless you pass require_sandbox_backend=True. There is no default sandbox provider: local runs commands on this host with the worker's environment and is not a security boundary, so choose it explicitly, and use lambda_microvm for untrusted code.
  • Telemetry content capture is opt-in. With PROMPTSTORE_CAPTURE_CONTENT=true, model and tool inputs and outputs go to PromptStore after credential and email redaction. Spans and usage are sent either way when the telemetry extra is configured.
  • Persisted state holds conversations. Checkpoints and journal snapshots store full message histories (credentials redacted). Nothing deletes them unless you do: schedule europacore.persistence.retention.purge_older_than(days), which deletes finished and crashed (lease long lapsed) sessions and finished autonomous graphs, and keeps paused sessions. Spooled images and large tool results live in your object store: reclaim them with a bucket lifecycle rule or purge_spooled_objects(store, prefix, days, keep=...), keeping the keys paused sessions reference. Results spooled through ToolResultSpool are stored with credentials redacted; ImageSpool resolves only handles under its own prefix.
  • Tool credentials never enter the conversation. A token goes from the provider to the encrypted store and from there into one call's context; it is not in the arguments, the history, the journal or telemetry, and an exact echo of it in the tool's output is redacted. The OAuth callback is unauthenticated by design: its single-use, expiring state is the authority (see Tool credentials).
  • Isolation is by owner. Postgres checkpoints, journal reads and saves are scoped to the configured workspace_id/user_id; in-memory stores are keyed by session id only and are meant for development. Workspace-only scoping (require_user_match=False) requires a workspace_id and matches only rows in that workspace: it no longer loads or overwrites a session saved with no workspace.

Environment variables

Tuning knobs an operator can set without a deploy. Constructor arguments always win over them. The unprefixed names these had before (HARNESS_COMPACTION_THRESHOLD, HARNESS_MAX_TOTAL_TOKENS, BEDROCK_CONTEXT_WINDOW, OPENAI_CONTEXT_WINDOW, BEDROCK_OPENAI_CONTEXT_WINDOW, LONG_CONTEXT_PRICING_ENABLED) are still read, with a DeprecationWarning once, until 0.3; the EUROPACORE_ name wins when both are set.

Variable Effect
EUROPACORE_COMPACTION_THRESHOLD Compaction trigger as a fraction of the context window (default 0.65). The harness clamps it to the headroom one more step needs, so a wide max_output_tokens or tool_result_max_chars can lower it.
EUROPACORE_MAX_TOTAL_TOKENS Cumulative run-token budget (default max(500_000, 2.5 x context_window)).
EUROPACORE_BEDROCK_CONTEXT_WINDOW / EUROPACORE_OPENAI_CONTEXT_WINDOW / EUROPACORE_BEDROCK_OPENAI_CONTEXT_WINDOW Override the context window europacore.llm.model_specs.model_context_window resolves from the bundled catalog or litellm for a provider.
LITELLM_LOCAL_MODEL_COST_MAP Defaulted to True before europacore imports litellm, so litellm reads its bundled price map rather than fetching one. Set it to the empty string to fetch the remote map.
EUROPACORE_LONG_CONTEXT_PRICING_ENABLED=true Lift the cap that keeps a model's window below its long-context price cliff (e.g. 272k on gpt-6-luna), accepting the higher rate above it.
BEDROCK_MAX_POOL_CONNECTIONS HTTP connection pool for the Bedrock clients (default botocore's 10). Raise it to match the thread count of parallel tool dispatch or batch embedding, or botocore churns connections past the pool.
EUROPACORE_TEMPLATE_DIRS Extra prompt-template directories (see above).
EUROPACORE_DATABASE_URL / DATABASE_URL Default Postgres connection (see below).
EUROPACORE_DB_CONNECT_TIMEOUT_SECONDS / EUROPACORE_DB_STATEMENT_TIMEOUT_MS Timeouts for that default connection (10 s / 30 000 ms; 0 disables the statement timeout).
EUROPACORE_OVERLAP_TOOL_DISPATCH=true Default for overlap_tool_dispatch (start read-only tools while the model is still streaming). A legacy name is still read (see docs/migration.md).
EUROPACORE_OTEL=0 Turn OpenTelemetry tracing off even when opentelemetry is installed (see Telemetry).
PROMPTSTORE_CAPTURE_CONTENT=true Send model and tool inputs and outputs to PromptStore telemetry (redacted for credentials and emails). Off by default.
SANDBOX_PROVIDER lambda_microvm (isolated) or local (subprocess on this host, not a security boundary). Required: unset or any other value raises.
SANDBOX_AGENT_TOKEN, SANDBOX_MICROVM_REGION, SANDBOX_MICROVM_IMAGE_ARN, SANDBOX_MICROVM_EXECUTION_ROLE_ARN MicroVM sandbox settings (region falls back to AWS_REGION, then AWS_DEFAULT_REGION). Set SANDBOX_AGENT_TOKEN on both the worker and the VM: the in-VM agent refuses every request without one.
SANDBOX_AGENT_INSECURE_NO_AUTH=1 Run the in-VM agent with no bearer check (local development only).
AWS_DEFAULT_REGION, AWS_REGION Region for the Bedrock models when none is passed (AWS_DEFAULT_REGION first, default us-east-1).
AWS_BEARER_TOKEN_BEDROCK / BEDROCK_API_KEY, BEDROCK_OPENAI_BASE_URL Credentials and endpoint for BedrockOpenAIModel (otherwise minted from AWS credentials).
OPENAI_API_KEY, OPENAI_API_URL, OPENAI_RESPONSES_API_URL, OPENAI_PROJECT, OPENAI_ORG_ID, OPENAI_FORCE_PROJECT_HEADER OpenAI credentials and endpoints for OpenAIModel.
OPENAI_CONNECT_TIMEOUT_SECONDS / OPENAI_READ_TIMEOUT_SECONDS OpenAIModel HTTP timeouts (10 s / 900 s).
OPENAI_PROMPT_CACHING_ENABLED Toggle OpenAI prompt-cache keys on requests.
EUROPACORE_CREDENTIALS_KEY Fernet key(s) encrypting stored tool credentials; comma-separated to rotate (the first encrypts). PostgresCredentialStore refuses to start without one unless allow_ephemeral_key=True.

Durable state (Postgres)

Apply the schema on every deploy. It is idempotent, has no foreign keys to application tables, and records its revision in europacore_schema_version:

from europacore.persistence import apply_schema
version = apply_schema(connection_factory)   # == europacore.persistence.SCHEMA_VERSION

schema_sql() returns the raw DDL if you run migrations with your own tool.

Then provide connections. A connection factory is a zero-argument callable that returns a psycopg 3 connection with row_factory=dict_row, used as with factory() as conn:

from europacore.agents import PostgresHarnessCheckpointer
from europacore.persistence.postgres import set_default_connection_factory

set_default_connection_factory(my_app.db.get_database_connection)
checkpointer = PostgresHarnessCheckpointer(workspace_id=ws, user_id=user)

Resuming is owner-scoped. A configured user_id must match the row's owner. user_id=None is a system context that may resume anything. require_user_match=False falls back to workspace-only scoping.

Human-in-the-loop

A guardrail can pause a run; for example, RequiresApprovalGuardrail(tools=["send_email"]). When that happens:

  1. The harness checkpoints status="paused".
  2. It returns a HarnessResult with paused=True.
  3. You continue the run with agent.resume(session_id, approve=True | False, instruction=..., pending_tool_call_id=result.pause["pending_tool_call_id"]).

Always pass pending_tool_call_id. It binds the decision to the pause it answers: if the session is on another pause by then (a retried request or a double click delivered the same approval twice), resume raises PauseMismatchError and changes nothing. Without it, a duplicated approval approves the next action, which no human has seen. It is optional in 0.1 and required from 0.2; an HTTP or websocket resume endpoint should require it from the client now, since retries and double submits are exactly what it guards against. Every pause carries the id. For a gated call it is that call's id. For a delegated child's approval (kind="subagent_approval") it is the child's gated call, and the parent's delegate call is in delegate_tool_call_id. A pause with no gated call (llm_recoverable, fork_seed, unknown_tool_outcome) gets a generated pause-… id.

A rejected call is answered with {"approval": "rejected", "executed": false, "reason": ..., "next_step": ...}, so the model reports it as declined rather than done.

Shutdown

A durable journal (Postgres) leases each open run to its process; the lease lasts max(120 s, 4 × lease_heartbeat_interval_seconds) and is renewed in the background. A renewal that fails transiently is retried until the lease would lapse, and one that hangs counts as lost at that point too. A process that stops mid-run (SIGTERM, a deploy, a crash) does not release its lease. A worker on the same host takes the session over at once when the holder is provably gone (its pid no longer exists, or it is an earlier incarnation of the worker's own pid: owner ids carry a per-process nonce). A worker on any host does the same when the holder's journal had a liveness lock: pass liveness=PostgresProcessLiveness(dsn) (europacore.agents.harness_journal_liveness) to PostgresHarnessCheckpointer or PostgresHarnessJournal. Each process then holds one Postgres advisory lock on a dedicated connection. Postgres releases it when the process dies: at once on a crash, and within about 25 s of TCP keepalives when the host is lost. A worker that finds the session taken probes that lock. The DSN must reach Postgres directly, or through a pooler in session mode, because a transaction-mode pooler releases the lock behind the process's back. A holder judged dead while still alive (its liveness connection dropped) is fenced: its next write raises RunLeaseError. Otherwise the session is unavailable until the lease expires (up to the TTL above): its next stream or resume raises RunLeaseError. Then the run is recovered. A non-idempotent tool that was in flight is asked whether its effect landed, if it overrides ToolAgent.reconcile(input, context). If the effect landed, it returns the call's result; if it did not, it returns None, and the call is treated as never run; if it cannot tell, it raises. Only a call that no tool can settle pauses the session as unknown_tool_outcome instead of running again. Every call's context carries an _idempotency_key (session_id:tool_call_id), the same on every attempt at that call. A tool that passes it to an API that de-duplicates by key (an Idempotency-Key header) can declare itself idempotent, or ask the API in reconcile. For effects written to the harness's own Postgres, europacore.agents.tool_effects makes the answer certain. The tool records its result with record_tool_effect(conn, context, result) in the transaction that makes the effect, and the TransactionalEffects mixin's reconcile reads it back. Effect and record commit together, so the call runs exactly once. To shut down cleanly, stop taking new turns and let open runs finish, or cancel them through their cancel_event, before the process exits.

With a journal, the checkpointer row is rewritten only at a run's first save, a pause, a final save, a compaction and every 16th save; in between the journal's deltas hold the state, and loading takes the newer of the two. So a session whose row says running will not load while the journal cannot be read: that row may be several saves behind, and resuming from it could run a tool again.

Live steering

agent.steer(text, command_id=None) queues a user message that is injected, framed as a mid-run interruption, before the next model call. command_id is an idempotency key: the same steer delivered twice (a live call plus a durable queue, say) lands once, and the applied keys are checkpointed in the run metadata for the rest of the turn. Without a command_id the text itself is the key.

A steer belongs to the run that is live when it arrives. steer() returns False when no run is live, so send a normal turn instead. A steer the run ended before injecting comes back on HarnessResult.undelivered_steers; it is never carried into the next run.

HTTP server (europacore.server)

The server extra serves harness sessions over HTTP with resumable Server-Sent Events. The protocol is in docs/server-api.md and the OpenAPI document in docs/server-openapi.json. Register a factory per agent; it is called once per session and must give the harness a checkpointer and journal scoped to the caller:

from europacore.server import AgentRegistry, StaticTokenAuthenticator, create_app, default_persistence

def coder(ctx):                                   # ctx: SessionContext(session_id, principal, model, metadata)
    stores = default_persistence(ctx)             # in-memory, or Postgres when EUROPACORE_DATABASE_URL is set
    return AgentHarness(create_model(ctx.model or "claude-sonnet-4-5"), tools=[...], system_prompt="...",
                        checkpointer=stores.checkpointer, journal=stores.journal)

registry = AgentRegistry()
registry.register("coder", coder, models=["claude-sonnet-4-5", "claude-haiku-4-5"], default_model="claude-sonnet-4-5")
app = create_app(registry, authenticator=StaticTokenAuthenticator(token))

or europacore serve myapp.agents:registry --port 0, which prints a {"event": "ready", "url": ..., "token_file": ...} line on stdout and writes a generated token to a 0600 file. examples/server_agents.py is an offline registry to try it with. examples/server_bedrock_agents.py serves a Bedrock Claude agent with tools, planning and an approval pause (needs the bedrock extra and AWS credentials).

  • Runs are resources. A run executes in a server-owned task; closing the connection does not cancel it. POST /v1/runs/{id}/cancel and /steer act on it, and if_busy (reject, steer, queue) decides what a second turn does while one is active.
  • Resumable streams. Every event has a per-session seq; reconnect with Last-Event-ID to replay what you missed. Token deltas are best-effort; the step's assistant-message always carries the full text.
  • Identity from the token. An Authenticator returns the Principal(workspace_id, user_id) that owns sessions; another principal's session answers 404.
  • Decisions are bound to their pause. POST /v1/sessions/{id}/resume requires pending_tool_call_id; a stale one gets 409 pause_mismatch with the current pause.
  • Tool credentials. create_app(..., credentials=service) puts each principal's credentials on SessionContext.credentials (pass it to AgentHarness(credentials=)) and serves /v1/credentials and the OAuth callback /v1/oauth/callback, which resumes the paused run itself.
  • Session index. Checkpointers cannot list sessions, so the server keeps an index and its event log: InMemorySessionIndex, or PostgresSessionIndex (the server_sessions tables, schema version 6).

Tool credentials (europacore.credentials)

A tool says which credential it needs; the harness finds it before each call, and asks the person for it when there is none. The token never passes through the model, the conversation, the pause or resume. docs/tool-oauth.md walks through the OAuth flow end to end.

from europacore.credentials import (
    CredentialService, OAuth2, PostgresCredentialStore, get_credential, github_provider,
)

@function_tool(retry_safety="read_only", auth=OAuth2("github", scopes=("repo",)))
def list_prs(repo: str, context: dict) -> list:
    """List open pull requests."""
    token = get_credential(context, "github").token     # from the call's context, never an argument
    ...

credentials = CredentialService(
    store=PostgresCredentialStore(),                    # encrypted with EUROPACORE_CREDENTIALS_KEY
    providers=[github_provider(client_id, client_secret, scopes=("repo",))],
    redirect_uri="https://app.example.com/v1/oauth/callback",
)
agent = AgentHarness(llm, tools=[list_prs], system_prompt="...", checkpointer=checkpointer,
                     credentials=credentials.for_owner(workspace_id, user_id))

Requirements. A tool declares one of three, on ToolAgent.auth or function_tool(auth=). Each names a provider the service is configured with, and a mismatch fails at construction.

  • OAuth2(provider, scopes=()): a person connects their account.
  • ApiKey(provider): a person enters a key once.
  • ServiceCredential(provider): the application's own, from ServiceProvider(fetch=); it never asks anyone.

Ownership is per provider. owner="user" (the default) means each person connects their own account. owner="workspace" means one connection serves everyone in the workspace.

When the credential is missing, the run pauses with kind="needs_credential" before the call runs. The pause has a connect block: provider, display_name, kind, owner, scopes, plus auth_url for OAuth or instructions for an API key. A call that also needs approval pauses for approval first, and to connect second, so nobody is asked to connect an account for an action they then reject. resume(approve=True) means "connected": the gate checks again, and pauses again with a fresh link if the credential is still missing. approve=False answers the call as not run. A delegated sub-agent with no credentials of its own acts with its parent's, and its pause reaches the parent with the same connect block.

The OAuth callback stores the token and resumes the run:

  • europacore.server. create_app(..., credentials=service) serves /v1/oauth/callback (docs/server-api.md §3.6).

  • Your own route. Call the framework-free helper:

    @app.get("/oauth/callback")
    def oauth_callback(state: str, code: str | None = None, error: str | None = None):
        result = credentials.complete_oauth(state, code=code, error=error)
        if result.resumable:   # connected, or refused at the provider
            start_in_background(harness_for(result.initiator).resume, result.session_id, approve=result.ok,
                                pending_tool_call_id=result.pending_tool_call_id)
        return HTMLResponse(callback_page(result), headers=PAGE_HEADERS)
    

    The single-use, ten-minute state is the only authority the callback needs: the provider's redirect carries no login. Flows use PKCE.

For an API key, store what the person entered with credentials.put_api_key(workspace_id, user_id, provider, key) (the server: PUT /v1/credentials/{provider}), then resume.

At call time.

  • An expired OAuth token is refreshed first, one refresh at a time per owner and provider.
  • If the provider refuses the refresh token, the credential is forgotten and the person is asked to connect again. A transient failure keeps it.
  • A tool that finds its credential rejected raises CredentialRejected(provider). The harness forgets the credential and tells the model the call did not run, and the next call asks to connect.
  • An exact echo of the token in the tool's result or error reaches the model and the history as [redacted].

Storage. PostgresCredentialStore (schema version 8) encrypts secrets with default_encryptor(). That is Fernet, from the credentials extra, keyed by EUROPACORE_CREDENTIALS_KEY. Several comma-separated keys rotate: the first encrypts and every one decrypts. Generate a key with FernetEncryptor.new_key(). Without a key the store refuses to start, unless allow_ephemeral_key=True; that per-process key loses every credential on restart, so use it in development only. Pass encryptor= to use a KMS. InMemoryCredentialStore is for tests. purge_older_than deletes expired flows; connected credentials stay until the person disconnects.

Long-term memory (europacore.memory)

A MemoryService keeps short texts per user and finds the ones relevant to a query. PostgresMemoryService(workspace_id) uses Postgres full-text search over the agent_memories table (schema version 5), so it needs no database extension. InMemoryMemoryService is for development.

from europacore.agents.tools.memory_tools import MemoryPreloadGuardrail, memory_tools
from europacore.memory.postgres import PostgresMemoryService

memory = PostgresMemoryService(workspace_id=ws)
agent = AgentHarness(llm, tools=memory_tools(memory, user, writable=True), system_prompt="...",
                     extra_guardrails=[MemoryPreloadGuardrail(memory, user)])
...
memory.add_session(user, session_id, checkpointer.load(session_id).messages)   # when a session ends
  • Tools. load_memory lets the model search, and save_memory (with writable=True) lets it store a fact.
  • Preload. MemoryPreloadGuardrail adds the memories matching the user's latest message to that turn.
  • Fencing. Memories reach the model fenced as background data, never as instructions.
  • Scope. Every call names its user_id, and a search returns only that user's memories within the workspace.
  • Deletion. forget(user_id) deletes one or all of a user's memories.

Autonomous runtime (europacore.autonomous)

A persisted graph of agent runs: a root run plus researcher, coder, reviewer and verifier children.

  • Graph store: AutonomousRunStore, in-memory or Postgres.

  • Agent tools: autonomous.tools.build_collaboration_tools(coordinator, run_id) gives an agent spawn_agent, send_message, wait_agent, interrupt_agent, list_agents and request_user_input.

  • Budgets: a spawned child's record carries what was left of its parent's token, time and cost limits (AgentRunRecord.budget). Neither coordinator applies it: the application's runner that builds the child's harness must call run.cap_harness(harness) (or ChildBudget.cap), and on a freshly built harness, since the cap is permanent. Then a child never gets more than its parent had left. (In-process delegation caps the child per call itself.)

  • In-process coordination: AutonomousCoordinator. A child that raises AutonomousInfrastructureError (dead object storage, bad credentials) fails the root and cancels its siblings at once, instead of being retried child by child; a Temporal child activity should apply the same rule.

  • Temporal coordination: TemporalAutonomousCoordinator, which needs three things from the app:

    • client_factory: an async callable returning a temporalio client
    • steering: optional; anything with steer(run_id, message, *, command_id) and cancel(run_id)
    • root_workflow_id_template: defaults to "autonomous-root-{root_run_id}"

    The app owns the root workflow. That workflow handles the spawn_child, message_child, followup_child and interrupt_child signals.

Sandboxes (europacore.sandbox)

get_sandbox_provider() picks the provider from the SANDBOX_PROVIDER env var (or name=); there is no default:

  • lambda_microvm: LambdaMicroVmProvider, isolated, for untrusted code
  • local: LocalSubprocessSandboxProvider, a subprocess on this host with the worker's environment; not a security boundary

sandbox_tools(sandbox) (europacore.agents.tools.sandbox_tools) gives a harness exec_command, read_file, write_file and edit_file bound to a sandbox:

from europacore.agents.tools.sandbox_tools import sandbox_tools
from europacore.sandbox import get_sandbox_provider
from europacore.sandbox.LazySandbox import LazySandbox

sandbox = LazySandbox(get_sandbox_provider("lambda_microvm"), clone_url=repo_url)
try:
    agent = AgentHarness(llm, tools=sandbox_tools(sandbox), system_prompt="...", require_sandbox_backend=True)
    ...
finally:
    sandbox.destroy()
  • Isolation. On an isolated sandbox (a MicroVM) the tools satisfy sandbox_required themselves. On the local provider they do not, so with require_sandbox_backend=True they are refused, which is the point: local runs commands on this host.
  • Commands. exec_command takes cmd as the permission rules read it: an argv list runs as given, and a shell line (a string) is refused unless sandbox_tools(sandbox, allow_shell=True). Argv words are literal, so the exec allowlist and denied flags see exactly what runs.
  • Paths. Paths are relative to the repository; absolute paths and .. are refused. writable=False leaves out write_file and edit_file.
  • Restarts. keep_sandbox_with_session(sandbox, harness, session_id) (europacore.agents.tools.sandbox_session) records the sandbox's id the moment it is created: in the run's journal at once (so a crash straight after creation still knows it), and in the session's checkpoint at the next save. A run resumed after a restart then reattaches to that sandbox (SandboxProvider.attach), with its edits and build output, instead of cloning afresh. Call destroy() only once the session is done with the sandbox. A sandbox that is gone (a MicroVM past its lifetime cap) is replaced, and a forked session never inherits one. On the local provider only the files survive a restart: services and exec sessions do not. A sandbox whose creation returned but whose record had not reached the journal when the process died is not found again; only the provider's lifetime cap ends it.

sandbox/microvm_image/ holds the in-VM agent and the base Dockerfile. The *_ops.py modules are stdlib-only, so they can be copied into the image unchanged.

Development

poetry install --all-extras
poetry run pytest                                          # unit tests (offline)
poetry run python scripts/api_reference.py                 # regenerate docs/api.md after changing the public API
EUROPACORE_INTEGRATION=1 poetry run pytest -m integration  # live Bedrock tests

Metadata

Release files for europacore 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for europacore 0.1.0
File Size Uploaded Provenance
europacore-0.1.0.tar.gz 624.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for europacore 0.1.0
File Interpreter ABI Platform
europacore-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / europacore-0.1.0.tar.gz

Download URL europacore-0.1.0.tar.gz
Size 624.0 kB
Tags Source
SHA-256 checksum
How to use checksums
7e94c3937bbfc9a1ecc395d197579d2d8b0a1a8dddb8542cd11af5835e932110
BLAKE2b-256 checksum
How to use checksums
4e1ac1ef0e8b0d1f3665464ec5477f5c449542bd464b420dbc68c9950a8019c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.0.1 CPython/3.13.5 Darwin/24.6.0

Release files / europacore-0.1.0-py3-none-any.whl

Download URL europacore-0.1.0-py3-none-any.whl
Size 770.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8d7c6bb4b67eb0b0e307662da8c50b6218178da6e469a81d5580123fd377800c
BLAKE2b-256 checksum
How to use checksums
1b6191e362a365c426108f6e8a02d863b3fdf2a5f3d9205cb3c1ca882f7c5b32
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.0.1 CPython/3.13.5 Darwin/24.6.0

Security for europacore 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page