refactor: 제거 기능 stale surface 정리 — 세션/wire 필드/rag_multi_turn/crate수 (PR #214 회차 1)
cut 으로 제거된 기능이 diff 밖 contract surface 에 남긴 잔재 정리: - integrations/claude-code/kebab/SKILL.md: 제거된 --session/session_id 안내, answer.v1 의 conversation_id/turn_index 필드 목록, rag-v2 기본 pin, kebab tui 언급 제거. 기본 템플릿 rag-v4(대체 rag-v3)로 갱신. - docs/mcp-usage.md: ask 입력 스키마의 session_id, 'Session 관리(multi-turn)' 섹션 전체(chat_sessions/chat_turns 포함), 예시의 conversation_id/turn_index 제거. - docs/wire-schema/v1/answer.schema.json + answer_event.schema.json: Answer 가 더는 방출 않는 conversation_id/turn_index 속성 제거(producer/doc 괴리 해소). - schema.rs + wire.rs: capabilities.rag_multi_turn true→false(search_cache 와 동일 처리; frozen v1 맵이라 키는 유지, 값만 flip). - CLAUDE.md: 24→22 crates(tui+candle crate 제거 반영).
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
Single-user local-first knowledge base + RAG. Rust 2024 workspace, 24 crates, single binary (`kebab`). All inference is local (Ollama + fastembed + whisper.cpp).
|
Single-user local-first knowledge base + RAG. Rust 2024 workspace, 22 crates, single binary (`kebab`). All inference is local (Ollama + fastembed + whisper.cpp).
|
||||||
|
|
||||||
The repo's documentation is split by audience — don't duplicate across them:
|
The repo's documentation is split by audience — don't duplicate across them:
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ fn capabilities_snapshot() -> Capabilities {
|
|||||||
json_mode: true,
|
json_mode: true,
|
||||||
ingest_progress: true,
|
ingest_progress: true,
|
||||||
ingest_cancellation: true,
|
ingest_cancellation: true,
|
||||||
rag_multi_turn: true,
|
rag_multi_turn: false,
|
||||||
search_cache: false,
|
search_cache: false,
|
||||||
incremental_ingest: true,
|
incremental_ingest: true,
|
||||||
streaming_ask: true,
|
streaming_ask: true,
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ mod tests {
|
|||||||
json_mode: true,
|
json_mode: true,
|
||||||
ingest_progress: true,
|
ingest_progress: true,
|
||||||
ingest_cancellation: true,
|
ingest_cancellation: true,
|
||||||
rag_multi_turn: true,
|
rag_multi_turn: false,
|
||||||
search_cache: false,
|
search_cache: false,
|
||||||
incremental_ingest: true,
|
incremental_ingest: true,
|
||||||
streaming_ask: false,
|
streaming_ask: false,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ stdio JSON-RPC MCP 표준을 따르는 모든 host 가 지원. 위 형식 (`comm
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Input | `{ "query": string, "session_id"?: string, "mode"?: "lexical"\|"vector"\|"hybrid" }` |
|
| Input | `{ "query": string, "mode"?: "lexical"\|"vector"\|"hybrid" }` |
|
||||||
| Defaults | `mode = "hybrid"` |
|
| Defaults | `mode = "hybrid"` |
|
||||||
| Output | `answer.v1` (single object) |
|
| Output | `answer.v1` (single object) |
|
||||||
|
|
||||||
@@ -186,8 +186,7 @@ stdio JSON-RPC MCP 표준을 따르는 모든 host 가 지원. 위 형식 (`comm
|
|||||||
{
|
{
|
||||||
"name": "ask",
|
"name": "ask",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
"query": "What's our internal Kubernetes ingress setup?",
|
"query": "What's our internal Kubernetes ingress setup?"
|
||||||
"session_id": "ops-onboarding-2026-05"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -201,16 +200,12 @@ stdio JSON-RPC MCP 표준을 따르는 모든 host 가 지원. 위 형식 (`comm
|
|||||||
"citations": [ ... ],
|
"citations": [ ... ],
|
||||||
"grounded": true,
|
"grounded": true,
|
||||||
"refusal_reason": null,
|
"refusal_reason": null,
|
||||||
"model": { ... },
|
"model": { ... }
|
||||||
"conversation_id": "...",
|
|
||||||
"turn_index": 0
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**`grounded: false` 처리**: KB 에 충분한 context 없음. `refusal_reason` 확인 후 사용자에게 \"KB 에 정보 없음\" 으로 안내, 본인 지식 fallback 또는 source 요청. **paraphrase 하면 안 됨** (hallucination 위험).
|
**`grounded: false` 처리**: KB 에 충분한 context 없음. `refusal_reason` 확인 후 사용자에게 \"KB 에 정보 없음\" 으로 안내, 본인 지식 fallback 또는 source 요청. **paraphrase 하면 안 됨** (hallucination 위험).
|
||||||
|
|
||||||
multi-turn 은 [Session 관리](#session-관리-multi-turn-ask) 참조.
|
|
||||||
|
|
||||||
### `schema` — capability discovery
|
### `schema` — capability discovery
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
@@ -233,7 +228,7 @@ multi-turn 은 [Session 관리](#session-관리-multi-turn-ask) 참조.
|
|||||||
"wire": { "schemas": ["answer.v1", "search_hit.v1", ...] },
|
"wire": { "schemas": ["answer.v1", "search_hit.v1", ...] },
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"json_mode": true,
|
"json_mode": true,
|
||||||
"rag_multi_turn": true,
|
"rag_multi_turn": false,
|
||||||
"mcp_server": true,
|
"mcp_server": true,
|
||||||
"streaming_ask": false,
|
"streaming_ask": false,
|
||||||
...
|
...
|
||||||
@@ -398,65 +393,6 @@ tool dispatch 가 `Err` 반환 시. content 의 `error.v1` JSON 의 `code` 로
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Session 관리 (multi-turn ask)
|
|
||||||
|
|
||||||
`ask` tool 의 `session_id` 가 multi-turn RAG context 활성화. 같은 `session_id` 로 연속 호출 시 이전 Q/A history 가 새 query 의 retrieval expansion + prompt context 에 포함.
|
|
||||||
|
|
||||||
### session_id 명명
|
|
||||||
|
|
||||||
`<topic>-<date>` 형식 권장 — 사용자 친화 + uniqueness:
|
|
||||||
|
|
||||||
- `ops-onboarding-2026-05`
|
|
||||||
- `kubernetes-ingress-debug-2026-05-07`
|
|
||||||
- `agent-research-session-1` (auto-numbered)
|
|
||||||
|
|
||||||
session_id 는 임의 string — kebab 이 처음 보는 id 면 새 session 생성, 기존 id 면 history append.
|
|
||||||
|
|
||||||
### 언제 새 session 시작?
|
|
||||||
|
|
||||||
- 주제 완전 전환 (KB 의 다른 도메인) — 이전 history 가 noise.
|
|
||||||
- 사용자 명시 reset 요청.
|
|
||||||
- Long session (50+ turn) 의 context bloat — 새 session 으로 fresh start.
|
|
||||||
|
|
||||||
### Session lifetime
|
|
||||||
|
|
||||||
session 데이터는 SQLite `chat_sessions` + `chat_turns` 에 영속. `kebab reset --data-only` 가 모두 wipe. session 별 삭제 명령은 없음 (P+).
|
|
||||||
|
|
||||||
### 예시 multi-turn flow
|
|
||||||
|
|
||||||
```json
|
|
||||||
// turn 1
|
|
||||||
{ "name": "ask", "arguments": {
|
|
||||||
"query": "What's our internal Kubernetes ingress setup?",
|
|
||||||
"session_id": "ops-2026-05"
|
|
||||||
}}
|
|
||||||
// → answer.v1 with conversation_id, turn_index: 0
|
|
||||||
|
|
||||||
// turn 2 — 이전 답변을 context 로 retrieval expansion
|
|
||||||
{ "name": "ask", "arguments": {
|
|
||||||
"query": "What about TLS?",
|
|
||||||
"session_id": "ops-2026-05"
|
|
||||||
}}
|
|
||||||
// → kebab 가 "TLS" 만으로 retrieval 안 함, 이전 \"Kubernetes ingress\" history 포함 query 로 검색
|
|
||||||
|
|
||||||
// turn 3 — 명시적 reference
|
|
||||||
{ "name": "ask", "arguments": {
|
|
||||||
"query": "How does that compare to AWS ALB?",
|
|
||||||
"session_id": "ops-2026-05"
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Session vs single-shot
|
|
||||||
|
|
||||||
`session_id` 없이 `ask` 호출 = single-shot. agent host 자체가 conversation 추적하면 single-shot + agent-side context 도 OK. session 이 필요한 경우:
|
|
||||||
|
|
||||||
- KB 가 \"이전 질문\" 을 retrieval expansion 에 사용해야 정확 (e.g. follow-up 의 대명사).
|
|
||||||
- 한 session 안에서 같은 chunk 반복 fetch 회피 (kebab 가 turn 간 chunk overlap 인지).
|
|
||||||
|
|
||||||
agent host 가 conversation 추적 + 충분한 context 보유면 session 불필요.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
- **첫 tool call**: cold start ~1-2s (SQLite open + Lance dataset open + fastembed model load).
|
- **첫 tool call**: cold start ~1-2s (SQLite open + Lance dataset open + fastembed model load).
|
||||||
|
|||||||
@@ -45,15 +45,6 @@
|
|||||||
"retrieval": { "type": "object" },
|
"retrieval": { "type": "object" },
|
||||||
"usage": { "type": "object" },
|
"usage": { "type": "object" },
|
||||||
"created_at": { "type": "string", "format": "date-time" },
|
"created_at": { "type": "string", "format": "date-time" },
|
||||||
"conversation_id": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"description": "p9-fb-15: same conversation 의 turn 들이 공유. CLI single-shot / TUI 첫 turn 은 null."
|
|
||||||
},
|
|
||||||
"turn_index": {
|
|
||||||
"type": ["integer", "null"],
|
|
||||||
"minimum": 0,
|
|
||||||
"description": "p9-fb-15: 같은 conversation 안 0-based 순서. null 이면 single-shot."
|
|
||||||
},
|
|
||||||
"hops": {
|
"hops": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"ts": { "type": "string", "format": "date-time" },
|
"ts": { "type": "string", "format": "date-time" },
|
||||||
"hits": { "type": "array", "description": "retrieval_done: search_hit.v1[]" },
|
"hits": { "type": "array", "description": "retrieval_done: search_hit.v1[]" },
|
||||||
"delta": { "type": "string", "description": "token: incremental string chunk" },
|
"delta": { "type": "string", "description": "token: incremental string chunk" },
|
||||||
"turn_index": { "type": ["integer", "null"], "minimum": 0, "description": "token: matches Answer.turn_index" },
|
|
||||||
"answer": { "type": "object", "description": "final: complete answer.v1 payload" }
|
"answer": { "type": "object", "description": "final: complete answer.v1 payload" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,13 +80,12 @@ Use when the user wants a synthesized answer, not a list of links.
|
|||||||
|
|
||||||
Input:
|
Input:
|
||||||
```json
|
```json
|
||||||
{ "query": "<question>", "session_id": "<optional-slug>", "mode": "hybrid", "multi_hop": false }
|
{ "query": "<question>", "mode": "hybrid", "multi_hop": false }
|
||||||
```
|
```
|
||||||
|
|
||||||
- Returns `answer.v1`: `answer` (markdown), `citations[]`, `grounded` (bool), `refusal_reason`, `model`, `conversation_id`, `turn_index`, `hops` (multi-hop only).
|
- Returns `answer.v1`: `answer` (markdown), `citations[]`, `grounded` (bool), `refusal_reason`, `model`, `hops` (multi-hop only).
|
||||||
- **If `grounded == false`** → KB doesn't have enough context. Don't paraphrase the refusal as if it were an answer. Tell the user the KB came up dry and fall back to your own knowledge or ask for the source.
|
- **If `grounded == false`** → KB doesn't have enough context. Don't paraphrase the refusal as if it were an answer. Tell the user the KB came up dry and fall back to your own knowledge or ask for the source.
|
||||||
- For follow-up turns on the same topic, pass `session_id` (e.g. `"team-onboarding-2026-05"`) and reuse it across the conversation. Sessions persist until `kebab reset --data-only`.
|
- p9-fb-40: 기본 `prompt_template_version = "rag-v4"`. 답변이 strict — fact 인용 시 verbatim span, 학습 지식 동원 금지, 근거 모호 시 "확실하지 않다" 출현 가능. `[rag] prompt_template_version = "rag-v3"` 로 이전 템플릿 선택 가능.
|
||||||
- p9-fb-40: 기본 `prompt_template_version = "rag-v2"`. 답변이 더 strict — fact 인용 시 verbatim span, 학습 지식 동원 금지, 근거 모호 시 "확실하지 않다" 출현 가능. user 가 `[rag] prompt_template_version = "rag-v1"` 명시 시 legacy 동작.
|
|
||||||
- **p9-fb-41 `multi_hop: true`** — opt the ask into the multi-hop pipeline. The query is decomposed into sub-questions, each retrieved independently (LLM-driven decide loop, up to `rag.multi_hop_max_depth` iters), then synthesized over the merged chunk pool. Cost trade-off: 2–5× LLM calls vs. single-pass. **Use** for compound questions ("X 와 Y 의 차이는?", prereq chains, cross-doc reasoning where one chunk alone is insufficient). **Don't** for simple fact-finding (single-pass is faster + cheaper). When set, `answer.v1.hops[]` carries the per-hop trace (`{iter, kind, sub_queries[], context_chunks_added, forced_stop, llm_call_ms}`) — surface a brief "Searched in N hops" note when the trace is non-trivial. Decompose-failure (model emitted non-JSON) → `refusal_reason = "multi_hop_decompose_failed"`; treat like any other refusal.
|
- **p9-fb-41 `multi_hop: true`** — opt the ask into the multi-hop pipeline. The query is decomposed into sub-questions, each retrieved independently (LLM-driven decide loop, up to `rag.multi_hop_max_depth` iters), then synthesized over the merged chunk pool. Cost trade-off: 2–5× LLM calls vs. single-pass. **Use** for compound questions ("X 와 Y 의 차이는?", prereq chains, cross-doc reasoning where one chunk alone is insufficient). **Don't** for simple fact-finding (single-pass is faster + cheaper). When set, `answer.v1.hops[]` carries the per-hop trace (`{iter, kind, sub_queries[], context_chunks_added, forced_stop, llm_call_ms}`) — surface a brief "Searched in N hops" note when the trace is non-trivial. Decompose-failure (model emitted non-JSON) → `refusal_reason = "multi_hop_decompose_failed"`; treat like any other refusal.
|
||||||
- **v0.18+ multi-hop NLI verification** — multi-hop ask (`mcp__kebab__ask` with `multi_hop: true`) runs a post-synthesize NLI groundedness gate when `[rag] nli_threshold > 0` is set in the user's config. `answer.v1.verification.nli_passed == true` means the generated answer is entailed by the retrieved chunks (grounded); `false` means the answer is refused with `refusal_reason = "nli_verification_failed"` and the `verification` block still ships so the agent can show what entailment score was rejected. Threshold tuning: 0.5 is the production default, 0.9 is strict mode. If the NLI model download / inference fails the pipeline emits `refusal_reason = "nli_model_unavailable"` — user-side workaround is `[rag] nli_threshold = 0` then retry multi-hop. Single-pass `ask` (multi_hop: false / unset) is unaffected — it keeps the LLM self-judge gate as the only verification.
|
- **v0.18+ multi-hop NLI verification** — multi-hop ask (`mcp__kebab__ask` with `multi_hop: true`) runs a post-synthesize NLI groundedness gate when `[rag] nli_threshold > 0` is set in the user's config. `answer.v1.verification.nli_passed == true` means the generated answer is entailed by the retrieved chunks (grounded); `false` means the answer is refused with `refusal_reason = "nli_verification_failed"` and the `verification` block still ships so the agent can show what entailment score was rejected. Threshold tuning: 0.5 is the production default, 0.9 is strict mode. If the NLI model download / inference fails the pipeline emits `refusal_reason = "nli_model_unavailable"` — user-side workaround is `[rag] nli_threshold = 0` then retry multi-hop. Single-pass `ask` (multi_hop: false / unset) is unaffected — it keeps the LLM self-judge gate as the only verification.
|
||||||
|
|
||||||
@@ -113,7 +112,6 @@ If MCP tools aren't in scope (host without MCP support, or `mcp.json` not config
|
|||||||
```bash
|
```bash
|
||||||
kebab search "<query>" --mode hybrid --json 2>/dev/null
|
kebab search "<query>" --mode hybrid --json 2>/dev/null
|
||||||
kebab ask "<question>" --json 2>/dev/null
|
kebab ask "<question>" --json 2>/dev/null
|
||||||
kebab ask "<question>" --session <stable-id> --json 2>/dev/null
|
|
||||||
kebab ask "<question>" --stream # ndjson answer_event.v1 on stderr, final answer.v1 on stdout
|
kebab ask "<question>" --stream # ndjson answer_event.v1 on stderr, final answer.v1 on stdout
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -151,9 +149,9 @@ Claude Code spawns `kebab mcp` at session start; the process stays alive across
|
|||||||
|
|
||||||
## Capability discovery
|
## Capability discovery
|
||||||
|
|
||||||
Before using streaming or multi-turn features, probe what this binary supports — call `mcp__kebab__schema` (or CLI `kebab schema --json`):
|
Before using streaming features, probe what this binary supports — call `mcp__kebab__schema` (or CLI `kebab schema --json`):
|
||||||
|
|
||||||
Returns `schema.v1`: `wire.schemas` (supported wire ids), `capabilities` (bool flags — e.g. `streaming_ask`, `rag_multi_turn`), `models` (version cascade 6-axis + v0.20.1 `active_parsers` / `active_chunkers` arrays for multi-version corpora), `stats` (doc/chunk/asset count + last_ingest_at, plus p9-fb-37 health surface: `media_breakdown` per-kind doc counts (5 zero-padded keys: markdown / pdf / image / audio / other), `lang_breakdown` per BCP-47 lang (NULL keyed as the literal string `"null"`), `index_bytes.{sqlite,lancedb}` on-disk byte sums, `stale_doc_count` for docs older than `config.search.stale_threshold_days`). Gate streaming / session flows on `capabilities.streaming_ask` / `capabilities.rag_multi_turn` being `true`. Cheap call (no LLM), once per session.
|
Returns `schema.v1`: `wire.schemas` (supported wire ids), `capabilities` (bool flags — e.g. `streaming_ask`, `rag_multi_turn`), `models` (version cascade 6-axis + v0.20.1 `active_parsers` / `active_chunkers` arrays for multi-version corpora), `stats` (doc/chunk/asset count + last_ingest_at, plus p9-fb-37 health surface: `media_breakdown` per-kind doc counts (5 zero-padded keys: markdown / pdf / image / audio / other), `lang_breakdown` per BCP-47 lang (NULL keyed as the literal string `"null"`), `index_bytes.{sqlite,lancedb}` on-disk byte sums, `stale_doc_count` for docs older than `config.search.stale_threshold_days`). Gate streaming flows on `capabilities.streaming_ask` being `true`. Cheap call (no LLM), once per session.
|
||||||
|
|
||||||
## Quick health check
|
## Quick health check
|
||||||
|
|
||||||
@@ -198,4 +196,3 @@ For files already on disk the user references, prefer `mcp__kebab__ingest_file`
|
|||||||
- Don't auto-invoke `mcp__kebab__ingest_file` / `mcp__kebab__ingest_stdin` / `kebab ingest` / `kebab reset` / `kebab init`. Those mutate state — the user must explicitly request.
|
- Don't auto-invoke `mcp__kebab__ingest_file` / `mcp__kebab__ingest_stdin` / `kebab ingest` / `kebab reset` / `kebab init`. Those mutate state — the user must explicitly request.
|
||||||
- Don't pass user-supplied raw text into the query without trimming — long queries (> a few hundred chars) waste embedding budget. Extract the question.
|
- Don't pass user-supplied raw text into the query without trimming — long queries (> a few hundred chars) waste embedding budget. Extract the question.
|
||||||
- Don't fabricate `doc_path`s. If you didn't see a doc in `search` / `ask` output, it's not in the KB.
|
- Don't fabricate `doc_path`s. If you didn't see a doc in `search` / `ask` output, it's not in the KB.
|
||||||
- Don't use `kebab tui` from a skill — it's interactive only.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user