Skip to content

@tape.effect and @tape.outbox_tool

The two decorators tools wear. Use @tape.effect when the tool body performs IO against an idempotent upstream; use @tape.outbox_tool (sugar for @tape.effect(dispatch="outbox", ...)) when the upstream can't be made naturally idempotent and the outbox reactor should own the dispatch.

The decoration-time safety rule for non-idempotent effects is enforced here, and the Rust server enforces the same contract at BeginEffect-time.

tape.effect.effect

effect(*, compensate: Optional[Callable] = None, status_check: Optional[Callable] = None, key_from: Optional[Callable] = None, compensation_payload: Optional[Callable] = None, retry: Optional[RetryPolicy] = None, max_attempts: int = 0, semantics: str = 'idempotent', dispatch: str = 'inline', connector: str = '', business_key: Any = None, allow_unsafe: bool = False) -> Callable

Declare an effect.

semantics is one of "idempotent" (default — the counterparty dedupes on our key; blind retry is safe), "non_idempotent" (a second call would land twice — Tape refuses inline dispatch), or "observe_only" (no side effects).

dispatch is "inline" (the tool body runs and calls the counterparty — the v1 model) or "outbox" (the tool body records intent; the outbox reactor calls the counterparty via a registered connector).

connector names the routing key the outbox reactor matches on (e.g. "bank.wire"). Required when dispatch="outbox".

business_key is either a static string or a callable (tool_args, tool_context) -> str that yields the cross-run business-level dedupe key — what the counterparty itself would use to recognise the same logical operation. When set, the server enforces uniqueness on (connector, business_key) across all runs.

tape.effect.outbox_tool

outbox_tool(*, connector: str, semantics: str = 'non_idempotent', business_key: Any = None, compensate: Optional[Callable] = None, status_check: Optional[Callable] = None, max_attempts: int = 0, allow_unsafe: bool = False) -> Callable

Sugar for the non-idempotent + outbox pattern.

@tape.outbox_tool(connector="bank.wire", semantics="non_idempotent",
                  business_key=lambda account, amount, date: f"{account}:{amount}:{date}",
                  compensate=reverse_wire, status_check=find_wire)
def wire_money(account, amount, beneficiary, date):
    return {"account": account, "amount": amount,
            "beneficiary": beneficiary, "date": date}

The tool body only builds the intent payload — it must not perform external IO. TapePlugin records the intent via BeginEffect and returns a synthetic {tape_status: "accepted", ...} result to ADK without running the body. The outbox reactor picks it up and the registered connector performs the call exactly once, with explicit ambiguity handling.


Tool-context helpers

tape.effect.idempotency_key

idempotency_key(tool_context: Any) -> str

Inside a tool body: the idempotency key Tape minted for this call.

Pass it to the counterparty (bank.wire(..., idempotency_key=key)) so a re-run after a crash is deduped at the floor.

tape.effect.run_id_of

run_id_of(tool_context: Any) -> str

tape.effect.business_key

business_key(tool_context: Any) -> str

The business-level dedupe key Tape derived (from the decorator's business_key= callable) for the in-flight effect. Empty if no key was declared.

tape.effect.external_ref

external_ref(tool_context: Any) -> str

The counterparty's identifier for the operation (a wire_id, a payment intent id), once Tape has observed it. Empty until known — typically only populated on re-drive after a connector + reconciler round-trip.

tape.effect.effect_semantics

effect_semantics(tool_context: Any) -> str

The declared semantics of the in-flight effect: 'idempotent', 'non_idempotent', or 'observe_only'. '' if not set.

Registries

tape.effect.register_compensator

register_compensator(name: str, fn: Callable) -> None

tape.effect.register_status_check

register_status_check(tool_name: str, fn: Callable) -> None

tape.effect.get_tool_compensator

get_tool_compensator(tool_name: str) -> Optional[dict]