Agentic Runtime v2¶
ChemSpace Copilot 0.2 introduces explicit workflow-run contracts that can be
used by the Agno team and external MCP clients. The language model chooses and
coordinates work; deterministic tools, artifact storage, policy checks, and
event replay remain ordinary Python infrastructure. Strict persisted task-DAG
and task-contract enforcement is currently implemented as the
chembl-to-gtm-report MCP pilot. Other catalog workflows retain their
taskless, legacy execution shape while still benefiting from run and artifact
contracts.
Identity and lifecycle¶
A session_id identifies the chat or storage session, a workflow_slug
identifies a catalog procedure, and a run_id identifies one execution of
that procedure. Runs move through submitted, planning, running,
input_required, and a terminal completed, partial, failed, or
cancelled state. Workflows that declare tasks also give each task a validated
lifecycle and assigned role/profile.
A structured handoff carries only the objective, constraints, acceptance criteria, trace identifiers, and input/output artifact contracts needed by the receiving role. Full histories and private reasoning are not handoff data.
At run creation, the selected workflow and all transitive dependencies are snapshotted with their semantic versions, Markdown procedures, and SHA-256 contract hashes. Resume and task validation use this pinned contract rather than silently adopting later catalog edits.
Artifact-backed source of truth¶
Each run writes beneath workflows/<run_id>/:
manifest.json: rebuildable current-state snapshot.events/*.jsonl: immutable state-transition records used for replay.artifacts/index.json: checksummed artifact provenance.- Operation folders for chemical space, design, retrosynthesis, and reports.
The event stream is authoritative. Manifest and artifact-index resources are replayed from those immutable events, so a missing, stale, or failed snapshot refresh cannot change run truth. SQLite stores conversation history, while workflow events and artifacts provide reproducible scientific state. Cross-session agent memory remains disabled.
Every tool call appends three to four events, so appends stay constant-cost
as a run grows. A context that already holds the stream checks that its last
known event is unchanged and reads newer events by their exact sequence names,
so an append needs no directory listing and no replay of the known prefix;
every 256 catch-ups a single listing confirms that no known event disappeared,
and a missing or rewritten event still fails with EventReplayError. The
replaceable snapshots are rewritten only for state-changing events, not for
the observational tool_progress, tool_call_recorded, and task_progress
events. scripts/benchmark_run_ledger.py --check measures append latency
against the configured storage backend.
Registered artifacts must remain inside their run root and record SHA-256, MIME type, producer task/tool, size, trust classification, and creation time. Local artifact verification and reads use no-follow, descriptor-relative traversal; MCP session-write arguments are normalized into the active run, and the storage layer repeats that boundary check at the actual descriptor-relative, no-follow open. Absolute, traversal, symlink-swap, reserved-runtime-file, encoded-path, hard-link, and foreign-S3 escapes fail closed. Paths already registered in the artifact ledger are immutable during domain-tool execution. For the task-DAG pilot, required task inputs are selected explicitly, persisted on the task, and checksum-verified before handoff, activation, and every domain call. A workflow cannot complete until its declared run, task, and artifact contracts are satisfied.
Domain writes use create-only invocation transactions. In-process files remain
private until the tool returns successfully; subprocess files remain beneath a
job-specific .staging/ prefix until the parent accepts their checksums,
output limits, task epoch, and state merge. Registration failures release only
the new, checksum-matching files that have no authoritative artifact event.
Task and run lifecycle transitions, including failure transitions, wait until
each domain invocation reaches a durable terminal stage: result_accepted for
a successful registered result, cache_hit, failed, cancelled, or a
supervisor-confirmed abandoned. The later completed observation is
best-effort because result acceptance has already linearized publication and
registration. This prevents status changes from racing an in-flight mutation.
Shared execution kernel¶
The pipeline that enforces these guarantees for every tool call lives in
cs_copilot.execution, not in the MCP package, so every runtime that lets a
reasoner call cs_copilot toolkits can use it:
execution.kerneldefines the invocation phases: attribution capture, catalog-task authorization and budgets, the durablestartedevent, read and write boundaries, idempotency reservation, the run write lock, execution inside the write scope, result acceptance, artifact registration and rollback, and the terminal events and v2 envelope.execution.runner.execute_asyncdrives those phases on an event loop; the MCP adapter (mcp/tool_adapter.py) wraps it with the public tool signature, theidempotency_keyparameter, MCP context injection, and subprocess worker dispatch.execution.runner.execute_syncdrives the same phases on a worker thread for runtimes without a running event loop. Any interruption still records a terminalcancelledevent, so a run is never left with an open tool span.- The run write lock is shared by both drivers: async callers take an event-loop lock and then the run's thread lock, which is the lock thread-based callers take, so MCP tools and in-process Agno tools writing to the same run never interleave.
- A tool that runs other tools declares
delegates_execution=True(the MCPagno_team_runtool). Its own invocation only records events: it takes no write lock, sets up no read or write confinement, and registers no result artifacts, because each delegated call goes through the kernel itself and records the delegating span (execution.tracing.current_tool_span_id()) as itsparent_span_id. Itsstartedevent carriesdelegates_execution: true, so the ledger's in-flight guards on run transitions and artifact registration watch only the delegated calls' spans; crash recovery can still abandon an orphaned delegating span. execution.eventswrites thetool_progressandtool_call_recordedevents. Theirruntimefield and the slug of a lazily created ad-hoc run come from the execution context's runtime profile (mcp/mcp-session,agno/agno-session). Both ad-hoc slugs are exempt from catalog-task authorization.execution.errorsowns the seven-code error taxonomy shared withcs_copilot.workflows(ToolErrorCode);execution.llmgives toolkits a runtime-neutral way to require an in-process model.
The kernel never imports agno, cs_copilot.agents, cs_copilot.mcp, or the
mcp SDK, and it imports storage, workflows, and toolkit helpers only inside
functions, because cs_copilot.storage reads SESSION_ID at import time. Both
rules are enforced by tests/unit/mcp/test_no_team_imports.py and
tests/unit/test_runtime_import_isolation.py.
Capability profiles¶
cscopilot-mcp --profile <name> registers only the selected profile's tools. The shipped profiles are bootstrap, standard, chembl-retrieval, gtm-analysis, chemoinformatics, reporting, molecular-design, peptide-design, retrosynthesis, and robustness. Unknown profiles and workflows whose required tools are unavailable fail before execution.
One capability table (src/cs_copilot/capabilities.py) declares the role
grants and profiles for both runtimes: the in-process role policies that
enforce per-role toolkit allowlists for the coordinator and every specialist,
and the roles and profiles of every MCP tool, are derived from it. For catalog
workflows with a task DAG—currently the
chembl-to-gtm-report pilot—the MCP adapter also requires an active running
task and enforces its role, profile, and tool allowlist before domain
execution. Tool contracts include read/write,
destructive, open-world and idempotency annotations, execution limits, and
normalized errors. Network-capable tools are always advertised as open-world,
high-risk capabilities. Workflow loading also rejects permission metadata
that omits the network, compute, or artifact access implied by its declared
tools and artifact contracts.
The Agno team remains the central in-process supervisor. Member transcript
broadcasting is disabled, role factories reject tools outside their allowlists,
and the delegation guard validates handoff schema, private-context exclusion,
receiver role, and declared budgets. When the caller supplies a v2
RunContext, Agno additionally records the handoff through that runtime and
therefore receives its durable pinned-task validation.
Chainlit and the CLI give every chat one durable ad-hoc agno-session run,
created before the first message is processed and reused when a chat resumes
(cs_copilot.agents.session_runs). The coordinator invents task ids for
free-form chats, so for ad-hoc runs the delegation guard stamps the real run
identity onto each handoff, creates the task if it does not exist, records the
handoff, and starts the task; tasks started during a turn are completed (or
failed) when the turn ends. Tool spans and running tasks left behind by a
previous chat process are reconciled as abandoned or interrupted on resume.
Uploads are stored inside the run and registered as untrusted user_upload
artifacts.
Every toolkit call of the team goes through the shared kernel
(cs_copilot.agents.execution_binding). The mode is set by
CS_COPILOT_AGNO_EXECUTION:
enforce(default): the full kernel, as for MCP tools, adapted to in-process calls: destination arguments are rewritten into the chat's run, writes are confined and create-only (a registered output can never be overwritten), declared file inputs must be registered artifacts, each file is published when it is closed and every file a call wrote is registered, and a failing call's files are rolled back. Denials reach the model as tool errors. Resumed chats verify their artifacts (a chat whose artifacts no longer match starts a fresh run), and uploads stored outside the run are adopted into it. Only ad-hoc chat runs are supported;observe: calls run exactly as before, while the kernel records the same event protocol as MCP (labelledruntime: "agno", attributed to the calling role and delegated task), registers every file a call created inside the run, and adds anexecution_auditblock totool_call_recordeddescribing how the write and read boundaries would have rewritten or denied the call and how each write would be classified under confined writes;off: no recording; storage behaves exactly as before.
scripts/agno_execution_audit.py --session <id> aggregates the observe-mode
audits of a chat per tool — which writes enforce would refuse, which inputs
it would deny — so a new toolkit or workflow can be checked in observe mode
before it runs under enforce.
Because registered outputs are immutable, toolkits that write deterministic
file names pass them through S3.first_free_path(...), which returns the path
itself or its first free <stem>-vN<suffix> version, so repeating a step (a
rebuilt map, a rewritten report) writes a new version instead of failing.
Code that locates an output by its deterministic name rather than by the path
a tool returned uses S3.latest_version_path(...).
tests/unit/test_toolkit_enforce_canaries.py runs each built-in writer twice
under enforce mode.
In-process calls are recorded under the identity of their MCP twin tool when
one exists (ToolSpec.agno_bindings, merged from
mcp/tool_specs/agno_twins.py), and as agno.<Toolkit>.<function> otherwise.
For the MCP task-DAG pilot, handoffs are validated against the pinned task role
and exact capability, selected-input, output, and acceptance-criteria
contracts. Catalog tasks cannot start without an attempt-bound handoff, and
resuming from input_required or retrying a failed task requires a fresh one.
In MCP mode the server enforces its observable tool-call and elapsed-time
limits; the external reasoning client remains responsible for the declared
token budget because its token usage is not visible to the server. External
MCP clients are supervisors in MCP mode; the server never starts the Agno team
unless the separately gated compatibility tool is enabled.
Concurrent MCP calls with the same task-scoped idempotency key and arguments coalesce behind one execution; key reuse with different arguments is rejected. Retries remain idempotent-only. Async calls and subprocess workers have cancellable deadlines, including the remaining handoff deadline. In-process synchronous methods do not advertise a false cancellable timeout: cancellation waits for their worker thread to finish, and a handoff overrun is rejected only after that safe drain, so a mutation cannot continue invisibly and race a retry. Subprocess session-state results use a three-way merge and reject concurrent collisions or stale task scope atomically. Output limits still bound inline responses. The replay evaluator checks semantic milestones, role/tool permissions, preflight ordering, duplicate successful calls, artifact types, and terminal status instead of comparing generated prose.
Catalog and plugin boundaries¶
skills/*/SKILL.md and workflow_catalog/*/WORKFLOW.md remain the source of scientific procedures. Their metadata declares semantic versions, dependencies, required profiles/permissions, and input/output artifact contracts.
The repo plugin in plugins/chemspace-copilot is a delivery layer. It starts the MCP server and teaches Codex to bootstrap and fetch catalog procedures; it does not duplicate chemistry logic.
Breaking migration from 0.1¶
Version 0.2 does not project v1 session_state keys or cscopilot://session/ resources into the new schema. Existing files are left untouched but cannot be resumed as v2 runs. New resource URIs are run-scoped:
cscopilot://runs/<run_id>/manifest.jsoncscopilot://runs/<run_id>/events/<event-id>.jsonlcscopilot://runs/<run_id>/artifacts/<artifact-id>