Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/explanation/artifacts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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/
```

<Info>
**Episode IDs** use ULID format (monotonic, sortable, 48-bit timestamp + 80-bit entropy). **Directive and governance IDs** use deterministic UUIDv5 for reproducible lineage tracking.
</Info>

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.
Expand Down
6 changes: 6 additions & 0 deletions docs/guides/human-in-the-loop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Tip>

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:
Expand Down
80 changes: 80 additions & 0 deletions docs/guides/integrate-adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -124,30 +124,108 @@ 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

| 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 |
Expand All @@ -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`

Expand Down
22 changes: 22 additions & 0 deletions docs/reference/python-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
Loading