Skip to content

Host Functions -- Intermediate

This guide covers organizing host functions into extensions, the extension coordinator, namespace validation, lifecycle hooks, OS contributions, child policies, and the EventLoopExtension.

Prerequisites: Read the Beginner guide first.

MontyExtension

For anything beyond a handful of functions, use MontyExtension to group related functions into a namespaced, lifecycle-managed unit.

Key Members

Member Purpose
namespace Unique prefix for all function names (e.g., storage_)
functions List of HostFunctions this extension provides
osContribution Prefix map for declarative OS call interception
childPolicy How the extension propagates to child sandboxes
priority Attachment order (higher priority attaches first)
supportedBackends Which backends (FFI/WASM) this extension supports
onAttach(ctx) Lifecycle hook called during attachment
onDispose() Lifecycle hook called when the session ends

Example Extension

class StorageExtension extends MontyExtension {
  final Map<String, Object?> _store = {};

  @override
  String get namespace => 'storage';

  @override
  List<HostFunction> get functions => [
    HostFunction(
      schema: const HostFunctionSchema(
        name: 'storage_get',
        description: 'Get a value by key.',
        params: [HostParam(name: 'key', type: HostParamType.string)],
      ),
      handler: (args, ctx) async => _store[args['key']],
    ),
  ];

  @override
  Map<String, OsCallHandler>? get osContribution => {
    'os.storage_size': (op, args, kwargs) async => _store.length,
  };

  @override
  ChildPolicy get childPolicy => ChildPolicy.clone;

  @override
  MontyExtension createChildInstance(ChildSpawnContext context) =>
      StorageExtension();
}

ExtensionCoordinator

ExtensionCoordinator collects extensions, validates them, and wires them onto a bridge:

final coordinator = ExtensionCoordinator()
  ..register(StorageExtension())
  ..register(MathExtension());

await coordinator.attachTo(bridge);

OS Contributions

Extensions can intercept OS calls (like pathlib or os) declaratively via osContribution. The coordinator merges these from all extensions:

@override
Map<String, OsCallHandler> get osContribution => {
  'Path.': _myHandler,
  'os.': _myHandler,
};

If two extensions claim the same prefix, attachTo() throws a StateError.

Child Policies

When a child sandbox is spawned (via SandboxExtension), the parent coordinator propagates extensions to the child based on their childPolicy:

  • ChildPolicy.exclude (default): The extension is not present in children.
  • ChildPolicy.inherit: The same instance is shared with the child.
  • ChildPolicy.clone: createChildInstance() is called to create a fresh copy for the child.

EventLoopExtension

EventLoopExtension turns a one-shot execute() call into a long-running cooperative exchange.

Flow Diagram

sequenceDiagram
    participant D as Dart
    participant P as Python

    D->>P: execute(script_with_loop)
    loop Event Loop
        P->>D: el_emit(state)
        Note right of D: Dart UI updates
        P-->>D: el_recv() (blocks)
        Note left of P: Python is paused
        D->>P: dispatch(event)
        Note right of D: User action triggers dispatch
    end
    D->>P: dispatch({action: "quit"})
    P-->>D: Loop breaks, script finishes

The Pattern

Python calls el_recv() to pause and el_emit(value) to push data back:

while True:
    el_emit({"status": "ready"})
    event = el_recv() # Blocks until Dart calls dispatch()
    if event["action"] == "quit": break

Dart Side

final ext = EventLoopExtension();
final session = MontyRuntime(extensions: [ext]);

ext.lastEmittedSignal.subscribe((v) => print('Python: $v'));
session.execute(script);

ext.dispatch({'action': 'quit'});

What's on ctx

Every handler signature is (args, ctx) async. ctx is a HostContext exposing the platform plumbing your handler may need:

Field Purpose
emit(BridgeEvent) / emitText(String) Push events into the execution stream — progress, intermediate results, custom events
executionId Unique ID for this tool call; correlate with BridgeFunctionEmit and other per-call events
cancelToken Cooperative cancellation signal — race long-running work against cancelToken.future to bail when ExecutionHandle.cancel() fires
os Currently-registered OsCallHandler, for handlers that want to invoke OS primitives directly without routing through Python
parent Narrow HostParentRef? view of the owning runtime — emitChildEvent for child-spawning extensions, schemas for tool introspection. Deliberately does not expose execute() (that would deadlock the bridge).
subExecute Closure that runs a sub-script in a fresh Monty interpreter. Use when you need to drive another Python execution from inside your handler — see Inputs, run_script, and Sub-Execution.

The parent / subExecute split is intentional: nested execute() on the same bridge would always deadlock (_isExecuting stays true through dispatch), so the API offers subExecute for the safe path and a narrower HostParentRef for everything else.

Next Steps

The Advanced guide covers deep-dives into SandboxExtension and complex production patterns. For sub-scripting, see Inputs, run_script, and Sub-Execution.