Reusable, domain-agnostic runtime for tool-using LLM agents.
agent-core provides the orchestration layer that applications can compose with
their own prompts, tools, policy rules, storage and domain memory. It is meant
to be the generic runtime under an agent application, not a domain-specific
agent package.
Version 0.4.0 is an alpha release. The runtime is usable and tested, but the
public API may still evolve before 1.0.0.
- Tool-calling assistant loop with pending tool result and resume support
- Three run modes:
direct,investigate, anddeep_investigate - Bounded investigation state with auditable facts, gaps, actions and confidence
- Run trace persistence with prompt snapshots, events and compact summaries
- Generic structured task runner for bounded JSON-producing subtasks
- Tool registry and small tool protocol
- Persisted, idempotent autonomous run lifecycle
- Optional conversation threads and memory lifecycle helpers
- Policy guardrail entry points for tool execution
- Domain hooks for application-specific prompt and memory extensions
- Provider adapters for OpenAI, Azure OpenAI and Azure Anthropic backends
- Exact provider token usage, per-call telemetry and run-level usage summaries
- Optional run-level LLM budgets covering main, reflection, finalization and memory calls
- Optional provider-window context planning with atomic history compaction
- Optional lossless tool-result artifacts with bounded on-demand rereads
- Provider-enforced JSON Schema contracts for structured task final outputs
- OpenAI/Azure request normalization and adaptive retry handling
- Typed Python package marker (
py.typed)
agent-core intentionally does not ship domain-specific prompts, checklists,
tools, reports or integrations. Domain packages should depend on this runtime
and provide their own behavior through:
- application prompts
DomainHooks- registered tools
- policy rules
- external storage or domain memory
This keeps the repository usable for many application domains instead of specializing it for one workflow.
From a tagged Git repository:
python -m pip install "agent-core @ git+https://github.com/maxDcb/agent-core.git@v0.4.0"For local development:
python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pytestThe repository includes a minimal working example with two small tools:
get_current_time and echo.
python -m venv .venv
.venv/bin/python -m pip install -e .
cp .env.example .envSet OPENAI_API_KEY in .env, then run:
.venv/bin/python examples/quickstart.pyUse a custom one-shot prompt:
.venv/bin/python examples/quickstart.py "Echo hello through the echo tool."Or run a small REPL:
.venv/bin/python examples/quickstart.py --interactiveSee examples/README.md for the pending tool result and resume example.
For a complete application built on the supported public API, see the official maxDcb/agent-core-exemple repository.
RunContextcarries the namespace, run identity, execution scope, correlation and caller-owned application context.AgentRunServiceexecutes and persists autonomous headless runs without a conversation or session manager.ConversationAgentoptionally adds thread memory and pending-tool resumes.ToolRegistrystores application tools and exposes model-facing tool definitions.PolicyEngineauthorizes tool calls before execution.RunStorepersists run state;JsonFileRunStoreis the reference local filesystem implementation with thread and process execution locks.- Structured runs persist lossless checkpoints and can resume after process interruption without replaying completed tools.
- Ambiguous tool effects fail closed until the host supplies a reconciled result.
RunOptionsselects the execution mode and investigation budgets.DomainHookslet applications add domain prompt blocks and memory payloads without adding domain logic to the core package.
The default mode is direct, which preserves the ordinary assistant/tool loop.
For multi-step work, investigate and deep_investigate add bounded planning,
reflection, decision and optional final critique phases while storing only
auditable artifacts, not raw chain-of-thought.
Investigation modes are conversation modes by default: their user-visible
answer is text. Their planning, reflection, decision and critique phases use
internal JSON synthesis. If a caller needs a structured final answer, pass
final_output_mode="json_schema" with a StructuredOutputContract; the
investigation still runs normally, then a final no-tool turn renders the answer
through the provider-enforced JSON Schema contract.
from agent_core import RunContext, RunOptions
result = conversation_agent.execute_turn(
thread_id="incident-1",
context=RunContext(namespace_id="workspace-1", thread_id="incident-1"),
user_input="Investigate this issue using the available tools.",
options=RunOptions.investigate(),
)See docs/investigation_modes.md for mode details and metadata returned by completed investigation runs.
StructuredTaskRunner runs a bounded tool-using subtask. Without an
output_contract, it returns raw text and the caller owns any parsing. With an
output_contract, the provider JSON Schema contract is enforced only on the
final no-tool output, after any tool investigation is complete. Providers that
cannot enforce the contract fail the task instead of silently downgrading to
loose JSON mode. Provider enforcement is not trusted as the application
boundary: agent-core also validates the parsed object locally with JSON Schema
Draft 2020-12 and format checking before returning success. The contract schema
itself is checked when StructuredOutputContract is constructed.
An invalid structured result fails explicitly. agent-core does not silently coerce values, remove extra fields, invent missing fields, or retry. Validation metadata contains the contract name, validation phase, validator, instance path, schema path, and a safe message; payload values are excluded so secrets are not copied into diagnostics. The original raw completion remains available on the failed task result for controlled recovery or audit by the caller.
from agent_core import StructuredOutputContract, StructuredTaskSpec
spec = StructuredTaskSpec(
task_id="workspace_summary",
system_prompt="Return JSON only.",
objective="Summarize the provided workspace context.",
allowed_tools=["search_code"],
output_contract=StructuredOutputContract(
name="workspace_summary",
strict=True,
schema={
"type": "object",
"required": ["summary"],
"additionalProperties": False,
"properties": {"summary": {"type": "string"}},
},
),
)
from agent_core import AgentRunService, JsonFileRunStore, RunContext
service = AgentRunService(
settings=settings,
provider=provider,
tool_registry=tool_registry,
policy_engine=policy_engine,
run_store=JsonFileRunStore(".agent-core/runs"),
)
result = service.execute(
spec=spec,
context=RunContext(namespace_id="workspace-1", parent_id="job-1"),
)LLMBudget applies one shared budget to every model call made by a run,
including assistant steps, internal investigation synthesis, finalization and
post-turn memory refresh. No budget is applied unless one is configured, so
existing integrations preserve their current behavior.
Set a default on CoreSettings, or override it for one conversation or
structured task through RunOptions.llm_budget or
StructuredTaskSpec.llm_budget:
from agent_core import LLMBudget, RunOptions
budget = LLMBudget(
max_calls=12,
max_input_tokens=80_000,
max_output_tokens=12_000,
max_total_tokens=90_000,
max_duration_seconds=180,
mode="enforce",
)
options = RunOptions.investigate(llm_budget=budget)mode="observe" records limit violations without blocking calls or changing
provider output limits. In enforcement mode, a call that would exceed the call,
input-token, total-token or cumulative provider-call-duration limit is rejected
before provider invocation. The duration limit is checked between calls; it
does not cancel a provider request already in flight. The controller also caps
the provider's per-call maximum output to the remaining output/total budget
when the provider accepts call options.
Completed and pending results expose llm_budget and llm_budget_usage in
their metadata. Pending conversation state and structured-task checkpoints
persist the same usage so resume cannot reset a budget. Exact provider token
usage replaces pre-call estimates when available; otherwise the controller
retains its conservative estimate.
LLMContextPolicy plans the complete provider input immediately before every
LLM call. Unlike history-only compaction, it counts system prompts, conversation
messages, tool definitions and structured response schemas, then reserves room
for the model output and a safety margin.
Set a default on CoreSettings, or override it through
RunOptions.llm_context_policy or StructuredTaskSpec.llm_context_policy:
from agent_core import LLMContextPolicy, RunOptions
context_policy = LLMContextPolicy(
max_context_tokens=128_000,
reserved_output_tokens=8_192,
safety_margin_tokens=512,
mode="enforce",
)
options = RunOptions.investigate(llm_context_policy=context_policy)Enforcement preserves all system messages and the complete current turn,
including each assistant tool call with all matching tool responses. If the
request is too large, it removes only complete older history groups, newest
first. If the mandatory context still cannot fit, the call is rejected before
provider invocation with LLMContextOverflowError. mode="observe" records
the overflow without changing the request.
Results expose llm_context_policy and llm_context_usage; pending turns and
structured checkpoints persist the same planner usage across resume. Token
planning uses a deterministic character heuristic, so the safety margin should
cover tokenizer variance. Output reservation is provider-enforced only when
the provider supports LLMCallOptions.max_output_tokens.
ToolArtifactPolicy optionally stores every completed application-tool result
outside the transcript. Recent bounded results remain hot for immediate model
continuity; older or individually large results are represented by an opaque
artifact descriptor. The runtime exposes agent_core_read_artifact whenever
the policy is active so the model can read bounded chunks on demand.
from pathlib import Path
from agent_core import CoreSettings, ToolArtifactPolicy
settings = CoreSettings(
artifacts_directory=Path("./artifacts"),
tool_artifact_policy=ToolArtifactPolicy(
hot_context_bytes=64 * 1024,
max_inline_result_bytes=32 * 1024,
max_read_bytes=16 * 1024,
max_reads_per_run=20,
max_total_read_bytes=256 * 1024,
),
)The policy can also be overridden with RunOptions.tool_artifact_policy or
StructuredTaskSpec.tool_artifact_policy. JsonFileArtifactStore is the
default; hosts may inject an ArtifactStore into AgentRunService for remote
or transactional storage. Artifact reads are namespace-scoped, never accept a
filesystem path, have their own call/byte limits, and do not consume the
application tool-call budget. Pending conversation state and structured
checkpoint schema version 5 persist descriptors and artifact usage rather than
copying the full result.
The feature is disabled by default. JsonFileArtifactStore stores plaintext
UTF-8 files and is intended for trusted local deployments; production hosts
handling credentials or browser session data should inject an encrypted store
with explicit access control and retention.
Tools that need an external asynchronous result can return:
from agent_core.spi import ToolResult
return ToolResult.pending_result(
"Waiting for command output.",
metadata={"command_id": "cmd-123"},
)The application stores the returned pending_id, then resumes the turn when the
external result arrives:
completed = conversation_agent.resume(
namespace_id="workspace-1",
run_id=pending.run_id,
pending_id=pending.metadata["pending_id"],
tool_content="command output",
)- Build
CoreSettingsfrom your application config. - Create a
BaseLLMProviderimplementation or configure a bundled provider through the factories exported byagent_core.spi. - Register tools in
ToolRegistry. - Instantiate a
RunStoreandPolicyEngine. - Build
AgentRunServiceand execute aStructuredTaskSpecwith an explicitRunContext. - Only if conversation is needed, add
SessionManager,AgentOrchestratorandConversationAgent.
See docs/run_architecture.md for identifier, context, idempotence and pipeline ownership rules.
The supported API is intentionally split by responsibility. The package root contains the autonomous run engine:
from agent_core import (
AgentRunResult,
AgentRunService,
AgentRunState,
AgentRunMode,
CoreSettings,
ExecutionContext,
ExecutionScope,
JsonFileRunStore,
LLMBudget,
LLMBudgetExceededError,
LLMBudgetUsage,
LLMContextOverflowError,
LLMContextPlan,
LLMContextPolicy,
LLMContextUsage,
RunContext,
RunOptions,
RunStore,
StructuredOutputContract,
StructuredTaskResult,
StructuredTaskRunner,
StructuredTaskSpec,
)Provider, policy, domain and tool extension contracts live in agent_core.spi:
from agent_core.spi import (
BaseLLMProvider,
DomainHooks,
LLMProviderError,
PolicyEngine,
ToolRegistry,
ToolResult,
build_tool_definition,
)Conversation and session support is optional and imported separately:
from agent_core.conversation import (
AgentOrchestrator,
ConversationAgent,
SessionManager,
SessionRepository,
)
from agent_core.observability import RunTrace, configure_logging, get_loggerOnly names exported by these four facades are covered by the public compatibility
contract. Direct imports from other agent_core.* modules are implementation
dependencies and may change without notice.
See docs/public_api.md for the ownership and compatibility rules of each facade.
.venv/bin/python -m ruff check agent_core tests examples
.venv/bin/python -m mypy agent_core
.venv/bin/python -m pytest
.venv/bin/python -m pytest --cov=agent_core --cov-report=term
.venv/bin/python -m buildThe coverage configuration measures branches and enforces a ratchetable global
minimum. Deterministic fault-injection tests use the chaos marker and can be
run separately with python -m pytest -m chaos. Property-based resilience
tests are part of the normal suite; longer diagnostic runs are scheduled in CI.
This repository should stay focused on the generic runtime. Domain packages should depend on it instead of adding their prompts, tools or reporting logic here.