diff --git a/docs/explanation/artifacts.mdx b/docs/explanation/artifacts.mdx index e27739f..0ac2673 100644 --- a/docs/explanation/artifacts.mdx +++ b/docs/explanation/artifacts.mdx @@ -14,15 +14,51 @@ Every Noēsis episode produces a set of structured artifacts that capture the co events.jsonl # cognitive event timeline with lineage summary.json # metrics and KPIs (insight.metrics) state.json # plan, beliefs, memory, outcomes + final.json # sealed terminal result manifest.json # SHA-256 catalog + optional HMAC learn.jsonl # learning signals (optional) prompts.jsonl # prompt provenance (opt-in, ADR-005) + checkpoints/ # resume anchors for interrupted runs + tool_invocations/ # prepared side-effect state (when used) + prepared/ + approvals/ + idempotency/ ``` **Episode IDs** use ULID format (monotonic, sortable, 48-bit timestamp + 80-bit entropy). **Directive and governance IDs** use deterministic UUIDv5 for reproducible lineage tracking. +Paused approval workflows are intentionally not sealed. While a run is waiting +for approval, expect `events.jsonl`, `state.json`, `learn.jsonl`, and checkpoint +or tool-invocation records to exist, but not terminal-only files such as +`final.json`, `summary.json`, or `manifest.json`. Those terminal artifacts are +written after the run resumes and completes. + +## Tool invocation state + +Protocol-first tool integrations persist review and idempotency state under the +episode directory: + +| Path | Purpose | +| --- | --- | +| `tool_invocations/prepared/*.json` | Reviewable prepared tool intent keyed by `run_id + draft_id` | +| `tool_invocations/approvals/*.json` | Human or policy decisions bound to the reviewed draft | +| `tool_invocations/idempotency/*.json` | Execution fingerprints used to replay or reject duplicate side effects | + +For approval-gated subprocess tools, the runtime bridge prepares the draft, +emits `tool.approval.pending`, interrupts the run, and writes a checkpoint. After +an approved `ToolApprovalDecision` is persisted, `ns.resume_run(...)` continues +the same run and executes the existing draft without re-preparing it. + +Constraints to check during incident response: + +- there should be exactly one `pending_approval` prepared draft for the run +- unsupported bridge protocols (`http`, `mcp`) fail before draft persistence on + prepare or before `run.resume` is emitted on resume +- missing `final.json` or `manifest.json` is expected until the resumed run + reaches a terminal state + ## summary.json The summary captures episode outcomes, metrics, and cross-references. diff --git a/docs/guides/human-in-the-loop.mdx b/docs/guides/human-in-the-loop.mdx index 31a5bf4..1c68212 100644 --- a/docs/guides/human-in-the-loop.mdx +++ b/docs/guides/human-in-the-loop.mdx @@ -59,6 +59,12 @@ def run_with_approval(task: str, using): Prefer `resume_run(...)` over rerunning from scratch after approval. It preserves a continuous, append-only run history. +For protocol-first side-effect tools, prefer the prepared invocation bridge +instead of inventing a second approval store. It persists the reviewed draft +under `tool_invocations/prepared/`, pauses the episode, and lets +`ns.resume_run(...)` execute the same `run_id + draft_id` after approval. See +[Integrate adapters](/guides/integrate-adapters#episode-runtime-bridge). + ## Policy-driven approval Create a policy that flags operations for approval: diff --git a/docs/guides/integrate-adapters.mdx b/docs/guides/integrate-adapters.mdx index 144a018..14b2b9c 100644 --- a/docs/guides/integrate-adapters.mdx +++ b/docs/guides/integrate-adapters.mdx @@ -124,23 +124,98 @@ result = execute_prepared_tool_invocation( ) ``` +### Episode runtime bridge + +Use the runtime bridge when a prepared tool invocation should participate in the +same episode lifecycle as `ns.run(...)`, `ns.solve(...)`, and `ns.resume_run(...)`. +The bridge is internal and intentionally narrow: it currently supports +`ToolProtocol.SUBPROCESS` only. + +```python +from noesis.usecases.tool_invocation.runtime_bridge import ( + ToolRuntimeBridgePorts, + build_tool_invocation_actuation_bindings, +) + +ports = ToolRuntimeBridgePorts( + prepared_repository=prepared_repository, + approval_repository=approval_repository, + idempotency_store=idempotency_store, + dispatch=subprocess_dispatch, + normalizer=normalizer, + authenticator=authenticator, + authorizer=authorizer, + preflight=preflight, +) + +bindings = build_tool_invocation_actuation_bindings( + request_factory=lambda run_id: ToolInvocationInput( + run_id=run_id, + request_id="req-apply-canary", + draft_id=f"draft:{run_id}:req-apply-canary", + protocol=ToolProtocol.SUBPROCESS, + tool=ToolIdentity(namespace="deploy", name="apply_canary", version="1"), + raw_payload={"argv": ["./scripts/apply-canary.sh"]}, + execution=ExecutionContext(timeout_ms=30_000, retry_limit=0), + security=SecurityContext( + principal_id="user:ops", + scopes=("deploy:write",), + policy_scope="prod/canary", + ), + governance=GovernanceContext( + effect_kind=EffectKind.WRITE, + risk_tier=RiskTier.HIGH, + requires_approval=True, + ), + ), + run_dir=run_dir, + ports=ports, + run_lifecycle=run_lifecycle, +) +``` + +The prepare side of the bridge: + +- creates canonical `action_candidate` evidence 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`, and + `run.state_projection` + +After an approval service writes a bound `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` (or `tool.execution.failed`) - replay path: `tool.approved -> tool.replayed` +For `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 raises `ApprovalDecisionBindingError` +- non-subprocess runtime bridge protocols raise `UnsupportedToolProtocolError` - idempotency `replay` or `conflict` returns without dispatching side effects ### Common pitfalls @@ -148,6 +223,9 @@ Identity and binding rules: | 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 | @@ -158,6 +236,8 @@ Identity and binding rules: The runtime continuation bridge currently enforces the following: - only `ToolProtocol.SUBPROCESS` is supported for prepare/resume bridging +- unsupported protocols fail before prepared-draft persistence on prepare +- unsupported pending drafts fail before `run.resume` is 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` diff --git a/docs/reference/python-api.mdx b/docs/reference/python-api.mdx index 6f699ad..e434460 100644 --- a/docs/reference/python-api.mdx +++ b/docs/reference/python-api.mdx @@ -498,15 +498,37 @@ from noesis.usecases.tool_invocation import ( prepare_tool_invocation, execute_prepared_tool_invocation, ) +from noesis.usecases.tool_invocation.runtime_bridge import ( + ToolRuntimeBridgePorts, + build_tool_invocation_actuation_bindings, + build_resumed_tool_invocation_actuation_bindings, +) ``` Use this contract when you need explicit prepare/approve/execute boundaries for side-effecting tools. The canonical identity key is `run_id + draft_id`. +Use `prepare_tool_invocation(...)` and `execute_prepared_tool_invocation(...)` +when your adapter owns the approval workflow. Use +`build_tool_invocation_actuation_bindings(...)` when the prepared invocation +should pause a Noesis episode before a side effect and continue through +`ns.resume_run(...)` after approval. + +Bridge constraints: + +- runtime bridge continuation currently supports `ToolProtocol.SUBPROCESS` only +- prepare requires `normalizer`, `authenticator`, and `authorizer` ports +- resume uses the pending draft already persisted for the run and does not call + the original request factory again +- `ns.resume_run(...)` auto-attaches resumed bindings when exactly one pending + prepared draft exists for the run + Execution-time failures are surfaced as typed errors from `noesis.domain.tool_contract`: - `PreparedToolInvocationNotFoundError` +- `AmbiguousPreparedToolInvocationError` - `ApprovalDecisionRequiredError` - `ApprovalDecisionBindingError` +- `UnsupportedToolProtocolError` For full workflow, event ordering, and troubleshooting guidance, see [Integrate adapters](/guides/integrate-adapters).