# 함수 설계서: `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 dispatch({ required String toolName, required Map 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 | 어떤 타입이든 — 검증은 내부에서 | LLM 의 `FunctionCallResponse.args` | | `confirmContext` | BuildContext? | 살아있는 widget context | destructive 가 아니면 무관. null + destructive = 자동 cancel | | `deps` | ToolDeps | non-null | 핸들러가 호출할 Repository 묶음 | ## 4. 출력 - **반환**: `Future` — `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`). - 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 들 (각 핸들러가 사용)