import 'dart:convert'; /// Tool execution result. See design fn-tool_dispatcher.md §4. /// /// Always JSON-serialisable so the model can consume it. `toJson()` returns a /// shape with a stable `status` discriminator — easier for the LLM to parse /// than relying on key presence. sealed class ToolResult { const ToolResult(); Map toJson(); } final class ToolOk extends ToolResult { final Map data; const ToolOk(this.data); @override Map toJson() => {'status': 'ok', 'data': data}; } final class ToolErr extends ToolResult { final String code; final String reason; const ToolErr(this.code, this.reason); @override Map toJson() => {'status': 'error', 'code': code, 'reason': reason}; } final class ToolCancelled extends ToolResult { const ToolCancelled(); @override Map toJson() => {'status': 'cancelled', 'reason': 'user did not confirm'}; } /// Encode a [ToolResult] to a JSON string ≤ [maxBytes]. /// /// Tool result token budget (ADR-0005 / OQ-2): keep model context bounded. /// If serialised payload exceeds [maxBytes], we replace the tail of the data /// field with a truncation hint instead of letting the LLM blow its window. String encodeToolResult(ToolResult result, {int maxBytes = 2048}) { final encoded = jsonEncode(result.toJson()); if (encoded.length <= maxBytes) return encoded; // Truncate strategy: only ToolOk has unbounded payload. Replace data with // a hint pointing at follow-up tools. Errors/cancellations are always small. if (result is ToolOk) { final hint = { 'status': 'ok', 'data': { '_truncated': true, '_hint': '결과가 ${encoded.length} 바이트로 잘렸습니다. ' '구체 ID 가 필요하면 query_protocol 같은 단건 조회 도구를 사용하세요.', }, }; return jsonEncode(hint); } // Defensive: if some future ToolResult adds bulk, fall back to hard cut. return encoded.substring(0, maxBytes); }