sendMessage method
Send a user message and run the agentic loop.
Implementation
Future<ConversationTurn> sendMessage(
String text, {
List<Map<String, dynamic>>? attachments,
}) async {
_cancelled = false;
final stopwatch = Stopwatch()..start();
// Build user message
final userContent = <Map<String, dynamic>>[];
if (text.isNotEmpty) {
userContent.add({'type': 'text', 'text': text});
}
if (attachments != null) {
userContent.addAll(attachments);
}
final userMsg = {'role': 'user', 'content': userContent};
_messages.add(userMsg);
final toolExecutions = <ToolExecution>[];
int turnInputTokens = 0;
int turnOutputTokens = 0;
try {
_setState(ConversationState.streaming);
// Agentic loop — keep calling API until no more tool_use
var loopCount = 0;
while (loopCount < _config.maxTurns && !_cancelled) {
loopCount++;
// Build API request
final request = _buildRequest();
// Stream the response
final response = await _streamResponse(request);
turnInputTokens += response.inputTokens;
turnOutputTokens += response.outputTokens;
// Add assistant message to history
_messages.add({'role': 'assistant', 'content': response.contentBlocks});
// Check if there are tool_use blocks
final toolUseBlocks = response.contentBlocks
.where((b) => b['type'] == 'tool_use')
.toList();
if (toolUseBlocks.isEmpty || response.stopReason != 'tool_use') {
// No more tool calls — turn is complete
break;
}
// Execute tools
_setState(ConversationState.executingTool);
final toolResults = <Map<String, dynamic>>[];
for (final toolBlock in toolUseBlocks) {
if (_cancelled) break;
final toolName = toolBlock['name'] as String;
final toolId = toolBlock['id'] as String;
final toolInput = toolBlock['input'] as Map<String, dynamic>? ?? {};
_events.add(ToolUseRequested(toolName, toolId, toolInput));
// Check permission
final permitted = await _checkPermission(toolName, toolInput);
if (!permitted) {
toolResults.add({
'type': 'tool_result',
'tool_use_id': toolId,
'content': 'Permission denied by user.',
'is_error': true,
});
toolExecutions.add(
ToolExecution(
toolName: toolName,
toolUseId: toolId,
input: toolInput,
output: 'Permission denied by user.',
isError: true,
duration: Duration.zero,
permissionGranted: false,
),
);
continue;
}
// Execute the tool
_events.add(ToolExecutionStarted(toolName, toolId));
final execStopwatch = Stopwatch()..start();
try {
final resolvedName = _config.toolAliases?[toolName] ?? toolName;
final tool = _toolRegistry.get(resolvedName);
String result;
bool isError = false;
if (tool != null) {
final output = await tool
.execute(toolInput)
.timeout(_config.toolTimeout);
result = output.content;
} else {
result = 'Tool "$toolName" not found.';
isError = true;
}
execStopwatch.stop();
// Truncate very long outputs
if (result.length > 100000) {
result =
'${result.substring(0, 100000)}\n\n[Output truncated — ${result.length} chars total]';
}
toolResults.add({
'type': 'tool_result',
'tool_use_id': toolId,
'content': result,
if (isError) 'is_error': true,
});
final execution = ToolExecution(
toolName: toolName,
toolUseId: toolId,
input: toolInput,
output: result,
isError: isError,
duration: execStopwatch.elapsed,
permissionGranted: true,
);
toolExecutions.add(execution);
_events.add(ToolExecutionCompleted(execution));
} on TimeoutException {
execStopwatch.stop();
toolResults.add({
'type': 'tool_result',
'tool_use_id': toolId,
'content':
'Tool execution timed out after ${_config.toolTimeout.inSeconds}s.',
'is_error': true,
});
toolExecutions.add(
ToolExecution(
toolName: toolName,
toolUseId: toolId,
input: toolInput,
output: 'Timeout',
isError: true,
duration: execStopwatch.elapsed,
),
);
} catch (e) {
execStopwatch.stop();
toolResults.add({
'type': 'tool_result',
'tool_use_id': toolId,
'content': 'Error: $e',
'is_error': true,
});
toolExecutions.add(
ToolExecution(
toolName: toolName,
toolUseId: toolId,
input: toolInput,
output: 'Error: $e',
isError: true,
duration: execStopwatch.elapsed,
),
);
}
}
// Add tool results as user message
_messages.add({'role': 'user', 'content': toolResults});
// Continue the loop (next API call will see tool results)
_setState(ConversationState.streaming);
}
stopwatch.stop();
// Build turn summary
final userMessage = Message(
role: MessageRole.user,
content: [TextBlock(text)],
);
final assistantText = _extractAssistantText();
final assistantMessage = Message(
role: MessageRole.assistant,
content: [TextBlock(assistantText)],
);
final turn = ConversationTurn(
userMessage: userMessage,
assistantMessage: assistantMessage,
toolExecutions: toolExecutions,
duration: stopwatch.elapsed,
inputTokens: turnInputTokens,
outputTokens: turnOutputTokens,
cost: _estimateCost(turnInputTokens, turnOutputTokens),
turnIndex: _turns.length,
);
_turns.add(turn);
_totalInputTokens += turnInputTokens;
_totalOutputTokens += turnOutputTokens;
_events.add(
TokenUsageUpdated(
_totalInputTokens,
_totalOutputTokens,
_totalCacheReadTokens,
_totalCacheCreationTokens,
),
);
_events.add(TurnCompleted(turn));
_setState(ConversationState.idle);
return turn;
} catch (e, st) {
stopwatch.stop();
_events.add(ConversationError(e, st));
_setState(ConversationState.error);
rethrow;
}
}