dynamicMessageTool function
Creates the dynamic_message tool bound to callback.
When callback is null (headless/non-interactive host), executing the
tool throws — the agent loop converts it into an error tool result
telling the model this host cannot present widgets (the safe fallback).
Approval tier is ApprovalTier.read: presenting a widget mutates
nothing by itself — the user interacts with it in the host's own chat
surface. Execution is forced to ToolExecutionMode.sequential (like
ask): concurrent presentations would clobber the host's single chat
surface.
Implementation
AgentTool dynamicMessageTool({DynamicMessageCallback? callback}) {
return AgentTool(
name: 'dynamic_message',
label: 'dynamic_message',
tier: ApprovalTier.read,
executionMode: ToolExecutionMode.sequential,
description: dynamicMessageToolDescriptionPrompt,
parameters: const {
'type': 'object',
'properties': {
'title': {
'type': 'string',
'description':
'Short widget title shown above the widget and '
'used to prefix its events back to the model',
},
'jsSource': {
'type': 'string',
'description':
'Widget JavaScript source (at most 65536 UTF-8 bytes); uses '
'the same jsr.fa bridges as installed apps',
},
'initialState': {
'type': 'object',
'description':
'Optional initial state object handed to the '
'widget on boot',
},
'heightHint': {
'type': 'number',
'description': 'Preferred widget height in logical pixels (> 0)',
},
},
'required': ['title', 'jsSource'],
},
execute: (arguments, cancelToken, onUpdate) async {
cancelToken?.throwIfCancelled();
final title = _validateTitle(arguments['title']);
final jsSource = _validateJsSource(arguments['jsSource']);
final initialState = _validateInitialState(arguments['initialState']);
final heightHint = _validateHeightHint(arguments['heightHint']);
final present = callback;
if (present == null) {
throw StateError(
'This host cannot present interactive widgets (no dynamic message '
'surface is installed). Present your content as plain text '
'instead.',
);
}
final request = DynamicMessageRequest(
title: title,
jsSource: jsSource,
initialState: initialState,
heightHint: heightHint,
);
final id = await _awaitPresent(present, request, cancelToken);
if (id == null) {
return ToolExecutionResult.text(
'The host declined to present the widget (per-run presentation '
'cap reached or no chat session). Present your content as plain '
'text instead.',
);
}
return ToolExecutionResult.text(
"Dynamic message '$title' presented to the user (widget $id). "
'User interactions with it arrive as [widget $title] user messages.',
);
},
);
}