- 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
101 lines
5.0 KiB
Markdown
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 들 (각 핸들러가 사용)
|