Files
life-helper/docs/design/260-gemma-tool-calling/fn-tool_dispatcher.md
joungmin eca097aa2c [02-Architect] #260 design spec + ADR-0005
- design/260-gemma-tool-calling/README.md — overall (12 AC + 7 OQ + 모듈 구조)
- fn-tool_dispatcher.md — multi-tool router (validate → confirm gate → handler → envelope)
- fn-add_habit_handler.md — destructive 대표 (R3/R7/R8 enforce)
- fn-confirm_gate.md — 모달 AlertDialog 흐름 (OQ-3 = 모달 확정)
- fn-chat_session_controller.md — multi-turn loop 상태 머신 (MAX_TURNS=4)
- ADR-0005 — in-app tool runtime + R 규칙 = 핸들러 책임 + schema SoT=Dart + 모달

OQ-1/2/4 = README §11 결정. OQ-3 = 모달 (사용자 결정).
신규 OQ-5/6/7 = Developer 가 구현 중 검증.

Refs #260
2026-06-15 10:15:44 +09:00

101 lines
5.0 KiB
Markdown

# 함수 설계서: `ToolDispatcher.dispatch` (#260)
> **부모 설계서**: ./README.md · **상태**: Draft
> **작성**: [AI] Architect · **구현**: `lib/ai/tools/tool_dispatcher.dart:dispatch` · **테스트**: `test/ai/tools/tool_dispatcher_test.dart`
## 1. 시그니처
```dart
Future<ToolResult> dispatch({
required String toolName,
required Map<String, dynamic> rawArgs,
required BuildContext? confirmContext, // null 이면 destructive tool 자동 cancel
required ToolDeps deps,
});
```
`ToolDeps` = `{ HabitDao habitDao, TrackerDao trackerDao, CatalogRepository catalog, String userId }`.
## 2. 책임 (단일 책임, 1줄)
`toolName` 으로 핸들러를 찾아, args 검증 → Confirm gate → 핸들러 호출 → 결과를 envelope 으로 감싸 반환한다.
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|---|---|---|---|
| `toolName` | String | non-empty, registry 에 등록된 이름 | LLM 의 `FunctionCallResponse.name` |
| `rawArgs` | Map<String, dynamic> | 어떤 타입이든 — 검증은 내부에서 | LLM 의 `FunctionCallResponse.args` |
| `confirmContext` | BuildContext? | 살아있는 widget context | destructive 가 아니면 무관. null + destructive = 자동 cancel |
| `deps` | ToolDeps | non-null | 핸들러가 호출할 Repository 묶음 |
## 4. 출력
- **반환**: `Future<ToolResult>``ToolOk` / `ToolErr` / `ToolCancelled`. 절대 throw 하지 않음.
- **부수효과**:
- Confirm gate 호출 시 모달 표시 (UI side effect)
- 핸들러 내부에서 DB write 가능 (destructive 인 경우만)
## 5. 동작 / 알고리즘
```
1. tool = ToolRegistry.byName(toolName)
if tool == null:
return ToolErr(code: 'unknown_tool', reason: '알 수 없는 도구: $toolName')
2. validatedArgs = ToolArgsValidator.validate(tool.parametersSchema, rawArgs)
if validatedArgs is ValidationError:
return ToolErr(code: 'validation', reason: '인자 오류: ${err.message}')
3. if tool.isDestructive:
if confirmContext == null:
return ToolCancelled()
ok = await ConfirmGate.show(confirmContext, tool, validatedArgs)
if !ok:
return ToolCancelled()
4. try:
payload = await tool.handler(validatedArgs, deps)
// handler 가 이미 ToolResult 를 반환하는 형태이므로 passthrough
return payload
catch (e, st):
log('tool_error', tool=$toolName, err=$e)
return ToolErr(code: 'handler_error', reason: '도구 실행 실패: ${e.runtimeType}')
```
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환 |
|---|---|---|
| `toolName` 미등록 | log warn | `ToolErr('unknown_tool', ...)` |
| `rawArgs` schema 위배 | log info | `ToolErr('validation', ...)` |
| destructive + confirmContext null | 조용히 | `ToolCancelled()` |
| 사용자 모달 거부 | 조용히 | `ToolCancelled()` |
| 핸들러 예외 | log error + stacktrace | `ToolErr('handler_error', ...)` — 사용자 메시지엔 타입만 |
| 핸들러가 R 규칙 위배 detect | 핸들러 자체에서 반환 | passthrough `ToolErr('r3_quota', ...)` 등 |
**불변식**: dispatch 는 throw 하지 않는다. 모든 실패 경로는 ToolResult 로 환원.
## 7. 엣지케이스
- **빈 args**: `{}` 가 들어와도 schema 가 required 필드 검증으로 잡음.
- **redundant args** (스키마에 없는 키): 무시 — 모델이 환각해도 통과시키되 로깅.
- **모달 race**: confirmContext 가 dispatch 호출 후 dispose 되는 경우 → `ConfirmGate` 내부에서 `context.mounted` 체크 후 false 반환.
- **dispatch 중 사용자가 chat 화면 dismiss**: 핸들러는 계속 실행됨 (취소 안 함). 결과는 폐기되지만 DB write 는 commit 된 채 남음. ChatSessionController 가 lifecycle 책임짐 (Architect 결정: side effect 보존 = 사용자가 의도적으로 chat 닫았다고 가정).
## 8. 복잡도 / 성능
- O(1) registry lookup (`Map<String, ToolDefinition>`).
- args validate ≤ 50자 keyword 등 small payload — O(n) JSON schema 매칭.
- 호출 빈도: 사용자 대화 turn 당 0~N (보통 0 또는 1). 폴링 루프 아님.
- 메모리: stateless — instance 변수 없음.
## 9. 테스트 케이스 (필수)
| 케이스 | 입력 | 기대 |
|---|---|---|
| unknown tool | `dispatch('foo', {}, ...)` | `ToolErr('unknown_tool', ...)` |
| validation fail | `dispatch('add_habit', {'protocol_id': 123}, ...)` | `ToolErr('validation', ...)` (123 is int not string) |
| destructive + null context | `dispatch('add_habit', validArgs, null, ...)` | `ToolCancelled()` |
| destructive + user accept | mock ConfirmGate → true | handler 결과 그대로 |
| destructive + user reject | mock ConfirmGate → false | `ToolCancelled()` |
| handler throw | mock handler throws | `ToolErr('handler_error', ...)` |
| read-only normal | `dispatch('search_catalog', validArgs, null, ...)` | `ToolOk(data:...)` |
## 10. 의존
- `ToolRegistry` (정적 lookup)
- `ToolArgsValidator` (JSON schema validator — 간단 자체 구현 권장)
- `ConfirmGate.show` (UI)
- `ToolDeps` 내부의 Repository 들 (각 핸들러가 사용)