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=2arms 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=Trueregisterstodo_writeand, 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
LLMcontract 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.
serveris anything the MCP SDK'sClientaccepts: 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_filterandprefixselect 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.
ReplayLLMserves the calls in order and raisesCassetteMismatchwhen a request differs from the recorded one, showing both keys and each side's last message (strict=Falseskips the check), so a change in what the harness sends fails the test. -
Evaluation.
europacore.evaluationscores an agent against an eval set (JSON). Each case declares the tool calls it expects (exact,in_orderorany_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, andthreshold=sets the bar per metric. -
Reproducible runs.
frozen_clockandsequential_idsfix every timestamp and generated id europacore records: run, turn, event and pause ids, checkpoint times and the prompt'scurrent_date. They are process-wide for thewithblock.
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 thatBedrockConfigknows, and Bedrockanthropic.ids, use Bedrock.gpt-*ando*names use OpenAI's Responses API.openai.ids use GPT on Bedrock.- A prefix such as
bedrock/oropenai/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'sCancelToken(Bedrock and OpenAI do, for non-streaming calls too: they go over the streaming transport). After the harness'sstream_idle_timeoutfinds a model stalled before its first chunk, calls start at the next model forstall_cooldownseconds (default 60; 0 turns it off). - Reported attributes. It reports the smallest context window of its models.
served_bynames 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 editargsand returns None to run the tool, or a value that becomes its result.after(name, args, result)returns None or a replacement result.ToolHooksfailures.aftersees the full result before it is truncated and stored, and also a failed call's error result (after_errors=Falseskips those). A hook that raises fails closed by default:beforeblocks the call andafterwithholds 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:
PromptRegistry(template_dirs=[...])EUROPACORE_TEMPLATE_DIRS(separated byos.pathsep)- the library's own templates (
europacore/prompts/templates, which holdsguardian_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, andloop.iterationper step. A delegation is one trace. They carrygen_ai.operation.name,gen_ai.agent.name,gen_ai.tool.name,gen_ai.provider.name,gen_ai.request.modelandgen_ai.usage.*. Other attributes use thetelemetry_attr_prefix. - Metrics.
gen_ai.client.token.usage(bygen_ai.token.type) andgen_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=0turns 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.PermissionGuardrailis opt-in and allows anything no rule names. Forexec_command, passexec_allowlist=default_exec_allowlist()(the spec optioninclude_exec_defaultsdoes this): a command then runs unasked only when it is an argv (a listcmd, noshell=True, notsh -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-onlygit, 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,treeandprintfare 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 optionexec_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 coreexec_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 toGuardianGuardrail. - Sandboxing is your choice. A tool that declares
sandbox_requiredstill runs in-process, with a warning, unless you passrequire_sandbox_backend=True. There is no default sandbox provider:localruns commands on this host with the worker's environment and is not a security boundary, so choose it explicitly, and uselambda_microvmfor 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 thetelemetryextra 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 orpurge_spooled_objects(store, prefix, days, keep=...), keeping the keys paused sessions reference. Results spooled throughToolResultSpoolare stored with credentials redacted;ImageSpoolresolves 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
stateis 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 aworkspace_idand 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:
- The harness checkpoints
status="paused". - It returns a
HarnessResultwithpaused=True. - 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}/canceland/steeract on it, andif_busy(reject,steer,queue) decides what a second turn does while one is active. - Resumable streams. Every event has a per-session
seq; reconnect withLast-Event-IDto replay what you missed. Token deltas are best-effort; the step'sassistant-messagealways carries the full text. - Identity from the token. An
Authenticatorreturns thePrincipal(workspace_id, user_id)that owns sessions; another principal's session answers 404. - Decisions are bound to their pause.
POST /v1/sessions/{id}/resumerequirespending_tool_call_id; a stale one gets 409pause_mismatchwith the current pause. - Tool credentials.
create_app(..., credentials=service)puts each principal's credentials onSessionContext.credentials(pass it toAgentHarness(credentials=)) and serves/v1/credentialsand 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, orPostgresSessionIndex(theserver_sessionstables, 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, fromServiceProvider(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
stateis 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_memorylets the model search, andsave_memory(withwritable=True) lets it store a fact. - Preload.
MemoryPreloadGuardrailadds 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 agentspawn_agent,send_message,wait_agent,interrupt_agent,list_agentsandrequest_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 callrun.cap_harness(harness)(orChildBudget.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 raisesAutonomousInfrastructureError(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 atemporalioclientsteering: optional; anything withsteer(run_id, message, *, command_id)andcancel(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_childandinterrupt_childsignals.
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 codelocal: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_requiredthemselves. On thelocalprovider they do not, so withrequire_sandbox_backend=Truethey are refused, which is the point:localruns commands on this host. - Commands.
exec_commandtakescmdas the permission rules read it: an argv list runs as given, and a shell line (a string) is refused unlesssandbox_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=Falseleaves outwrite_fileandedit_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. Calldestroy()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 thelocalprovider 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| europacore-0.1.0.tar.gz | 624.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.