samplingMessagesProblem function
Why the messages of a sampling/createMessage request break the
sampling rules, or null (specification, client/sampling: message content
constraints, 2025-11-25+):
- every message has role
userorassistantand content; - a user message with a
tool_resultcontains only tool results; - an assistant message with
tool_useblocks is followed by a user message of tool results answering every one of them bytoolUseId.
Implementation
String? samplingMessagesProblem(Object? messages) {
if (messages is! List) return '"messages" must be an array';
for (var i = 0; i < messages.length; i++) {
final m = messages[i];
if (m is! Map) return 'messages[$i] is not an object';
final role = m['role'];
if (role != 'user' && role != 'assistant') {
return 'messages[$i].role must be "user" or "assistant"';
}
if (!m.containsKey('content')) return 'messages[$i] has no content';
final blocks = _blocks(m['content']);
final types = [for (final b in blocks) b is Map ? b['type'] : null];
if (role == 'user' && types.contains('tool_result') && types.any((t) => t != 'tool_result')) {
return 'messages[$i] mixes tool results with other content';
}
if (role == 'assistant' && types.contains('tool_use')) {
final ids = {
for (final b in blocks)
if (b is Map && b['type'] == 'tool_use') b['id'],
};
final next = i + 1 < messages.length ? messages[i + 1] : null;
if (next is! Map || next['role'] != 'user') {
return 'messages[$i] uses tools but is not followed by their results';
}
final results = _blocks(next['content']);
if (results.any((b) => b is! Map || b['type'] != 'tool_result')) {
return 'messages[${i + 1}] must contain only tool results';
}
final answered = {for (final b in results) (b as Map)['toolUseId']};
final missing = ids.difference(answered);
if (missing.isNotEmpty) return 'tool result missing for ${missing.join(', ')}';
}
}
return null;
}