Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

feat(wasm): add hook-provider interface for bidirectional host control#998

Open
mathpal wants to merge 1 commit into
strands-agents:mainfrom
mathpal:feat/hooks
Open

feat(wasm): add hook-provider interface for bidirectional host control#998
mathpal wants to merge 1 commit into
strands-agents:mainfrom
mathpal:feat/hooks

Conversation

@mathpal

@mathpal mathpal commented May 4, 2026

Copy link
Copy Markdown
Contributor

Motivation

WASM hosts (Rust SDK, future language bindings) need to control the agent lifecycle — cancel expensive operations, enforce policies, redact secrets, retry on transient failures — without forking or modifying the guest. The existing LifecycleBridge is observation-only (unidirectional). This adds a bidirectional hook-provider WIT contract so hosts can make decisions at each lifecycle point.

Public API Changes

New WIT import hook-provider with 9 functions (8 lifecycle hooks + capability negotiation). The guest calls out to the host at each lifecycle point and receives a decision:

┌─────────────────────────────────────────────────────────────────┐
│  Agent loop (WASM guest)                                        │
│                                                                 │
│  invoke("prompt")                                               │
│    │                                                            │
│    ├─► host::before-invocation() ──► { cancel? }               │
│    │                                                            │
│    ├─► model.stream(messages)                                   │
│    │     ├─► host::before-model-call(tokens) ──► { cancel? }   │
│    │     └─► host::after-model-call(stop, err) ──► { retry? }  │
│    │                                                            │
│    ├─► if tool_use:                                             │
│    │     ├─► host::before-tools(message) ──► { cancel batch? } │
│    │     │                                                      │
│    │     ├─► for each tool:                                     │
│    │     │     ├─► host::before-tool-call(toolUse)              │
│    │     │     │     ──► { cancel? rewrite? swap tool? }        │
│    │     │     ├─► execute tool                                 │
│    │     │     └─► host::after-tool-call(toolUse, result, err)  │
│    │     │           ──► { retry? replace result? }             │
│    │     │                                                      │
│    │     └─► host::after-tools()                                │
│    │                                                            │
│    └─► host::after-invocation() ──► { resume with new input? } │
└─────────────────────────────────────────────────────────────────┘

Hosts declare supported hooks via get-capabilities(). Unsupported hooks are never called (zero overhead for hosts that don't use them).

Modification capabilities

┌────────────────────┬──────────────────────────────────────────┐
│ Hook               │ What the host can do                     │
├────────────────────┼──────────────────────────────────────────┤
│ before-tool-call   │ Cancel, rewrite input (partial — only    │
│                    │ fields present in JSON are overwritten),  │
│                    │ swap to a different tool by name          │
├────────────────────┼──────────────────────────────────────────┤
│ after-tool-call    │ Retry execution, replace result in       │
│                    │ conversation history                      │
├────────────────────┼──────────────────────────────────────────┤
│ before/after model │ Cancel (inject text), retry              │
├────────────────────┼──────────────────────────────────────────┤
│ before-invocation  │ Cancel entire invocation                 │
│ after-invocation   │ Resume loop with new input               │
├────────────────────┼──────────────────────────────────────────┤
│ before-tools       │ Cancel all tool calls in a batch         │
└────────────────────┴──────────────────────────────────────────┘

Scope: Control-Plane Hooks (not full TS SDK parity)

This patch gives WASM hosts control-plane capabilities — the ability to cancel, retry, redirect, and mutate operations. It does NOT expose the full set of TS SDK hook event types or read access to agent state.

What WASM hosts get (7 of 15 TS SDK hookable events):

  • BeforeInvocationEvent, AfterInvocationEvent
  • BeforeModelCallEvent, AfterModelCallEvent
  • BeforeToolsEvent, AfterToolsEvent
  • BeforeToolCallEvent, AfterToolCallEvent

What WASM hosts do NOT get:

  • No access to event.agent, event.messages, event.model, or event.tool references
  • No InitializedEvent, MessageAddedEvent, AgentResultEvent
  • No streaming/data events (ModelStreamUpdateEvent, ContentBlockEvent, etc.)
  • AfterModelCallEvent only receives stopReason + error, not the full ModelStopData message

These are deliberate limitations of the WIT boundary — full state access would require serializing the entire agent on every hook call. The existing LifecycleBridge stream events remain available for observation.

Python host state

Python (_wasm_host.py) registers no-op stubs that return zero-value decisions and an empty capabilities list. This means no hooks fire in Python today — the stubs exist solely to satisfy the WIT linker. A follow-up PR will add a Python-side hook registration API (WasmAgent(hooks=...)) that wires user callbacks to the WIT functions.

Breaking Changes

Adding import hook-provider to the WIT world makes it a required import. Every host must implement all 9 functions. The Python host already has stubs. Any new host language binding must provide stubs (all returning zero-value decisions) to link successfully.

Add a WIT hook-provider contract and HookProviderBridge plugin that
forwards SDK lifecycle hooks (before/after invocation, model call,
tool call, tools batch) across the WASM boundary to host-side
handlers. The bridge uses capability negotiation so hosts only
receive hooks they declare support for.

Key behaviors:
- Cancel decisions propagated from host to guest
- Tool-use rewrite (partial field updates preserve originals)
- Tool selection override via selectedToolName
- Result replacement via after-tool-call
- Model/tool retry signaling
- Resume from after-invocation with arbitrary InvokeArgs JSON

Includes Python host stubs, comprehensive unit tests covering
partial rewrites, multi-tool batch cancel, retry chains, and
malformed JSON graceful degradation.
@mathpal mathpal marked this pull request as ready for review May 4, 2026 23:59
@strands-agent

Copy link
Copy Markdown
Collaborator

This repository has been merged into the strands-agents/harness-sdk monorepo and will be archived shortly. All new development happens there.

If this PR is still relevant, please recreate it against the monorepo. The code now lives under strands-ts/. Full commit history was preserved, so your base should be findable.

Apologies for the disruption, and thank you for contributing!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants