This release is a pre-release and may not be stable for production use.
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) |
claude-code |
europacore.claude_code: Claude Code sessions served next to coder by the coding server, running Claude Code's own agent loop through the Claude Agent SDK (docs/claude-code.md) |
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) |
graph |
europacore.graph: FlowScript flows (.flow) compiled onto pydantic-graph and run by FlowRunner — pauses and resumes, sub-flows, background work, harness nodes on AgentHarness, flow and secret stores; with server, the /v1/graph routes and the designer at /graph/ui |
images |
Pillow, so images a tool returns (read_file on a screenshot) are scaled to at most 1568 px and 750 KB before they are sent (europacore.utils.images.fit_image); without it they are sent as they are, up to 3.75 MB |
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 tool can also carry rules on when and how to use it, as prompt_guidelines (a tuple of strings). The harness lists every loaded tool's rules once, under "Using the tools:", after the system prompt, so a rule only reaches the model when its tool is loaded. @function_tool(prompt_guidelines=[...]) (below) takes them too.
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) |
tools=[SubagentTool(name, factory, description=..., input_schema=...)] |
One sub-agent as its own named, typed tool, run like a delegated child (budget, depth, cancel, resumable pauses). europacore.graph.flow_tool builds one for a flow |
|
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 |
tool-call-delta |
A tool call's argument text as the provider streams it (stream_tokens=True; data.index, data.name on the first chunk, data.arguments); 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) |
prerequisite_nudge, required_tools_failed |
A flow's harness node answered before calling its required_tools and was nudged, or gave up after max_terminal_attempts |
prerequisite_violation |
A flow's harness node called a tool before its tool_prerequisites |
validation_required |
The flow designer's assistant tried to finish before validate_syntax passed |
hook_message |
A coding agent's command hook said something for the user (data.event, data.message: its systemMessage, an error, or why it was not honoured) |
design_findings |
The coding agent's design gate found blocking findings its change introduced (data.findings); data.unresolved is true when it stopped sending the agent back (its rounds are used up, or a round fixed nothing) |
sandbox.ready |
A cloud session's checkout is in a new sandbox (data.sandbox_id, data.head; data.restored_from: the work-in-progress snapshot it was restored from) |
sandbox.lost |
A cloud session's sandbox was lost (data.reason); the next command starts a new one from data.snapshot |
snapshot.pushed |
A cloud session's work was pushed to its work-in-progress ref (data.ref, data.sha, data.files_changed) |
snapshot.failed |
Pushing a cloud session's work failed (data.error); the next snapshot retries |
claude_config_warnings |
Once per session: what in the repository's or user's Claude Code configuration the coding agent does not honour (data.warnings) |
flow-run-start, flow-run-end |
A flow turn (europacore.graph) begins and ends (data.status) |
flow-node-start, flow-node-end |
A flow node is about to run / ran (name is the node; data.status, data.next, data.result, a size-bounded data.state) |
flow-route |
A node was skipped by enabled_when or a rejected approval, or a wait node received its event |
flow-message |
The flow showed the user a message (data.message) |
flow-paused |
An approval gate paused the run before a node (data.prompt) |
flow-cutoff |
The turn hit max_nodes_per_turn |
flow-background-start, flow-background-end |
Background children started / were collected |
flow-fork-start, flow-fork-end |
A parallel or map node's children started / all finished |
flow-node-retry |
A node with a retry policy failed and runs again |
flow-field-delta |
A harness node's streamable_output_field as the model writes it (data.field, data.delta); live only, never journaled |
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_BASE_URL / PROMPTSTORE_TELEMETRY_KEY set (a pst_… key, which is scoped to one workspace; PROMPTSTORE_URL is still accepted for the URL), 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. The run span carries the run's session_id, which PromptStore shows as the trace's Conversation, so a session's runs can be found together. A flow session is one conversation, a single row in PromptStore: a harness.run root named after the flow, with ids derived from the session id so every turn (in any process) reaches it; a loop.iteration per turn, a hitl.pause while the flow waits on the person, and the root closes when the flow completes. Under a turn, a composition.call span per node (content: what it was given and the fields it wrote), and under a node the run of any harness it starts (content: the user message in, the structured output or text out). A nested run's own session id goes in europacore.session.id, so only the root names the conversation.
europacore serve traces every run the same way: install europacore[server,telemetry] and set the two variables in the server's environment, or put them in a .env file and pass --env-file .env (variables already set in the shell win). At startup it logs PromptStore tracing on (<url>), or why tracing is off, and on shutdown it flushes queued spans after draining its runs. An application serving its own FastAPI app can do the same with telemetry.ensure_configured(), telemetry.status() and telemetry.flush().
A script loads a .env itself with europacore.load_env_file(missing_ok=True) before its first run; the examples do, so poetry run python examples/<name>.py traces too.
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_BASE_URL, PROMPTSTORE_TELEMETRY_KEY |
Where PromptStore telemetry goes, and its pst_… key (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. docs/server-settings.md covers running one: flags, authentication, persistence, workspaces, and the coding server's coder and claude-code agents. 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). - Claude Code sessions. With the
claude-codeextra,europacore serve europacore.coding:agentsalso servesclaude-code: Claude Code running its own agent loop (theclaudeCLI, through the Claude Agent SDK) in the same directories ascoder. Its sessions stream, pause for approvals, steer, cancel and fork like any other agent's, so a client switches between both kinds without knowing which is which. See docs/claude-code.md.
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.
Flows (europacore.graph)
A declarative layer over the harness for conversations and multi-agent work: a flow is a graph of nodes — ask the user, show a message, collect a form, route on a CEL condition, transform state, call a service or an HTTP endpoint, fire a webhook, make one structured model call, run a multi-turn model loop, run an autonomous harness (a full AgentHarness with tools and guardrails), nest a sub-flow, start work in the background and collect it later, run children side by side (parallel) or once per item of a list (map) and continue when all are done. Any node can be retried with a backoff (retry, retry_delay) and bounded per attempt (time_limit). Flows are written in FlowScript (.flow, see docs/flowscript.md) or built in the designer, and compile to a FlowDefinition.
from europacore.graph import FlowDefinition, FlowRunner, FlowServices, DefaultLLMFactory, compile_flow
flow = FlowDefinition.model_validate(compile_flow(open("triage.flow").read()))
runner = FlowRunner(flow, services=FlowServices(llm_factory=DefaultLLMFactory(models={"small": "claude-haiku-4-5", "large": "claude-sonnet-4-5"})))
first = await runner.start(session_id="s1") # runs to the first question
second = await runner.step("s1", user_message="It drips all night")
print(second.status, second.message, second.state.fields)
- One loop, one trace. A flow run is a journaled, checkpointed run of a session, exactly like a harness run (
ajournaled_run); every LLM node runs on the one harness in a child session (<session>::sub::<node>), capped by what is left of the flow's budget (FlowLimits), cancelled with it, and journaled with it. There is no second agent loop: routing is CEL on state, never a model's choice. - Pauses. A node that waits for the user (
ask,form,loop) pauses the run (flow_input); any node can carry an approval gate (requires_approval trueor a CEL expression →flow_approval, approved, remembered for the session, or rejected ontoon_reject); awaitnode pauses until an external event is delivered through a resume (flow_waiting); a harness node's own pause — an approval, a credential, a question inside a sub-flow it delegated to — bubbles up assubagent_approvalandrunner.resume(session_id, pending_tool_call_id=..., approve=...)re-enters the child. Snapshots are saved after every node, so a run resumes at the node it parked on. - A flow as an agent's tool.
flow_tool(QUOTE_FLOW, name="get_quote", services=svcs, inputs=["product", "qty"])wraps a flow (aFlowDefinition, FlowScript source, orflow_id=loaded throughservices.flow_resolver) as a tool with the flow's own name and typed arguments (the chosenstate_schemafields, plus an optionalmessage);AgentHarness(llm, tools=[search, quote], checkpointer=cp)lists it like any other. The flow runs as a delegated child: a question, gate or wait inside it pauses the agent's run assubagent_approval, andagent.resume(session_id, approve=True, instruction="blue")answers it and splices the flow's result into the tool call. - Node types. The built-in set is generic; an application registers its own (
NodeTypewith its FlowScript syntax,NodeTypeRegistry.register) and both the language and the engine know them. Every application choice — models, service callables, agents, webhooks, HTTP, memory, secrets — is injected throughFlowServices. - Rails.
FlowLimits(tokens, cost, seconds, nodes per turn, concurrent agents, depth); harness nodes takeguardrailsspecs (build_guardrail_from_spec),required_tools/tool_prerequisites, per-toolrequires_approval;validate_flowlints a definition (dead transitions, unknown CEL roots, pauses in the background or in a fork, mistyped defaults);strict_state truemakes the declared state types binding (FlowStateTypeError). - Stores. Flow definitions with versions (
InMemoryFlowStore,SqliteFlowStore,PostgresFlowStore, schema v12) and per-flow secrets ({secret:NAME}in tool config;InMemoryFlowSecretStore,SqliteFlowSecretStore,PostgresFlowSecretStore, encrypted with thecredentialsextra whenEUROPACORE_CREDENTIALS_KEYis set, plaintext with a warning otherwise). - Serving.
register_flow(registry, "quote", flow, services=...)registers a flow as an agent: sessions, runs, SSE, resume, steer (it reaches the harness node running at that moment) and cancel work unchanged.--serve-flowsserves every flow saved in the designer the same way, each as the agent named after it ("Tap Triage" istap-triage, soec -a tap-triage), with no code: a saved, renamed or deleted flow shows on the next request, and a session paused mid-flow stays on the version it paused on (europacore.graph.SavedFlows, anAgentRegistry.add_sourcesource).europacore serve myapp.agents:registry --graph [myapp.graph:services] --graph-store flows.db --uiadds the/v1/graphroutes (flows, FlowScript compile/decompile, node types, test runs, run history, the assistant, secrets;docs/server-api.md§3.8) and the designer at/graph/ui(the ready line'sui_urlcarries the token in the fragment);make graphdoes this for the coding agents on port 8766 (GRAPH_PORT=), besidemake serveon 8765, withGRAPH_STORE=for the flow store andGRAPH_SERVICES=module:attrfor models. The designer is a Vite app inui/graph-designer, built into the wheel byscripts/build_ui.py. - Examples.
examples/graph_flow.pyruns a four-node FlowScript flow offline with a scripted model.examples/nba_cards.flowis a complete flow that uses every built-in node: a Next Best Action card studio modelled on cardbuilder, with a brief form, the library loaded inparallel, background drafting with deterministic checks and a rewrite, a review loop behind an approval gate, a harness over REST tools that delegates to a sub-agent and a sub-flow (whose own gate bubbles up), amapthat assembles each Card, a wait for legal sign-off and memory;examples/nba_cards_flow.pyruns it offline through every pause kind and serves it as the agentnba_cards(europacore serve examples.nba_cards_flow:registry --graph examples.nba_cards_flow:graph --ui).make exampleslists the servable examples and serves the one you pick onMODEL(defaultgpt-6.1-sol).examples/table_booking.flowis the small one to chat with: a bistro concierge (asaywith quick replies, aloopthat collects the booking, acallrouted on the diary's answer, an approval gate before anything is booked);examples/table_booking_flow.pyruns it offline on a stand-in model and serves it forecor the designer (europacore serve examples.table_booking_flow:registry --graph examples.table_booking_flow:graph --ui;EUROPACORE_MODELgoes live); the runbook is docs/examples/table-booking.md.examples/flow_tool_agent.pymakes a flow with a branch a tool of a plainAgentHarness(flow_tool): a refund flow routes small amounts straight to payments and larger ones through an approval gate, which pauses the agent assubagent_approvaluntilresume(approve=...)picks the paid or theon_rejectbranch; it runs offline, andmake examples EXAMPLE=refundsserves it as the agentrefundsfor ec, with the Refund flow in the designer (EUROPACORE_MODELgoes live, else a rule-based stand-in answers). Design notes:docs/graph.md.
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.
Coding agent (europacore.coding)
build_coding_harness(llm, root) is an AgentHarness set up to work on a checkout or git worktree on this machine: read, search, edit and run commands under the OS sandbox, the project's AGENTS.md/CLAUDE.md and skills, planning, an explore sub-agent, and (with parallel_tasks=True) coders in worktrees of their own. europacore serve europacore.coding:agents serves it (see europacore.coding.server for its environment variables).
Permission modes. A session runs in one of Claude Code's permission modes, switchable while it runs (set_permission_mode(harness, session_id, mode), or the server's permission_mode):
| Mode | What runs without asking |
|---|---|
acceptEdits (default) |
Edits and confined commands; operations that lose work (git reset --hard, rm -r, pushes) ask |
default |
Read-only calls and what a rule allows by name; every other change asks |
plan |
Read-only calls only. The agent investigates, then calls exit_plan_mode with its plan; approving it switches back and the agent implements |
dontAsk |
As default, but what would ask is denied: for unattended runs |
auto |
As acceptEdits; what would ask goes to a guardian model (auto_mode_llm, else the configured adjudicator, else the agent's model) before the user |
bypassPermissions |
Everything deny rules allow, except widening the OS sandbox |
from europacore.agents import InMemoryHarnessCheckpointer, set_permission_mode
from europacore.coding import build_coding_harness
agent = build_coding_harness(llm, "/src/app", checkpointer=InMemoryHarnessCheckpointer())
set_permission_mode(agent, "s1", "plan")
paused = agent(question="Make the importer resumable", session_id="s1") # pauses on exit_plan_mode
print(paused.pause["arguments"]["plan"])
done = agent.resume("s1", approve=True, pending_tool_call_id=paused.pause["pending_tool_call_id"])
Each result lists what the guardrail turned down (result.permission_denials).
Pull requests. The agent's commands never reach the user's ssh agent or gh login, so it can't push. Its work leaves the machine through create_pull_request, which asks first. The tool pushes the committed branch and opens a pull request into the branch the worktree was made from (else the remote's default branch). Calling it again pushes new commits to the open pull request. It publishes with a GitHub App, the same one the fleet uses (EUROPACORE_GITHUB_APP_ID and its private key; docs/fleet.md, "Publishing"), or with build_coding_harness(pull_requests=GitHubPublisher(app)). The App's token covers that one repository and is never given to the agent. Without an App there is no tool.
A repository set up for Claude Code. The coding agent reads the same configuration Claude Code does:
- Instructions:
CLAUDE.md(orAGENTS.md),.claude/CLAUDE.md,CLAUDE.local.md, the user's~/.claude/CLAUDE.md, with their@pathimports (four hops; a repository's files import only from inside it), and.claude/rules/*.md(those withoutpaths). - Permissions:
permissions.allow,askanddenyin.claude/settings.json,.claude/settings.local.jsonand~/.claude/settings.json($CLAUDE_CONFIG_DIR), with Claude Code's rule syntax:Bash(git push:*),Read(./.env),Edit(src/**),WebFetch(domain:example.com),Agent(name),mcp__server__tool; alsoadditionalDirectoriesanddefaultMode. Rules that cannot be expressed are reported. - Hooks: the settings'
hooks(with.europacore/hooks.toml), with Claude Code's protocol: exit codes,permissionDecision(allow/ask/deny),updatedInput,additionalContext,updatedToolOutput,continue,systemMessage, onPreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,Notification,Stop,SessionStart,UserPromptSubmit,SubagentStart,SubagentStop,PreCompact,PostCompactandSessionEnd. Matching hooks run in parallel;asynchooks report at the next model call;httphooks (from your own settings) are POSTed the payload;$CLAUDE_ENV_FILEandtranscript_pathwork as in Claude Code. A hook written for Claude Code sees Claude Code's tool names (Bash,Edit…) and input fields. - Sub-agents:
.claude/agents/*.md, each a sub-agent the coding agent can delegate to, with its own prompt, tools (tools: Read, Grep, Bash), model and turn limit, under the agent's permission rules. - Slash commands:
.claude/commands/*.md; a prompt/fix 123runs the command's prompt with its arguments ($ARGUMENTS,$0…). Your own commands may allow tools for their turn (allowed-tools);!`git status`in a command puts that command's output there. - MCP servers:
.mcp.jsonand~/.claude.json. A repository's server runs once you enable it (enabledMcpjsonServersin your settings, or approving the agent'sconnect_mcp_server"always"); its tools (mcp__server__tool) ask before each call until a rule allows them. The server lists them atGET /v1/sessions/{sid}/mcp.
The coding tools also read Jupyter notebooks as cells and edit one cell at a time (notebook_edit), and web_fetch can take a prompt that a small model answers from the page (web_fetch_llm=, or the server's EUROPACORE_FAST_MODEL), so the page itself never reaches the agent's context.
Trust. A cloned repository's own settings may narrow what the agent does, not widen it, until you trust the repository: its deny and ask rules and its hooks (which run under the OS sandbox) always apply, but its allow rules, additionalDirectories and a widening defaultMode are ignored, with a warning (the claude_config_warnings event). Your own files (~/.claude/settings.json, and a settings.local.json git does not track) are trusted, and your own hooks run on the host. Trust a repository with europacore trust [PATH] (or europacore.coding.trust_project(path), or build_coding_harness(trust_project=True)); it is kept per main checkout in ~/.europacore/approvals/trusted.json. Leave the user's ~/.claude out with build_coding_harness(claude_user_config=False) (the coding server's EUROPACORE_CLAUDE_USER_CONFIG=0), as a server serving other people should.
Fleet (europacore.fleet)
A fleet runs many coding tasks over one repository, each in its own worktree and harness session, and judges each by scorers run on an immutable snapshot of its work. A spec lists the tasks, their dependencies, the profile (an agent in .claude/agents) that works on each, the paths it may write, and its scorers (a command that must exit 0, the design checks, or a model judge):
europacore fleet run tasks.toml --check --live # validate, one tiny call per model, nothing created
europacore fleet run tasks.toml # create and supervise until done, or until it waits on you
europacore fleet status RUN # each task, whose turn it is, the exact next commands
europacore fleet answer RUN TASK --approve # also retry / steer / cancel / resume; all idempotent by --request-id
The design, in brief:
- The ledger holds only intent (tasks, attempts, commands, receipts); git holds the code and each session's journal holds what its worker did, and status is derived from them when read.
- One supervisor per run, holding the journal's lease on
fleet::<run>, applies every command. - A retry continues the task's session with its scorers' findings, and a crash resumes it in place, so nothing is redone.
- Workers never push or open pull requests. A completed run is published (
integration.publish = "branch" | "pr") by the fleet's GitHub App, with a token for that one repository, from a laptop or a server alike. - Signed webhooks start runs from a template in the repository, then steer, approve and cancel them (
europacore serve --fleet-triggers): a labelled issue becomes a run, and one started by someone untrusted waits for a/fleet approve.
See docs/fleet.md.
Design checks (europacore.design)
A change may not make a repository's design worse. The checks are deterministic and run base against head on the files a change touched, so a repository's existing debt never fails a change and nothing needs a baseline to start:
- file and function size;
- complexity, counted as ruff's C901 counts it;
- import layers (forbidden edges and leaf packages) and import cycles;
- optional dependencies imported at module top level;
- function bodies copied with their names changed;
- inheritance used to reuse implementation, broad
excepts that swallow errors silently, and undocumented exports; - guessing: code that hedges over a fact it could have looked up (
d.get("userId") or d.get("user_id"),*argsit never reads,getattrprobes, fallback chains).
The rules live in .europacore/design.toml. A documented trade-off is listed under accepted, with its reason.
europacore design check --base main # exit 2 when the change adds a blocking finding: a hook, a CI step or a verify line
europacore design debt # every finding in the repository as it stands
europacore review --fix --publish # a whole-repository review, then a fleet run that fixes it and opens a pull request
build_coding_harness(..., design_gate=True) runs them after each edit and before the agent may finish. A fleet scores with them through { kind = "design" }, and { kind = "judge", rubric = "design" } asks a model for what no check can see. See docs/design.md.
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.4.0a1
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.4.0a1.tar.gz | 2.5 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| europacore-0.4.0a1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 5.2 MB
Release files / europacore-0.4.0a1.tar.gz
| Download URL | europacore-0.4.0a1.tar.gz |
|---|---|
| Size | 2.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
67fba223ab402d635bd05ad21c274966b63e27202119e5d62fd82b8e768dc2b4
|
|
BLAKE2b-256 checksum How to use checksums |
677a9dfe7beb2e7ade3647a3bb7b5203a22871095925a6012cdd2f221ba72e39
|
| 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.4.0a1-py3-none-any.whl
| Download URL | europacore-0.4.0a1-py3-none-any.whl |
|---|---|
| Size | 2.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
85fedfd4c81325229c20489fe8b418b4b1d90d05fcffb4b921518c0cc2efd905
|
|
BLAKE2b-256 checksum How to use checksums |
2a3a5c12831d520fdbdefbe30d4286779d7985e3b033d24a9f8494faa1460b30
|
| 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.4.0a1
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.