250 lines
22 KiB
Markdown
250 lines
22 KiB
Markdown
---
|
|||
|
|
phase: 13-aiui-functional-conversational-node-control-and-content-surf
|
||
|
|
plan: 10
|
||
|
|
type: execute
|
||
|
|
wave: 4
|
||
|
|
depends_on: ["13-08"]
|
||
|
|
files_modified:
|
||
|
|
- core/archipelago/src/assistant/backends/ollama.rs
|
||
|
|
- core/archipelago/src/assistant/backends/mod.rs
|
||
|
|
- core/archipelago/src/assistant/history.rs
|
||
|
|
- core/archipelago/src/assistant/mod.rs
|
||
|
|
- core/archipelago/src/api/rpc/assistant_chat.rs
|
||
|
|
autonomous: true
|
||
|
|
requirements: [AIUI-01]
|
||
|
|
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "Node data never leaves the node when a local model is available: Ollama is tried first, Claude second (D-04)"
|
||
|
|
- "The local model gets tools, and its writes clear the same confirm gate as any other backend — so a mis-called tool from a weak model surfaces as a prompt the user rejects, not a wrong action (D-07)"
|
||
|
|
- "Chat history lives node-side under data_dir, inheriting the node's backup, factory-reset and future LUKS story rather than growing a second sensitive-data location (D-08)"
|
||
|
|
- "History is scoped by caller identity and permission scope, so an operator's AIUI session and a mesh peer's query never see each other's transcript (D-02)"
|
||
|
|
- "A transcript that outgrows the local model's context window is compacted, not truncated mid-turn, and the compaction summary is regenerated incrementally rather than from scratch"
|
||
|
|
artifacts:
|
||
|
|
- path: "core/archipelago/src/assistant/backends/ollama.rs"
|
||
|
|
provides: "Ollama /api/chat tool-calling adapter — a different endpoint and request shape from assist.rs::call_ollama"
|
||
|
|
contains: "api/chat"
|
||
|
|
- path: "core/archipelago/src/assistant/history.rs"
|
||
|
|
provides: "D-08 node-side chat persistence under data_dir, scoped by CallerScope, with compaction"
|
||
|
|
contains: "pub struct History"
|
||
|
|
key_links:
|
||
|
|
- from: "core/archipelago/src/assistant/backends/mod.rs"
|
||
|
|
to: "core/archipelago/src/assistant/backends/ollama.rs"
|
||
|
|
via: "select_backend tries Ollama before Claude, per D-04's order"
|
||
|
|
pattern: "OllamaBackend"
|
||
|
|
- from: "core/archipelago/src/assistant/history.rs"
|
||
|
|
to: "core/archipelago/src/assistant/mod.rs"
|
||
|
|
via: "History keyed by CallerScope, the promoted primary from 13-01"
|
||
|
|
pattern: "CallerScope"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
Complete D-04's local-first half and D-08's persistence.
|
||
|
|
|
||
|
|
**Ollama.** `mesh/listener/assist.rs::call_ollama` posts a bare prompt string to `/api/generate`
|
||
|
|
with no `tools` field — that endpoint has no tool-calling support at all. The new adapter is a
|
||
|
|
different endpoint (`/api/chat`), a different request shape (a `messages` array plus a `tools`
|
||
|
|
array), and a different response shape (`message.tool_calls`). Do not extend `call_ollama` in
|
||
|
|
place. Two cross-provider gotchas are in play and are the reason the `ToolCall` normalization
|
||
|
|
lives at the adapter edge: Ollama returns tool-call arguments as an already-parsed object (unlike
|
||
|
|
OpenAI-shape, which returns a JSON-encoded string), and Ollama gives tool calls **no `id`
|
||
|
|
field**, so the adapter must synthesize a stable per-turn id or the loop's result-matching breaks
|
||
|
|
silently.
|
||
|
|
|
||
|
|
**Weak local models are accepted, not chased.** A small model will hallucinate tool names, omit
|
||
|
|
required arguments and emit malformed JSON far more often than Claude. D-07's mitigation is that
|
||
|
|
every destructive call passes the same confirm gate regardless of backend — so this is a
|
||
|
|
UX/latency concern, not a security gap, and the fix is never a prompt trick.
|
||
|
|
|
||
|
|
**History (D-08).** The transcript lives under `data_dir`, inheriting the node's backup,
|
||
|
|
factory-reset and future LUKS story rather than growing a second sensitive-data location. It is
|
||
|
|
keyed by caller identity **and** permission scope, so an operator's AIUI session and a mesh
|
||
|
|
peer's `!ai` query never see each other's history. Pending confirmations remain in-memory only —
|
||
|
|
that is 13-08's structural property and this plan must not accidentally give them a persistence
|
||
|
|
path by writing the whole `ToolExecCtx`.
|
||
|
|
|
||
|
|
Output: `backends/ollama.rs`, the D-04 chain wired in order, `history.rs`, and `assistant.history`.
|
||
|
|
</objective>
|
||
|
|
|
||
|
|
<flagged_assumptions>
|
||
|
|
**`qwen2.5-coder` tool-capability is `[ASSUMED]`.** AI-SPEC §4 flags that `assist.rs`'s
|
||
|
|
`DEFAULT_MODEL = "qwen2.5-coder"` has not been confirmed tool-capable. Task 1 checks the
|
||
|
|
configured model's capability at runtime and degrades to the next backend rather than silently
|
||
|
|
producing tool-free answers; the check, not the assumption, is what ships.
|
||
|
|
</flagged_assumptions>
|
||
|
|
|
||
|
|
<artifacts_this_phase_produces>
|
||
|
|
Symbols created by **this plan**:
|
||
|
|
- `assistant/backends/ollama.rs`: `pub struct OllamaBackend`, `fn synthesize_call_id`,
|
||
|
|
`fn model_supports_tools`, `const OLLAMA_CHAT_URL`, `const OLLAMA_NUM_PREDICT`
|
||
|
|
- `assistant/backends/mod.rs`: `select_backend` extended with the Ollama leg,
|
||
|
|
`pub enum BackendId`
|
||
|
|
- `assistant/history.rs`: `pub struct History`, `pub struct HistoryKey`, `History::load`,
|
||
|
|
`History::append`, `History::recent`, `History::compact`, `const KEEP_VERBATIM_TURNS`,
|
||
|
|
`const MAX_TOOL_RESULT_CHARS`
|
||
|
|
- `api/rpc/assistant_chat.rs`: `handle_assistant_history`, `handle_assistant_clear_history`
|
||
|
|
- New RPC method names: `assistant.history`, `assistant.clear-history` (through 13-01's existing
|
||
|
|
`assistant.` arm — `dispatcher.rs` is not touched)
|
||
|
|
</artifacts_this_phase_produces>
|
||
|
|
|
||
|
|
<execution_context>
|
||
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
||
|
|
</execution_context>
|
||
|
|
|
||
|
|
<context>
|
||
|
|
@.planning/PROJECT.md
|
||
|
|
@.planning/STATE.md
|
||
|
|
@CLAUDE.md
|
||
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md
|
||
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-AI-SPEC.md
|
||
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-08-SUMMARY.md
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto" tdd="true">
|
||
|
|
<name>Task 1: Ollama tool-calling, first in the D-04 chain</name>
|
||
|
|
<files>core/archipelago/src/assistant/backends/ollama.rs, core/archipelago/src/assistant/backends/mod.rs</files>
|
||
|
|
<behavior>
|
||
|
|
- A turn with tools produces a request to the chat endpoint carrying a `messages` array and a `tools` array — never the generate endpoint and never a bare prompt string.
|
||
|
|
- A response containing tool calls maps to `BackendTurn::ToolCalls`, with each call assigned a non-empty, unique-within-the-turn id even though the wire response carries none.
|
||
|
|
- Tool-call arguments arrive as an already-parsed object and are passed through without a second string-parse.
|
||
|
|
- A response with only text maps to `BackendTurn::Text`.
|
||
|
|
- Every turn that may emit a tool call is requested non-streaming, so arguments are complete before validation.
|
||
|
|
- The generation length cap is set explicitly on every request; it is never left unbounded.
|
||
|
|
- `select_backend` returns Ollama when Ollama is reachable and its configured model reports tool capability; it falls through to Claude when Ollama is unreachable, and also when the configured model is reachable but not tool-capable.
|
||
|
|
- An Ollama transport error falls through to the next backend rather than failing the turn.
|
||
|
|
</behavior>
|
||
|
|
<read_first>
|
||
|
|
- `core/archipelago/src/mesh/listener/assist.rs` lines 429-451 (`call_ollama`) — `13-PATTERNS.md` marks this an **exact** analog for the HTTP client construction and a **do-not-copy** for everything else: the endpoint, the body shape and the constants all change. Read `run_assist`'s catch-and-fall-back-to-next-backend handling too; that is the model for the D-04 chain.
|
||
|
|
- `core/archipelago/src/api/rpc/mesh/assistant.rs` lines 164-192 — `detect_ollama()`, which already reports `ollama_detected` and `models`. **Reuse it rather than re-probing.**
|
||
|
|
- `core/archipelago/src/assistant/backends/mod.rs` and `backends/claude.rs` from 13-01 — the `Backend` trait, `BackendTurn`, and the `select_backend` seam the tracer left for exactly this.
|
||
|
|
- `.planning/phases/13-.../13-AI-SPEC.md` §3 Pitfalls 1, 2, 3, 4 and 6, and §4 "Model Configuration" (Ollama).
|
||
|
|
</read_first>
|
||
|
|
<action>
|
||
|
|
Create `core/archipelago/src/assistant/backends/ollama.rs` implementing the `Backend` trait against Ollama's chat endpoint. Build the request with a `messages` array mapped from `ChatMessage`, and a `tools` array whose entries wrap each `ToolDef`'s name, description and `parameters` in Ollama's function-tool envelope. Request non-streaming for every turn while the loop is still deciding whether a tool is being called — partial JSON tool arguments cannot be structurally validated mid-stream. Set the generation-length cap explicitly in the request options; never leave it unbounded.
|
||
|
|
|
||
|
|
Parse `message.tool_calls` into `BackendTurn::ToolCalls`. Ollama's response carries no id per call, so `synthesize_call_id` assigns a monotonically increasing per-turn id — leaving it empty makes the loop's result-matching fail silently, which is worse than failing loudly. Ollama's `function.arguments` is an already-parsed object: assign it straight into `ToolCall.arguments`; do not run a string-parse over it. That normalization belongs here, at the adapter edge, so the shared loop stays wire-agnostic.
|
||
|
|
|
||
|
|
Define new module constants for the chat URL and the generation cap. **Do not import `OLLAMA_TIMEOUT`, `MAX_REPLY_CHARS` or `CHUNK_CHARS` from `assist.rs`** — those are LoRa-airtime-tuned and would either under-time-out a multi-turn loop or truncate a chat answer that has no reason to be capped.
|
||
|
|
|
||
|
|
`model_supports_tools` queries the configured model's capability through Ollama's own model-info endpoint and caches the answer for the process lifetime. This is what turns AI-SPEC's `[ASSUMED]` note about `qwen2.5-coder` into a runtime fact: a model that cannot call tools is not silently used as the assistant's primary, it falls through to Claude, and the fall-through reason is logged and surfaced.
|
||
|
|
|
||
|
|
Extend `select_backend` in `backends/mod.rs` to D-04's order — Ollama, then Claude, with the Routstr slot left where 13-13 will insert it. Reuse `detect_ollama()` rather than writing a second probe. A transport error at any leg falls through to the next, matching `run_assist`'s existing behaviour.
|
||
|
|
|
||
|
|
Write the tests FIRST, one per `<behavior>` bullet, using a local HTTP stub for the Ollama endpoint. Name them under `assistant::backends::ollama::tests::`, including `ollama_uses_chat_endpoint_not_generate`, `tool_calls_get_synthesized_ids`, `arguments_object_is_not_string_parsed`, and `non_tool_capable_model_falls_through_to_claude`.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago assistant::backends:: 2>&1 | tail -25</automated>
|
||
|
|
<automated>cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago ollama_uses_chat_endpoint_not_generate</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `cd core && cargo test --package archipelago assistant::backends::` exits 0 with a test per `<behavior>` bullet
|
||
|
|
- `grep -c 'api/generate' core/archipelago/src/assistant/backends/ollama.rs` returns 0
|
||
|
|
- `grep -q 'api/chat' core/archipelago/src/assistant/backends/ollama.rs`
|
||
|
|
- `grep -rncE 'OLLAMA_TIMEOUT|MAX_REPLY_CHARS|CHUNK_CHARS' core/archipelago/src/assistant/ | grep -vq ':[1-9]' — no airtime-tuned constant is imported into the assistant module
|
||
|
|
- `grep -q 'synthesize_call_id' core/archipelago/src/assistant/backends/ollama.rs` and the test asserts ids are non-empty and unique within a turn
|
||
|
|
- `grep -c 'from_str' core/archipelago/src/assistant/backends/ollama.rs` returns 0 — Ollama's arguments are not string-parsed
|
||
|
|
- `grep -q 'detect_ollama' core/archipelago/src/assistant/backends/mod.rs` — the existing probe is reused, not duplicated
|
||
|
|
- `grep -q 'model_supports_tools' core/archipelago/src/assistant/backends/ollama.rs`
|
||
|
|
</acceptance_criteria>
|
||
|
|
<reversibility rating="reversible">A backend adapter behind an existing trait; adding or reordering legs of the chain is additive.</reversibility>
|
||
|
|
<done>A local model gets tools through the chat endpoint with synthesized call ids and an explicit generation cap; a non-tool-capable or unreachable Ollama falls through to Claude with a logged reason instead of silently degrading the assistant.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="auto" tdd="true">
|
||
|
|
<name>Task 2: Node-side history, scoped by caller, compacted rather than truncated</name>
|
||
|
|
<files>core/archipelago/src/assistant/history.rs, core/archipelago/src/assistant/mod.rs, core/archipelago/src/api/rpc/assistant_chat.rs</files>
|
||
|
|
<behavior>
|
||
|
|
- A completed turn is appended to a transcript stored under `data_dir` and survives a daemon restart.
|
||
|
|
- An operator's AIUI transcript and a mesh peer's transcript are separate: reading one never returns a turn from the other.
|
||
|
|
- A tool result longer than the cap is truncated before it enters history, with a visible marker that it was truncated.
|
||
|
|
- Once the transcript exceeds the verbatim window, older turns fold into a running summary and the recent window stays verbatim; the summary is extended incrementally rather than regenerated from the full transcript.
|
||
|
|
- `assistant.history` returns only the calling session's own transcript.
|
||
|
|
- `assistant.clear-history` removes the calling session's transcript and nothing else.
|
||
|
|
- No pending confirmation and no tool argument value from a `wallet`- or `files`-category tool is written to the transcript file.
|
||
|
|
- The transcript file is created 0600.
|
||
|
|
</behavior>
|
||
|
|
<read_first>
|
||
|
|
- `core/archipelago/src/streaming/session.rs` — `13-PATTERNS.md`'s role-match analog for `data_dir`-scoped persisted state: its load/save shape, its file permissions, its error handling.
|
||
|
|
- `core/archipelago/src/assistant/mod.rs` — `CallerScope` (13-01's promoted primary) and `PermissionCategory`. `HistoryKey` is derived from `CallerScope`, which is what makes the per-caller separation fall out of the type rather than out of a convention.
|
||
|
|
- `core/archipelago/src/assistant/confirm.rs` (13-08) — read it to confirm you are not giving pending confirmations a persistence path by serializing something that reaches them. S-09 is structural and this task must not weaken it.
|
||
|
|
- `.planning/phases/13-.../13-AI-SPEC.md` §4 "State Management" and §4b.4 "Context Window Management" (truncate tool results with an assistant-scoped constant, keep the last K turns verbatim, fold older turns into an incrementally-regenerated summary, and budget conservatively when the model's context length is unknown).
|
||
|
|
- `.planning/phases/13-.../13-AI-SPEC.md` §7b — the field policy: what may and may not be emitted. It applies to the transcript as well as to the logs.
|
||
|
|
</read_first>
|
||
|
|
<action>
|
||
|
|
Create `core/archipelago/src/assistant/history.rs`. `HistoryKey` is derived from `CallerScope` so a mesh peer's transcript and the local operator's transcript are structurally distinct files or structurally distinct keys — not two rows distinguished by a field someone could forget to filter on. Store under `data_dir` with 0600 permissions, following `streaming/session.rs`'s conventions, and write atomically (temp sibling plus rename) so a crash mid-append cannot corrupt a transcript.
|
||
|
|
|
||
|
|
`MAX_TOOL_RESULT_CHARS` is a **new**, assistant-scoped constant — a long log tail or directory listing is truncated with a visible marker before it becomes a `ToolResult` in history. Do not reuse the mesh reply cap; it is airtime-tuned, not context-window-tuned.
|
||
|
|
|
||
|
|
`compact` keeps the last `KEEP_VERBATIM_TURNS` turns verbatim and folds older turns into a running summary, extending the existing summary with the turns that just aged out rather than re-summarizing the whole transcript — otherwise the summarization cost itself grows without bound. Generate the summary with the already-selected backend, preferring the local one when it is available: this is a sub-task, and D-04's chain is already the cost lever. When the configured model's context length is not discoverable, assume a conservative window and truncate proactively rather than letting a request fail mid-loop.
|
||
|
|
|
||
|
|
Apply AI-SPEC §7b's field policy to what is persisted: never write a tool's raw argument values for a `wallet`- or `files`-category tool, and never write anything reachable from the pending-confirmation state. Record tool name, category, outcome and a truncated result instead. A transcript is a sensitive-data location by definition, which is exactly why D-08 puts it where the node's backup and factory-reset story already reaches.
|
||
|
|
|
||
|
|
Wire `mod.rs`'s `chat()` to append each completed turn, and add `handle_assistant_history` and `handle_assistant_clear_history` to `assistant_chat.rs` — both scoped to the calling session's own `HistoryKey`, both routed through 13-01's existing `assistant.` arm. **Do not touch `dispatcher.rs`.**
|
||
|
|
|
||
|
|
Write the tests FIRST, one per `<behavior>` bullet, under `assistant::history::tests::`. Name the isolation case `operator_and_mesh_transcripts_are_separate` and the redaction case `wallet_tool_arguments_never_reach_the_transcript`.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago assistant:: 2>&1 | tail -30</automated>
|
||
|
|
<automated>cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago operator_and_mesh_transcripts_are_separate</automated>
|
||
|
|
<automated>cd core && git diff --exit-code -- archipelago/src/api/rpc/dispatcher.rs</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `cd core && cargo test --package archipelago assistant::` exits 0 with a test per `<behavior>` bullet
|
||
|
|
- `grep -q 'pub struct History' core/archipelago/src/assistant/history.rs` and `grep -q 'CallerScope' core/archipelago/src/assistant/history.rs`
|
||
|
|
- `grep -cE '0o600|from_mode' core/archipelago/src/assistant/history.rs` ≥ 1
|
||
|
|
- `grep -cE 'rename' core/archipelago/src/assistant/history.rs` ≥ 1 — the append is atomic
|
||
|
|
- `grep -q 'MAX_TOOL_RESULT_CHARS' core/archipelago/src/assistant/history.rs` and `grep -c 'MAX_REPLY_CHARS' core/archipelago/src/assistant/history.rs` returns 0
|
||
|
|
- `cd core && cargo test --package archipelago assistant::confirm::tests::restart_drops_pending_not_executes` still passes — S-09 was not weakened
|
||
|
|
- `cd core && git diff --exit-code -- archipelago/src/api/rpc/dispatcher.rs` exits 0
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>Transcripts persist under `data_dir` per caller scope, survive restarts, stay inside a bounded context budget through incremental compaction, and never carry a wallet/files argument value or a pending confirmation.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
<threat_model>
|
||
|
|
## Trust Boundaries
|
||
|
|
|
||
|
|
| Boundary | Description |
|
||
|
|
|----------|-------------|
|
||
|
|
| node → 127.0.0.1:11434 | Loopback only; nothing leaves the node on the Ollama leg |
|
||
|
|
| node → api.anthropic.com | The fall-through leg; the only egress in this plan |
|
||
|
|
| transcript → disk | A new sensitive-data location, deliberately placed inside `data_dir` so backup/factory-reset/LUKS already cover it |
|
||
|
|
| one caller's transcript → another caller | Mesh peers and the local operator share the service but must not share history |
|
||
|
|
|
||
|
|
## STRIDE Threat Register
|
||
|
|
|
||
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
||
|
|
| T-13-61 | Information Disclosure | A mesh peer reading the operator's transcript | high | mitigate | `HistoryKey` derives from `CallerScope`, so separation is structural rather than a filter someone can forget. Asserted by `operator_and_mesh_transcripts_are_separate` |
|
||
|
|
| T-13-62 | Information Disclosure | A secret or a sensitive path landing in a persisted transcript | high | mitigate | AI-SPEC §7b field policy applied to persistence: no `wallet`/`files` argument values, no pending-confirmation state, truncated results. Asserted by `wallet_tool_arguments_never_reach_the_transcript` |
|
||
|
|
| T-13-63 | Information Disclosure | Transcript world-readable on disk | high | mitigate | 0600 under `data_dir`, following `streaming/session.rs`. Asserted by grep |
|
||
|
|
| T-13-64 | Information Disclosure | Node data escalated to a cloud backend when the local model could have answered | high | mitigate | D-04 order enforced in `select_backend` with Ollama first; the escalation *payload* minimality guardrail (G-B2/E-04) lands in 13-12 and is named there, not assumed here |
|
||
|
|
| T-13-65 | Tampering | A weak local model's malformed tool call coerced into an execution | medium | mitigate | D-07: the same confirm gate and the same `validate` run regardless of backend. Accepted as a UX cost per AI-SPEC §3 Pitfall 4 — not chased with prompt tricks |
|
||
|
|
| T-13-66 | Tampering | Silent loop breakage from empty Ollama tool-call ids | medium | mitigate | `synthesize_call_id` assigns non-empty unique ids; asserted by `tool_calls_get_synthesized_ids` |
|
||
|
|
| T-13-67 | Denial of Service | Unbounded generation length, or a summarization cost that grows with the transcript | medium | mitigate | Explicit generation cap on every Ollama request; compaction extends the summary incrementally instead of re-summarizing the whole transcript |
|
||
|
|
| T-13-68 | Repudiation | A non-tool-capable model silently answering without tools, so the assistant looks broken rather than misconfigured | low | mitigate | `model_supports_tools` checks at runtime, falls through to Claude, and logs the reason. This retires AI-SPEC §4's `[ASSUMED]` on `qwen2.5-coder` with a check rather than a guess |
|
||
|
|
| T-13-69 | Tampering | Pending confirmations gaining a persistence path via history serialization | high | mitigate | Nothing reachable from the pending-confirmation state is serialized; 13-08's S-09 test is re-run as an acceptance criterion of this plan |
|
||
|
|
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | **Zero** packages added. No install task, so no legitimacy checkpoint required |
|
||
|
|
</threat_model>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
- `cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago assistant::` green, including 13-08's confirm suite
|
||
|
|
- `grep -c 'api/generate' core/archipelago/src/assistant/backends/ollama.rs` == 0
|
||
|
|
- `cd core && git diff --exit-code -- archipelago/src/api/rpc/dispatcher.rs` exits 0
|
||
|
|
- With Ollama running and tool-capable, a read question is answered locally; with Ollama stopped, the same question falls through to Claude and the fall-through is logged
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
The assistant is local-first in fact rather than in intent, the local model gets real tools
|
||
|
|
behind the same gate as every other backend, and the transcript lives exactly where D-08 put it
|
||
|
|
— per caller, bounded, atomic, and carrying nothing the field policy forbids.
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-10-SUMMARY.md` when done
|
||
|
|
</output>
|