This guide reflects the current codebase and is updated frequently while Noēsis is in active development.
How adapters work
An adapter is any execution target that Noēsis can invoke.The ns.solve() integration contract
ns.solve() invokes the resolved using= target in the act phase by calling invoke(), then run(), then __call__() (in that order). EpisodeRunner owns cognition, governance, and event emission.
If you’re already inside an event loop, use ns.solve_async(...), which preserves the same invocation order and semantics while allowing async adapters to be awaited.
The execute(...) adapter protocol (advanced)
The repo also defines an Adapter.execute(...) protocol in noesis.adapters.protocols.Adapter:
ns.solve() does not call execute() directly.
Protocol-first tool invocation contract (internal)
Noesis now includes an application-layer contract for side-effect tools innoesis.usecases.tool_invocation:
prepare_tool_invocation(...)execute_prepared_tool_invocation(...)
ns.run(...), ns.solve(...), ns.governed_act(...)) remain unchanged.
Intent
The contract separates reviewable intent from side effects:prepare_tool_invocation(...)may validate, normalize, authenticate, authorize, emit candidate evidence, compute preflight bindings, and persist aPreparedToolInvocation.prepare_tool_invocation(...)must not execute tool side effects.execute_prepared_tool_invocation(...)loads previously prepared intent by identity and dispatches only after approval/idempotency checks.
Prepare -> execute workflow
Use this runbook when building a tool protocol adapter:- Build
ToolInvocationInputfrom inbound protocol data. - Call
prepare_tool_invocation(...)with your ports and persist the returned draft. - If status is
pending_approval, collect approval externally and persistToolApprovalDecisionusing the samerun_id+draft_id. - Call
execute_prepared_tool_invocation(...)with thatrun_id+draft_id. - Return
ToolExecutionResultto the caller; handlereplayed/failedidempotency outcomes without re-dispatching.
- prepare:
ToolPayloadNormalizerPort,ToolAuthenticatorPort,ToolAuthorizerPort,ToolCandidateEmitterPort,ToolEventRecorderPort,PreparedInvocationRepositoryPort, optionalToolPreflightPort - execute:
PreparedInvocationRepositoryPort,ApprovalDecisionRepositoryPort,IdempotencyStorePort,ToolDispatchPort,ToolEventRecorderPort
Episode runtime bridge
Use the runtime bridge when a prepared tool invocation should participate in the same episode lifecycle asns.run(...), ns.solve(...), and ns.resume_run(...).
The bridge is internal and intentionally narrow: it currently supports
ToolProtocol.SUBPROCESS only.
- creates canonical
action_candidateevidence before any side effect - persists a pending prepared draft for
run_id + draft_id - returns an interrupted actuation result when approval is required
- lets the run lifecycle emit
run.interrupt,run.checkpoint, andrun.state_projection
ToolApprovalDecision, call
ns.resume_run(episode_id, checkpoint_id=...). resume_run(...) detects a
pending prepared draft for the same run, builds resumed bindings internally, and
executes that draft without calling the original request_factory again.
Event and identity invariants
For write + approval-required flows, prepare emits:tool.requested -> tool.validated -> tool.authn.passed -> tool.authz.passed -> action.candidate_emitted -> tool.preflight.computed -> tool.draft_created -> tool.approval.pending
When the runtime bridge pauses a run, the episode trace also contains:
action_candidate -> action.candidate_emitted -> tool.approval.pending -> run.interrupt -> run.checkpoint -> run.state_projection
For approved execution, execute emits:
- new execution:
tool.approved -> tool.execution.started -> tool.execution.succeeded(ortool.execution.failed) - replay path:
tool.approved -> tool.replayed
ns.resume_run(...) continuation, the existing event log remains an
append-only prefix and resume adds:
run.resume -> tool.execution.started -> ... -> terminate
Identity and binding rules:
- execute lookup key is
run_id + draft_id - runtime resume lookup expects exactly one pending draft for the run
- missing prepared draft raises
PreparedToolInvocationNotFoundError - multiple pending drafts raise
AmbiguousPreparedToolInvocationError - missing/non-approved decision raises
ApprovalDecisionRequiredError - mismatched
request_id, reviewed fingerprint, or impact hash raisesApprovalDecisionBindingError - non-subprocess runtime bridge protocols raise
UnsupportedToolProtocolError - idempotency
replayorconflictreturns without dispatching side effects
Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
PreparedToolInvocationNotFoundError | approval service called execute with wrong run_id/draft_id | persist and pass durable identity unchanged across systems |
UnsupportedToolProtocolError before draft files exist | runtime bridge prepare received http or mcp | use ToolProtocol.SUBPROCESS for bridge continuation, or call lower-level prepare/execute outside the bridge |
UnsupportedToolProtocolError during resume_run with no run.resume event | pending draft has an unsupported protocol | correct/remove the draft before resuming; unsupported drafts fail before lifecycle mutation |
AmbiguousPreparedToolInvocationError | more than one pending_approval draft exists for the same run | resolve or archive extra drafts so one pending draft remains |
ApprovalDecisionRequiredError | decision missing or not approved | write an approved ToolApprovalDecision before execute |
ApprovalDecisionBindingError | decision is not bound to reviewed prepared intent | store request_id, reviewed_fingerprint, and impact_hash from the prepared artifact and verify before save |
tool.replayed result with no dispatch | same idempotency key and fingerprint seen previously | treat as successful replay; do not retry with a new side effect |
summary.json / manifest.json missing while run is paused | expected non-terminal lifecycle state | inspect latest run.state_projection links; resume and terminate run to produce final artifacts |
Runtime bridge guardrails (current)
The runtime continuation bridge currently enforces the following:- only
ToolProtocol.SUBPROCESSis supported for prepare/resume bridging - unsupported protocols fail before prepared-draft persistence on prepare
- unsupported pending drafts fail before
run.resumeis emitted on resume - resume lookup expects exactly one pending draft for the run (
load_pending_for_run) - multiple pending drafts for one run raise
AmbiguousPreparedToolInvocationError
UnsupportedToolProtocolError.
Subprocess payload contract
ForToolProtocol.SUBPROCESS, the normalized payload supports only:
argv: required, non-emptylist[str]cwd: optionalstrenv: optionaldict[str, str]timeout_ms: optionalint > 0(falls back to execution default when omitted)
Operational runbook: inspect and resume pending drafts
Prepared/approval/idempotency records are persisted under the run directory:tool_invocations/prepared/*.jsontool_invocations/approvals/*.jsontool_invocations/idempotency/*.json
- Confirm one pending draft exists in
tool_invocations/prepared/for the target run. - Persist an approved
ToolApprovalDecisionbound to the samerun_id + draft_id. - Resume the run (
ns.resume_run(...)) so continuation can execute the pending draft through the runtime bridge.
Runtime evidence checks (pause/continue)
When a run pauses before side effects, inspect runtime events to confirm bridge evidence is complete:- expect
phase="runtime",event_type="run.interrupt"andrun.checkpoint - expect a
phase="runtime",event_type="run.state_projection"event - on paused runs,
run.state_projection.payload.linksshould contain onlyeventsandlearn(nosummary/manifestyet) - on paused runs,
run.state_projection.payload.statusshould match the pause status (for exampleinterrupted)
ns.resume_run(...) completes terminally, the latest
run.state_projection should include terminal links (summary, manifest) and
state finalization artifacts will be present.
When you call ns.solve() with using=, Noēsis:
- Resolves the target via the graph loader (callable, object, or string source)
- Captures the task in the observe phase
- Runs the target in the act phase
- Records the result in the reflect phase
- Emits all events to the timeline
Resolution rules
using= accepts several shapes, resolved by the loader:
- Callable: used directly (or invoked if it is a zero-arg factory).
- Object: used directly if it exposes
invoke()or is callable. - String: can be a dotted factory (
pkg.mod:make), a filesystem path, or a short name resolved viaflows.<name>ornoesis_user.<name>.
invoke(), run(), or __call__() (in that order).
Objects that only define execute() are not compatible with ns.solve() unless wrapped.
If you need to map the input task string to a structured payload, you can:
- wrap the target and do the mapping inside your wrapper, or
- if supported by the invocation path you’re using, define a
__noesis_input_mapper__callable that transforms the task before invocation.
Plain functions
The simplest adapter is a plain Python function:LangGraph integration
Compiled LangGraph graphs exposeinvoke(), so you can pass them directly:
noesis.adapters.langgraph.LangGraphAdapter. It resolves awaitables with asyncio.run() when no event loop is running (and raises if a loop is already running). It also supports an optional input mapper and may fall back to __noesis_input_mapper__ if the wrapped graph provides one.
CrewAI integration
Wrap a CrewAI crew withnoesis.adapters.crewai.CrewAIAdapter:
{"task": task} unless the crew defines __noesis_input_mapper__.
If your crew expects a different input shape, provide __noesis_input_mapper__ on the crew or pass input_mapper= when constructing CrewAIAdapter.
Claude Agent SDK integration
When you’re already running inside an event loop, usens.solve_async(...) and pass an async adapter directly.
If you’re in synchronous code, wrap the SDK’s async loop with
asyncio.run(...) or move the call
to a worker thread before calling ns.solve(...).SDK surface shown above reflects the public
claude_agent_sdk examples; verify against your installed SDK version.Experimental Assistants adapter
noesis.adapters.assistant.AssistantsAdapter is experimental and writes its own events via write_event. It follows the execute(...) protocol but does not expose invoke() / run() / __call__(), so it is not compatible with ns.solve() without a wrapper. Use it only with a custom runner that explicitly calls execute().
Custom adapters with metadata
Adapters can return any object. Noēsis usesstr(result) when constructing the action summary. If you return a dict, the summary will include its string representation.
Error handling
Noēsis treats exceptions as failures. Raise to mark a failed action:Using string sources
You can also pass a stringusing= value that the loader resolves:
pkg.mod:factoryto import and call a zero-arg factory./path/to/module.pyto load a local modulenameto loadflows.nameornoesis_user.name
Adapter best practices
Keep adapters stateless. If you need state, use the Noēsis state artifact rather than adapter instance variables.
Testing adapters
Test adapters independently before integrating:Next steps
Export metrics
Send adapter metrics to your observability stack.
Python API reference
Full API documentation for adapters.

