Host Functions -- Beginner
This guide covers typed parameter schemas, argument validation and
coercion, error handling in handlers, and the BridgeEvent stream.
Prerequisites: Read the Intro guide first.
HostFunctionSchema
Every host function has a schema that declares its name, description, and parameters:
const schema = HostFunctionSchema(
name: 'fetch_data',
description: 'Fetch JSON data from a URL.',
params: [
HostParam(name: 'url', type: HostParamType.string),
HostParam(
name: 'timeout',
type: HostParamType.integer,
isRequired: false,
defaultValue: 30,
description: 'Request timeout in seconds.',
),
],
);
The schema serves two purposes:
- Runtime validation -- the bridge validates and coerces arguments
before calling your handler. If Python passes wrong types, the bridge
raises a
FormatExceptionback to Python before your code runs. - JSON Schema export --
schema.toJsonSchema()produces a standard JSON Schema object for tool discovery (e.g., MCP integration).
HostParam
Each parameter is defined with HostParam:
| Field | Type | Default | Description |
|---|---|---|---|
name |
String |
required | Parameter name (key in the args map) |
type |
HostParamType |
required | Expected type |
isRequired |
bool |
true |
Whether the caller must supply a value |
description |
String? |
null |
Human-readable description for JSON Schema |
defaultValue |
Object? |
null |
Value used when argument is absent and not required |
jsonSchemaOverride |
Map<String, Object?>? |
null |
Full JSON Schema override for complex types |
HostParamType
The type system maps Python types to Dart types with automatic coercion:
HostParamType |
Python type | Dart type | Coercion |
|---|---|---|---|
string |
str |
String |
Exact match only |
integer |
int |
int |
Accepts num (truncates) and numeric String |
number |
float |
num |
Accepts int, double, and numeric String |
boolean |
bool |
bool |
Exact match only |
list |
list |
List<Object?> |
Exact match only |
map |
dict |
Map<String, Object?> |
Exact match only |
any |
any | Object? |
Passes through without type checking |
The integer and number types perform smart coercion. If Python sends
a float where you declared integer, the bridge truncates it to int.
If Python sends a numeric string, the bridge parses it. All other types
require exact matches.
Argument Mapping
When Python calls a host function, Monty provides positional arguments
and optional keyword arguments. The schema's mapAndValidate() method
maps them to a named parameter map:
Python: fetch_data("https://example.com", timeout=10)
positional: ["https://example.com"]
kwargs: {"timeout": 10}
Schema maps to: {"url": "https://example.com", "timeout": 10}
Rules:
- Positional arguments are matched to
paramsby declaration order. - Keyword arguments overlay by name.
- Extra positional arguments beyond the declared params raise a
FormatException. - Unknown keyword argument names raise a
FormatException. - Missing required parameters raise a
FormatException. - Absent optional parameters receive their
defaultValue.
Error Handling in Handlers
When your handler throws, the bridge catches the exception and sends it
back to Python as a runtime error via resumeWithError(). Python sees a
standard exception.
HostFunction(
schema: const HostFunctionSchema(
name: 'divide',
description: 'Divide two numbers.',
params: [
HostParam(name: 'a', type: HostParamType.number),
HostParam(name: 'b', type: HostParamType.number),
],
),
handler: (args) async {
final a = args['a'] as num;
final b = args['b'] as num;
if (b == 0) throw ArgumentError('Division by zero');
return a / b;
},
)
If Python calls divide(10, 0), it gets a Python exception with the
message "Division by zero". The bridge logs the error with stack trace
via struct_log and emits a BridgeFunctionCallResult with the error
message.
You do not need to catch exceptions yourself for error propagation -- the bridge does it automatically. Only catch if you need custom recovery.
The BridgeEvent Stream
bridge.execute() returns a Stream<BridgeEvent>. The events form a
sealed class hierarchy describing the full execution lifecycle:
Lifecycle Events
| Event | When | Key fields |
|---|---|---|
BridgeRunStarted |
Execution begins | threadId, runId |
BridgeRunFinished |
Execution completes | threadId, runId, value, printOutput |
BridgeRunError |
Execution fails | message, printOutput |
Tool Call Events
Each host function call emits a sequence of tool call events:
BridgeStepStarted(stepId: "greet")
BridgeFunctionCallStart(callId: "0", name: "greet")
BridgeFunctionCallArgs(callId: "0", delta: '{"name":"World"}')
BridgeFunctionCallEnd(callId: "0")
-- handler runs --
BridgeFunctionCallResult(callId: "0", result: "Hello, World!")
BridgeStepFinished(stepId: "greet")
| Event | Description |
|---|---|
BridgeStepStarted |
A host function call step begins |
BridgeFunctionCallStart |
Function name identified |
BridgeFunctionCallArgs |
Arguments as JSON delta |
BridgeFunctionCallEnd |
Arguments complete |
BridgeFunctionCallResult |
Handler result (or error message) |
BridgeStepFinished |
Step complete |
Text Events (Print Capture)
Python print() calls are intercepted by the bridge. Output is buffered
and flushed as text events at the end of execution:
| Event | Description |
|---|---|
BridgeTextStart |
Text output block begins |
BridgeTextContent |
Text content delta |
BridgeTextEnd |
Text output block ends |
The bridge overrides Python's print() with a preamble that routes
output through an internal __console_write__ host function. This
ensures print output is captured as bridge events rather than lost to
stdout.
Listening to Events
Use pattern matching to handle events:
await for (final event in bridge.execute(code)) {
switch (event) {
case BridgeRunStarted(:final threadId, :final runId):
print('Started: thread=$threadId run=$runId');
case BridgeFunctionCallStart(:final callId, :final name):
print('Calling: $name (call $callId)');
case BridgeFunctionCallResult(:final callId, :final result):
print('Result: $result (call $callId)');
case BridgeTextContent(:final delta):
print('Output: $delta');
case BridgeRunFinished(:final value, :final printOutput):
print('Done: value=$value output=$printOutput');
case BridgeRunError(:final message):
print('Error: $message');
default:
break;
}
}
JSON Schema Export
HostFunctionSchema.toJsonSchema() produces a standard JSON Schema
object compatible with MCP tool inputSchema:
final schema = HostFunctionSchema(
name: 'search',
description: 'Search by query.',
params: [
HostParam(name: 'query', type: HostParamType.string),
HostParam(name: 'limit', type: HostParamType.integer, isRequired: false),
],
);
print(schema.toJsonSchema());
// {
// "type": "object",
// "properties": {
// "query": {"type": "string"},
// "limit": {"type": "integer"}
// },
// "required": ["query"]
// }
For complex schemas (nested objects, enums, array item types), use
jsonSchemaOverride on individual HostParams:
HostParam(
name: 'filters',
type: HostParamType.map,
jsonSchemaOverride: {
'type': 'object',
'properties': {
'status': {'type': 'string', 'enum': ['active', 'archived']},
'tags': {'type': 'array', 'items': {'type': 'string'}},
},
},
)
Note: jsonSchemaOverride controls only the schema advertised to
external consumers (e.g., LLMs via MCP). Runtime validation in
validate() still uses the type field. Ensure the override is
consistent with type to avoid mismatches.
MontyBridge Configuration
MontyBridge accepts several constructor parameters:
MontyBridge(
platform: createPlatformMonty(), // Required: MontyPlatform backend
limits: MontyLimits( // Optional: resource constraints
timeoutMs: 5000,
memoryBytes: 10 * 1024 * 1024,
stackDepth: 100,
),
useFutures: true, // Optional: enable futures batching (default: true)
logger: myLogger, // Optional: custom Logger instance
)
Futures batching: When useFutures is true and the platform
supports MontyFutureCapable, host function calls are dispatched as
futures and resolved in batches. This allows Python to issue multiple
host function calls that execute concurrently on the Dart side. When
false, each call blocks until its handler completes.
Unregistering Functions
Functions can be removed at runtime:
bridge.register(myFunction);
// ...later...
bridge.unregister('my_function_name');
Both register() and unregister() throw StateError if the bridge
has been disposed.
Next Steps
The Intermediate guide covers organizing
functions into extensions with MontyExtension and ExtensionCoordinator, namespace
validation, lifecycle hooks, introspection builtins, and the
EventLoopExtension for bidirectional Python/Dart communication.