Compile a declarative DAG of subagents into an orchestrated team on AWS Bedrock AgentCore.
Project description
Concursus
Compile a declarative DAG of subagents into a deployed, orchestrated team on AWS Bedrock AgentCore.
Concursus (Latin "a running-together / convergence") is cursus's agent-orchestration sibling. Where cursus compiles a pipeline DAG + configs into a SageMaker pipeline, Concursus compiles an AgentDAG + per-agent .agent.yaml manifests into (1) an AgentCore provisioning plan — one CreateAgentRuntime per agent — and (2) a supervisor that dispatches the agents in topological order, wires each agent's declared output into its dependents' input, and routes shared state through AgentCore Memory.
It is the coordinator AgentCore deliberately doesn't ship: AgentCore gives you transport (A2A), tool discovery (Gateway), microVM isolation, identity, memory, and hosting — but no scheduler, dependency graph, or supervisor. You declare a DAG of agents; Concursus provisions them and runs them.
Status: alpha. This release ships the declarative core (
AgentDAG+AgentManifest) and the offline compiler: the dependency resolver, the runtime builder, theOrchestrationAssembler(DAG + manifests → aProvisioningPlan), and the topologicalSupervisor— plus theplan/deploy/runCLI verbs. The compiler is pure-Python; boto3 stays behind the[agentcore]extra and is imported lazily only whendeploy --execute/run --executeactually calls AWS.
Installation
pip install concursus # declarative core (pure Python)
pip install "concursus[agentcore]" # + the AWS Bedrock AgentCore runtime binding (roadmap)
Requires Python 3.10+.
Quick start
Declare a team as an AgentDAG (nodes = agents, edges = data dependencies):
from concursus import AgentDAG
dag = AgentDAG()
for agent in ["ingest", "summarize", "critique", "format"]:
dag.add_node(agent)
dag.add_edge("ingest", "summarize")
dag.add_edge("summarize", "critique")
dag.add_edge("critique", "format")
dag.topological_sort() # ['ingest', 'summarize', 'critique', 'format'] <- dispatch order
dag.validate() # raises if the topology has a cycle
Describe each agent with an .agent.yaml manifest — its AgentCore binding + typed interface:
# summarize.agent.yaml
registry:
container_uri: 111122223333.dkr.ecr.us-east-1.amazonaws.com/summarize-agent:latest
role_arn: arn:aws:iam::111122223333:role/ConcursusAgentRuntimeRole
network_mode: PUBLIC # or VPC
protocol: HTTP # HTTP (/invocations) | MCP (/mcp) | A2A (/)
qualifier: DEFAULT
# ...or reuse an already-deployed agent:
# agent_runtime_arn: arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/summarize-xyz
contract:
inputs:
document: {type: string}
outputs: # required — the dependency resolver's type gate
summary: {type: string}
key_points: {type: array, items: {type: string}}
spec:
depends_on:
- {from: ingest.document, to: document}
from concursus import AgentManifest
m = AgentManifest.from_yaml("summarize.agent.yaml").validate()
m.protocol # 'HTTP'
m.output_schema # {'summary': {...}, 'key_points': {...}}
Or from the CLI:
concursus info # overview
concursus validate *.agent.yaml # validate manifests
concursus --version
Compile a plan (plan → deploy → run)
Point the compiler at your manifests. Edges are inferred from each manifest's depends_on
(or pass --dag ingest->summarize to set them explicitly). plan prints a JSON
ProvisioningPlan — a topological order, one create_agent_runtime entry per agent, and
the resolved producer→consumer wiring — without touching AWS:
concursus plan *.agent.yaml
from concursus import AgentDAG, AgentManifest, OrchestrationAssembler, Supervisor
manifests = {m.name: m for m in map(AgentManifest.from_yaml, paths)}
dag = AgentDAG()
for name in manifests:
dag.add_node(name)
dag.add_edge("ingest", "summarize").add_edge("summarize", "critique")
plan = OrchestrationAssembler(account="111122223333", region="us-east-1").assemble(dag, manifests)
plan.order # ['ingest', 'summarize', 'critique'] <- dispatch order
plan.to_dict() # JSON-serializable preview (what `concursus plan` prints)
deploy dry-runs what would be created (nothing imported); --execute provisions each agent
end-to-end — ensure its IAM execution role, build + push its image to ECR (when the plan carries a
placeholder URI), then CreateAgentRuntime — reusing an existing image or runtime ARN as-is. run
dry-runs the topological dispatch; --execute invokes the live runtimes, threading each output
into its dependents:
concursus deploy *.agent.yaml # dry-run: the role/image/create steps
concursus deploy *.agent.yaml --execute --source-dir . # + boto3 + docker: role → ECR image → create
concursus run *.agent.yaml --inputs '{"uri": "s3://doc"}' # dry-run the dispatch
concursus run *.agent.yaml --inputs @inputs.json --execute # live InvokeAgentRuntime
outputs = Supervisor(plan, manifests).run({"uri": "s3://doc"}) # {node_id: output_dict}
How it works (the compile target)
Concursus compiles AgentDAG + manifests through validate → resolve → provision → assemble, mapping cursus concepts onto AgentCore primitives:
| cursus | Concursus | AgentCore primitive |
|---|---|---|
PipelineDAG |
AgentDAG |
dispatch order (topological) |
.step.yaml |
.agent.yaml manifest |
container image + roleArn + protocol |
DependencyType enum |
output JSON Schema (mandatory) | the resolver's type gate |
PropertyReference (deferred) |
AgentRef (eager JSONPath) |
InvokeAgentRuntime response |
| step registration | agent registration | CreateAgentRuntime → ARN + V1 + DEFAULT endpoint |
PipelineAssembler → Pipeline |
OrchestrationAssembler → supervisor + plan |
BedrockAgentCoreApp supervisor |
| S3 artifact channels | shared run state | AgentCore Memory |
The supervisor dispatches agents in topological order, invokes each with InvokeAgentRuntime under one runtimeSessionId (session affinity → warm microVMs), extracts each producer's output by JSONPath and injects it into its consumers, and persists outputs to Memory so state survives the ephemeral microVMs.
Durable run state (the StateStore seam)
The supervisor threads every output through a StateStore — an append-only log of validated
outputs plus a derived {node: output} projection (the slipbox's single-source-of-truth /
derived-DB discipline). Three backends share one Protocol:
InProcessStateStore— the zero-dependency, offline default. Nothing new to install.MemoryStateStore— opt-in, AgentCore Memory-backed. Each validated output is one Blob event; a run resumes by replaying its event log, so it survives micro-VM teardown / mid-run crashes — the supervisor skips any node alreadycompleted(). boto3 is imported lazily (the[agentcore]extra); passrun --memory-id <id> [--actor-id <id>] --execute. For long-lived / standing loops, an optionalcheckpoint()compacts the log so a warm resume reads only O(events-since-the-last-checkpoint) (aCHECKPOINTsnapshot event + anepochtag, resumed via boundedEQUALS_TOfilters) instead of the whole session; the log stays the source of truth and resume falls back to the full rebuild when no checkpoint is present.FileVaultStateStore— opt-in, persistent on-disk (no AWS). Each record is written as a round-trip-exact markdown note under<vault>/runs/<session>/(two authoritative base64 JSON blobs —meta+payload— are the source of truth; everything else is a greppable display copy), so a run is durable and inspectable offline and resumes by reloading the notes. Notes are Abuse-SlipBox-conformant by default (P.A.R.A. tags, a derivedbuilding_block,folgezettel/lineageforming a per-run trail, a typed H1, a## Related Notessection) — they validate undercheck_note_format.pyand read as a genuine slipbox trail;slipbox_form=Falsegives a lean machine schema.concursus.build_run_dbmaterializes a derived, gitignored SQLite graph/index over the notes (metadata postings,consumesedges, the execution-address tree, a latest-validated projection view) — the notes stay canonical, the DB is disposable. Pure stdlib; passrun --vault <dir> --execute.
Each record also persists its resolved AgentRef edges (consumes), turning the log into a
queryable run graph (RunGraph: upstream/downstream, a structural validate, bounded
context_order). Supervisor.context(node) returns a node's transitive upstream outputs — shared
context as a query, not point-to-point wiring:
from concursus import Supervisor, InProcessStateStore
sup = Supervisor(plan, manifests, state_store=InProcessStateStore())
outputs = sup.run({"uri": "s3://doc"}) # {node_id: output_dict}
sup.context("critique") # {producer: output} for its transitive upstream
The governor (opt-in dynamic outer loop)
The compiler and supervisor are static: assemble freezes one ProvisioningPlan and
Supervisor.run executes it in a single forward pass. When you want a dynamic control loop —
standing "keep-the-lights-on" monitoring, replan-on-signal, program/portfolio rollups — the
governor subpackage wraps a bounded cycle around the compiler. The design is a dynamic
outer loop hosting freeze inner episodes: each round the governor forms a fresh frozen plan at
the compiler front and dispatches one new bounded Supervisor.run episode. It never reaches
inside a running supervisor, never mutates a frozen plan, and never turns the compiler into a
runtime governor — the compiler-not-runtime-governor identity holds.
It is entirely opt-in. The zero-config path stays static assemble → Supervisor.run; you
reach for the governor only when you want the cyclic driver. Like the reasoning tier, LangGraph
stays optional — the loop mirrors the DKSEngine template (lazy import, pure-Python fallback), so
everything imports and runs with no langgraph installed.
from concursus import GovernorLoop
# A bounded cycle: planner (assemble/recompile) -> router -> run_episode (one Supervisor.run)
# -> collect -> {replan | synthesize}. Terminates on frontier-exhaustion / stall / max_rounds
# / a hard step_cap. backend="auto" uses LangGraph if present, else the pure-Python driver.
loop = GovernorLoop(goal="triage-abuse-signal", manifests=manifests, max_rounds=8)
result = loop.run({"uri": "s3://signal"}) # GovernorResult: plan sequence + folded episode log
The subpackage is layered strictly outside the compiler (identity invariants INV-1..INV-5):
GovernorState— persistent outer-loop state: the sequence of frozen plan VALUEs byplan_version+ a pointer to the append-onlyStateStorelog (the sole executed-prefix anchor), never a mutable compiler plan.GovernorLoop— the fixed cyclic driver.plannerforms a fresh frozen plan each round (first viaplan_from_goal+assemble, later via monotonicrecompile);run_episodecallsSupervisor.runonce;collectfolds outputs into the log and re-derives the executed prefix fromstore.completed(). Durable dual resume (outerplan_versioncheckpoint + inner log replay) when backed by aMemoryStateStore.TrustLadderScheduler— therouter's per-decision matcher: matches each ready step to a standing agent (read-onlyAgentRegistry), reads its earned trust, and proposes a frontier (DISPATCH/ESCALATEL1→L3 /UNMATCHED) that feeds the nextrecompile— never mutating a frozen plan.AgentRegistry— the governor's process table: a read-only versioned view over the shippedDeployLedgeranswering "which standing agent, at which version, can do task X?" (the ledger answers content-identity only). Spawn/fork delegate to the shippedprovision_agent.DirectorCockpit— a read-only director view composing a briefing, an exception queue, and a runs monitor purely out ofrender_precedent_hub+Supervisor.summary()/.index().KTLODaemon— a standing monitor above the loop (monitor → triage → escalate → replan | close) that wakes on a liveEventSource+ drift and dispatches one fresh bounded episode per investigation.LAUNCH(one-shot drain) vsKTLO(standing,max_ticks-bounded) is a config, not two code paths.scope— theorg → portfolio → program → tasklayer above the single run: aScopeAddressstack, a cross-program programs index (the program-grain analogue of the runs-grain precedent hub), and a 1:Ndirector_leverage_view— all read-only projections over the per-run precedent notes.
These modules are now wired into the loop behind opt-in seams (identity-preserving; the
default GovernorLoop(...) with no scheduler= and deliberate=False is byte-for-byte today's
behavior):
routergates the frontier by earned trust — passscheduler=TrustLadderScheduler(...)and each roundrouterholds below-bar (ESCALATE) and no-agent (UNMATCHED) nodes out of the episode via theSupervisor's opt-inheldskip param — a pure non-dispatch that never mutates the frozenplan.order(the held node stays in the open frontier for a later round). Held nodes surface onGovernorResult.escalated/.unmatched.collectre-earns trust GOV-side — with a scheduler wired, each node re-earns its grade viaupdate_trustthe round it first completes (keyed by matched agent name); the only place earned trust moves across episodes, never in the compiler, never per-invocation.plannercan deliberate before freezing — passdeliberate=Trueto author round-1's DAG via the boundedform_plandeliberation (adjust → converge → lower to a frozenAgentDAG) strictly beforeassemble; later rounds still userecompile. Defaults to deterministic stubs — no LLM.- live read-only cockpit / scope —
loop.cockpit(),loop.programs_index(vault), andloop.leverage_view(vault)render theDirectorCockpit/scopeprojections over the loop's own log and final frozen plan — pure reads that dispatch nothing.
The scheduler/deliberation seams also thread up through the standing daemon and out to the
cockpit (opt-in; the default KTLODaemon(...) with no scheduler= and deliberate=False is
byte-for-byte today's plain episode):
- the
KTLODaemoncan spawn governed episodes — passscheduler=TrustLadderScheduler(...)(and/ordeliberate=True) and the daemon forwards them into each fresh boundedGovernorLoopit enqueues per triggered investigation, so a keep-the-lights-on run is trust-gated and can deliberate before freezing. The daemon still only enqueues fresh loops over fresh stores and holds no mutable plan (INV-1/INV-4). - the cockpit surfaces the governance holds —
DirectorCockpit.exception_queue()now folds the last episode's below-barESCALATEand no-standing-agentUNMATCHEDholds in alongside the failed-node rows (threaded throughloop.cockpit()), so a held frontier is operator-visible, not just failures — still a pure read that dispatches nothing. - an unmatched-node stall is named — when a governed loop can make no further progress solely
because every remaining open-frontier node is
UNMATCHED,GovernorResult.terminated_byreports the distinctunmatched_stalllabel instead of a generic stall, so the terminal cause is explicit.
Two shipped-but-idle core seams are now wired into the dispatch path (C-3, identity-preserving):
the Supervisor constructor runs the shipped RunGraph.validate() once as a pre-dispatch
structural gate (a dangling AgentRef or cycle is rejected before the first invoke; run() stays a
single static pass), and MemoryStateStore.replay() is documented as a full cold rebuild — the
INV-5-correct choice, since AgentCore's nextToken is an opaque pagination cursor, not an
events-after filter.
Roadmap
- Declarative core:
AgentDAG+AgentManifest(.agent.yaml) + validation + CLI - Dependency resolver over declared output JSON Schemas (
AgentRefwiring + type-gating) -
OrchestrationAssembler: emit an AgentCore provisioning plan (CreateAgentRuntimeper agent + synthesized IAM roles + endpoints) - The supervisor: topological dispatch over
InvokeAgentRuntimewithAgentRefwiring + one stableruntimeSessionId -
plan/deploy/runCLI verbs (deploy/run--executebind boto3 lazily) - Memory-backed shared run state (the
StateStoreseam: in-process default / AgentCore Memory opt-in, replay-resume, the AgentRef link graph +context(node)) - The governor: an opt-in dynamic outer loop (
GovernorLoop/TrustLadderScheduler/AgentRegistry/DirectorCockpit/KTLODaemon/scope) that drives the freeze compiler as bounded episodes (LangGraph optional) - Gateway/A2A node types; a data-driven catalog + recommender of team topologies
License
MIT © Tianpei Xie
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file concursus-0.4.2.tar.gz.
File metadata
- Download URL: concursus-0.4.2.tar.gz
- Upload date:
- Size: 262.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71cd65bd365ca4ac4b991e486c85ca19d3b0d22cee9067094284354a7c609ccf
|
|
| MD5 |
203fa6542d94dc358025f94e7acf07a1
|
|
| BLAKE2b-256 |
e8e513d0589c4bc26ac03e1d7ffcea9c38dc074681894b83a1f243edf89cd934
|
File details
Details for the file concursus-0.4.2-py3-none-any.whl.
File metadata
- Download URL: concursus-0.4.2-py3-none-any.whl
- Upload date:
- Size: 184.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b29f8805855e2f065ae8f84efe4f3940e43097af6bb7873dd1346ed6b70366c4
|
|
| MD5 |
ce46039f2f16a2cb322bdced68a8134f
|
|
| BLAKE2b-256 |
7dd198277977b72ea0c66e65e55c0ab93a634a5635bec5dcd5dba56c5b494b90
|