[03-Developer] #260 in-app tool calling (Gemma 4 multi-turn)
ADR-0005 in-process tool runtime — 6 tools (catalog 2 + tracker 2 + habit 2), ToolDispatcher with JSON-schema validation + modal ConfirmGate for destructive ops, multi-turn LlmChatSession abstraction wired to flutter_gemma 0.16.5 (ToolChoice.auto), ChatSessionController with MAX_TURNS=4 safety + 8-turn history hint, ChatScreen entry behind AI opt-in. R3/R7/R8 enforced inside handlers. 41 new tests (envelope, catalog/tracker/habit tools, dispatcher, controller loop) — 151 total passing. Refs #260
This commit is contained in:
63
app/lib/ai/tools/tool_envelope.dart
Normal file
63
app/lib/ai/tools/tool_envelope.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
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<String, dynamic> toJson();
|
||||
}
|
||||
|
||||
final class ToolOk extends ToolResult {
|
||||
final Map<String, dynamic> data;
|
||||
const ToolOk(this.data);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {'status': 'ok', 'data': data};
|
||||
}
|
||||
|
||||
final class ToolErr extends ToolResult {
|
||||
final String code;
|
||||
final String reason;
|
||||
const ToolErr(this.code, this.reason);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() =>
|
||||
{'status': 'error', 'code': code, 'reason': reason};
|
||||
}
|
||||
|
||||
final class ToolCancelled extends ToolResult {
|
||||
const ToolCancelled();
|
||||
|
||||
@override
|
||||
Map<String, dynamic> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user