Files
archy/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-PATTERNS.md
T

25 KiB

Phase 13: AIUI — Conversational Node Control & Content Surfaces - Pattern Map

Mapped: 2026-08-03 Files analyzed: 24 (net-new + modified, both repos) Analogs found: 17 / 24 (7 have no strong precedent — flagged below)

Scope note: this phase spans two repos: /home/archipelago/Projects/archy (Rust core/, Vue neode-ui/) and /home/archipelago/Projects/AIUI (Vue, branch development). File paths below are absolute-repo-relative and prefixed accordingly.


File Classification

New/Modified File Role Data Flow Closest Analog Match Quality
core/archipelago/src/assistant/mod.rs service request-response (loop) core/archipelago/src/mesh/listener/assist.rs role-match (Q&A→tool-loop, no precedent for the loop itself)
core/archipelago/src/assistant/tools.rs model/schema transform core/archipelago/src/api/rpc/mesh/assistant.rs (config shape) weak — no existing curated-tool-registry precedent in this codebase
core/archipelago/src/assistant/backends/ollama.rs service request-response core/archipelago/src/mesh/listener/assist.rs::call_ollama exact (endpoint/shape differs, HTTP client pattern identical)
core/archipelago/src/assistant/backends/claude.rs service request-response core/archipelago/src/mesh/listener/assist.rs::call_claude exact (endpoint/shape differs, HTTP client pattern identical)
core/archipelago/src/assistant/backends/routstr.rs service request-response + payment core/archipelago/src/swarm/payment.rs::auto_pay_token (payment half) + call_claude (HTTP half) partial — no existing OpenAI-compatible client in this codebase; net-new
core/archipelago/src/assistant/loop_.rs service event-driven (multi-turn) none in this codebase no analog — first tool-calling loop; see AI-SPEC §3 for the sketch instead
core/archipelago/src/assistant/confirm.rs service pub-sub (pending-queue) neode-ui/src/services/contextBroker.ts install-app confirm flow (cross-repo, browser-side half only) partial — Rust-side pending-queue has no precedent
core/archipelago/src/assistant/history.rs model/storage CRUD core/archipelago/src/streaming/session.rs (data_dir-scoped persisted state) role-match
core/archipelago/src/api/rpc/assistant_chat.rs route (RPC handler) request-response core/archipelago/src/api/rpc/mesh/assistant.rs exact
core/archipelago/src/api/rpc/dispatcher.rs (modified) route (registry) request-response itself — extend "mesh.assistant-*" block at ~445 exact
core/archipelago/src/music/mod.rs service batch/CRUD core/archipelago/src/content_server.rs (catalog load/scan shape) role-match
core/archipelago/src/music/index.rs model/storage CRUD core/archipelago/src/content_server.rs::load_catalog role-match
core/archipelago/src/music/tags.rs utility transform none — new lofty-based extractor no analog — net-new dependency, gate behind checkpoint:human-verify per RESEARCH.md
neode-ui/src/services/contextBroker.ts (modified) service (browser bridge) pub-sub (postMessage) itself — extend existing handleMessage switch and the install-app confirm block (lines 140-196) exact
neode-ui/src/types/aiui-protocol.ts (modified) model (protocol types) transform itself — extend AIActionType union exact
neode-ui/src/components/ToolConfirmModal.vue (new) component event-driven neode-ui/src/components/NostrSignConsent.vue exact (Teleport-to-body approve/deny modal)
neode-ui/src/composables/archyContentAdapter.ts (new) utility (adapter) transform none in neode-ui — net-new, shape target is AIUI's Film/Song/Podcast types no analog — see AIUI content types below
neode-ui/src/api/assistant-client.ts (new, optional) service (RPC client wrapper) request-response neode-ui/src/api/filebrowser-client.ts (scoped-token pattern) role-match — do not copy the streamUrl JWT-in-query leak (line 176)
scripts/build-aiui.sh (new) config/build script batch scripts/deploy-to-target.sh (AIUI rsync section, setup-aiui-server.sh) role-match
AIUI: packages/app/src/composables/useAI.ts (modified) service (chat client) streaming itself — replace streamClaude/streamOpenRouter direct-to-proxy calls exact (modify in place)
AIUI: packages/app/src/composables/useArchy.ts (modified) service (bridge client) request-response itself — extend buildArchyContext()/postMessage senders exact
AIUI: packages/app/src/composables/contentExtraction.ts (modified/deprecated for Archy content) utility (transform) transform itself — updatePanelFromText regex path stays for non-Archy content, bypassed for Archy-sourced grids exact (partial deprecation)
AIUI: packages/app/src/components/content/FilmGrid.vue / SongGrid.vue (consumers, unmodified props) component CRUD (prop-fed) itself — no code change, just a new data source feeding existing props exact — props unchanged, D-12
AIUI: packages/core/src/types/content.ts (read, not modified) model (types) transform itself — the target shape archyContentAdapter.ts must produce exact reference

Pattern Assignments

core/archipelago/src/assistant/backends/ollama.rs (service, request-response)

Analog: core/archipelago/src/mesh/listener/assist.rs (lines 429-451, call_ollama)

What to copy — HTTP client construction:

// Source: core/archipelago/src/mesh/listener/assist.rs:429-451 (VERIFIED, read in full)
async fn call_ollama(model: &str, prompt: &str) -> anyhow::Result<String> {
    let client = reqwest::Client::builder().timeout(OLLAMA_TIMEOUT).build()?;
    let body = serde_json::json!({
        "model": model,
        "prompt": prompt,
        "stream": false,
    });
    let resp = client.post(OLLAMA_URL).json(&body).send().await?;
    // ... no `tools` field, no multi-turn loop — /api/generate, not /api/chat
}

What must change (do NOT copy as-is):

  • Endpoint: /api/generate/api/chat (tool-calling requires the chat endpoint).
  • Request body needs a messages array (not bare prompt) and a tools array ([{"type":"function","function":{"name","description","parameters"}}]).
  • Response parsing needs message.tool_calls extraction; Ollama gives tool calls no id — synthesize one (monotonic counter within the turn), per AI-SPEC §3 Pitfall 3.
  • Do not reuse OLLAMA_TIMEOUT (60s, airtime-tuned for mesh) — define new constants in assistant/ per AI-SPEC §3 Pitfall 6.

Error handling: assist.rs::call_claude's anyhow::Result propagation and run_assist's catch-and-fall-back-to-next-backend pattern is the model for the D-04 backend chain (Ollama → Claude → Routstr fallback on error).


core/archipelago/src/assistant/backends/claude.rs (service, request-response)

Analog: core/archipelago/src/mesh/listener/assist.rs (call_claude) + core/archipelago/src/api/rpc/mesh/assistant.rs (key-file read pattern)

Key location pattern to copy (lines 27-30 of assistant.rs):

// Source: core/archipelago/src/api/rpc/mesh/assistant.rs:27-30 (VERIFIED)
let claude_available =
    tokio::fs::metadata(self.config.data_dir.join("secrets/claude-api-key"))
        .await
        .is_ok();

Reuse data_dir/secrets/claude-api-key as the key path — do not introduce a second key location. call_claude's single-user-message Messages API POST is the HTTP-shape starting point; extend it with tools: [...], tool_choice: {"type":"auto","disable_parallel_tool_use":true} (AI-SPEC §3 Pitfall 5), and max_tokens: 2048 (raised from mesh's 512).


core/archipelago/src/assistant/backends/routstr.rs (service, request-response + payment)

No direct analog for the HTTP client (first OpenAI-compatible client in this codebase). Compose from two existing pieces:

Payment half — copy verbatim as the reusable primitive (core/archipelago/src/swarm/payment.rs:77-101):

// Source: core/archipelago/src/swarm/payment.rs:77-101 (VERIFIED, read in full)
pub async fn auto_pay_token(
    data_dir: &Path,
    policy: &PaymentPolicy,       // budget_sats + max_fee_sats
    accepted_mints: &[String],
    price_sats: u64,
) -> Result<Option<String>> {
    if !policy.affords(price_sats) { return Ok(None); }  // hard cap, D-05
    match ecash::build_payment_token(data_dir, accepted_mints, price_sats, policy.max_fee_sats).await {
        Ok(token) => Ok(Some(token)),
        Err(e) => Ok(None), // never errors on a wallet/mint problem — origin always wins
    }
}

Call this exactly as-is for D-05's budget cap; loop_.rs must treat None as "stop and ask," never retry.

Nostr discovery half: core/archipelago/src/nostr_discovery.rs::build_nostr_client (Tor-proxy aware) — reuse this builder rather than constructing a second nostr-sdk client; subscribe to kind 38421 events for provider discovery.

HTTP half: model the reqwest::Client construction on call_claude's pattern (same crate, same TLS/socks features already in Cargo.toml), but the request/response shape is net-new (OpenAI tools/tool_calls JSON-string-encoded arguments — see AI-SPEC §3 Pitfall 2, do not confuse with Ollama's already-parsed object).


core/archipelago/src/assistant/loop_.rs (service, event-driven multi-turn loop)

No analog exists in this codebase — this is confirmed (RESEARCH.md, AI-SPEC.md) to be the first tool-calling loop ever written here. Do not attempt to derive it from run_assist (single-shot) or from Pine's HA intents (hardcoded read-only, no loop). Build directly from the run_loop/execute_tool sketch in 13-AI-SPEC.md §3/§4 — that IS the pattern source for this file; there is no in-repo precedent to extract instead.

Concurrency discipline to copy from assist.rs's own doc comment:

// "Spawned off the radio loop so it never blocks" — VERIFIED, assist.rs's own doc comment.

Apply the same discipline: never hold a shared lock (e.g. state.assistant.write().await) across the confirm-gate .await, which can block for human-response-time.


core/archipelago/src/assistant/confirm.rs (service, D-11 pending-confirmation queue)

Analog (browser-side half only, cross-repo): neode-ui/src/services/contextBroker.ts:140-196 — the install-app confirm flow (CustomEvent('aiui:install-request') / aiui:install-response, 60s timeout). This is the closest existing anti-spoofing confirm pattern in the whole codebase and is explicitly named in CONTEXT.md as the model to extend:

// Source: neode-ui/src/services/contextBroker.ts:140-196 (VERIFIED, read in full)
window.dispatchEvent(new CustomEvent('aiui:install-request', {
  detail: { requestId: id, appId, marketplaceUrl, version },
}))
const responseHandler = (e: Event) => {
  const detail = (e as CustomEvent).detail as { requestId: string; confirmed: boolean }
  if (detail.requestId !== id) return
  window.removeEventListener('aiui:install-response', responseHandler)
  // ... proceed or decline
}
window.addEventListener('aiui:install-response', responseHandler)
setTimeout(() => window.removeEventListener('aiui:install-response', responseHandler), 60000)

Do NOT reuse aiui:install-request/aiui:install-response directly — CONTEXT.md and RESEARCH.md both specify a new, distinct event pair (aiui:tool-confirm-request / aiui:tool-confirm-response), because D-11 requires the pending-action text be RPC-fetched (node-authored), never postMessage-carried (which the iframe could forge). The Rust-side confirm.rs queue itself (keyed by req_id/call_id, in-memory only, never persisted across a daemon restart per AI-SPEC §4 "State Management") has no existing analog — build per the AI-SPEC sketch.


core/archipelago/src/api/rpc/assistant_chat.rs (route, request-response)

Analog: core/archipelago/src/api/rpc/mesh/assistant.rs (full file — handle_mesh_assistant_status, handle_mesh_assistant_configure)

RPC handler shape to copy:

// Source: core/archipelago/src/api/rpc/mesh/assistant.rs:13-16 (VERIFIED)
impl RpcHandler {
    pub(in crate::api::rpc) async fn handle_mesh_assistant_status(
        &self,
    ) -> Result<serde_json::Value> {

Follow the same impl RpcHandler + pub(in crate::api::rpc) async fn handle_* + Result<serde_json::Value> convention for handle_assistant_chat, handle_assistant_confirm_tool, handle_assistant_list_tools, handle_assistant_history.

Registration pattern (core/archipelago/src/api/rpc/dispatcher.rs:445-446):

"mesh.assistant-status" => self.handle_mesh_assistant_status().await,
"mesh.assistant-configure" => self.handle_mesh_assistant_configure(params).await,

Add new "assistant.chat", "assistant.confirm-tool", "assistant.list-tools", "assistant.history" entries adjacent to this block. Verify session/CSRF/RBAC gating applies automatically — every method in this dispatch table already passes through api/rpc/mod.rs:264-330's session-cookie + CSRF + role.can_access() check before reaching the match; no bespoke auth needed (per Open Question 4 in RESEARCH.md, confirm this applies rather than assume).


core/archipelago/src/music/index.rs (model/storage, CRUD)

Analog: core/archipelago/src/content_server.rs::load_catalog (catalog-scan-and-persist shape) — read this function's on-disk index load/save pattern under data_dir and follow the same convention for the music index (own subdirectory under data_dir, per D-13's discretion on exact location).


neode-ui/src/components/ToolConfirmModal.vue (component, D-11 trusted-chrome modal)

Analog: neode-ui/src/components/NostrSignConsent.vue (full file, 70 lines) — the project's canonical Teleport-to-body approve/deny modal, structurally identical to what D-11 needs.

Structure to copy:

<!-- Source: neode-ui/src/components/NostrSignConsent.vue:1-20 (VERIFIED) -->
<template>
  <Teleport to="body">
    <Transition name="modal">
      <div
        v-if="show"
        class="fixed inset-0 z-[3000] flex items-center justify-center p-4"
        @click="deny"
      >
        <div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
        <div ref="modalRef" @click.stop class="glass-card p-6 max-w-md w-full relative z-10">
          <div class="flex items-start justify-between gap-4 mb-4">
            <h3 class="text-xl font-semibold text-white">Nostr Signing Request</h3>
            <button @click="deny" class="p-2 rounded-lg hover:bg-white/10 ..." aria-label="Close" />
          </div>
          <!-- request-specific detail rendering here -->
          <div class="flex gap-3">
            <button @click="deny" class="glass-button flex-1 py-2.5 rounded-lg text-sm font-medium">Deny</button>
            <button @click="approve" class="glass-button flex-1 py-2.5 rounded-lg text-sm font-medium text-orange-400 border-orange-400/30">Approve</button>
          </div>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

Content difference from the analog: D-11 requires the confirmation text be RPC-fetched from the node's own pending-action description (via assistant.list-tools/pending-confirmation poll or the chat:response channel that carries a tool:confirm-request payload), never model-authored text and never a postMessage-carried string from AIUI. Wire this modal's props from contextBroker.ts's new confirm-handling code, not from anything AIUI sends.


neode-ui/src/services/contextBroker.ts (modified — new chat:*/tool:confirm-* message types)

Analog: itself — the existing handleMessage switch (imports at lines 1-13) and the install-app confirm block (lines 140-196, shown above under confirm.rs).

Imports pattern already in file (lines 1-13):

// Source: neode-ui/src/services/contextBroker.ts:1-13 (VERIFIED)
import type { Ref } from 'vue'
import type {
  AIUIRequest, ArchyResponse, AIContextCategory,
  ArchyContextResponse, ArchyActionResponse,
} from '@/types/aiui-protocol'
import { useAIPermissionsStore } from '@/stores/aiPermissions'
import { useAppStore } from '@/stores/app'
import { useContainerStore, BUNDLED_APPS } from '@/stores/container'
import { rpcClient } from '@/api/rpc-client'
import { fileBrowserClient } from '@/api/filebrowser-client'

New assistant-client.ts (or direct rpcClient.call('assistant.chat', ...)) follows this same import convention. The class-level origin check (this.allowedOrigin, constructor lines 26-33) and the postToIframe helper are the transport primitives every new message type must use — do not add a second postMessage channel.


neode-ui/src/api/assistant-client.ts (new, role-match to filebrowser-client.ts)

Analog: neode-ui/src/api/filebrowser-client.ts — the scoped-token pattern D-01 follows for minting short-lived, purpose-scoped credentials via an authenticated RPC.

Known leak to fix, NOT propagate (neode-ui/src/api/filebrowser-client.ts:172-176):

// Source: neode-ui/src/api/filebrowser-client.ts:172-176 (VERIFIED)
async streamUrl(path: string): Promise<string> {
  // ...
  return `${this.baseUrl}/api/raw${safePath}?auth=${token}`
  //                                          ^^^^^^^^^^^^^ JWT in URL query string —
  //                                          lands in browser history, server access
  //                                          logs, Referer headers.
}

For any new content/tool streaming URL construction in this phase, do NOT copy this ?auth=${token} concatenation. Prefer header-based auth where the consumer can set headers (fetchBlobUrl()-style, per RESEARCH.md Pitfall 5); where a bare <audio>/<video src> is unavoidable, scope the token single-resource/single-use rather than reusing the general FileBrowser session token shape.


neode-ui/src/composables/archyContentAdapter.ts (new, no analog — net-new adapter)

No existing adapter in neode-ui. Target shape is AIUI's own types (/home/archipelago/Projects/AIUI/packages/core/src/types/content.ts, read directly):

// Source: AIUI packages/core/src/types/content.ts:7-53 (VERIFIED, read in full)
export interface Film {
  // id, title, ... (lines 7-19)
  posterUrl: string
  // ...
  sources: FilmSource[]           // line 20
}
export interface FilmSource { /* type: 'plex'|'nextcloud'|... , url, ... */ }

export interface Song {
  // ...
  coverUrl?: string                // line 50
  sources?: SongSource[]           // line 53
}

Source shape to map FROM (core/archipelago/src/content_server.rs, ContentItem):

// id, filename, mime_type, size_bytes, description, access, availability, added_at

This is Pitfall 4 in RESEARCH.md — there is genuinely no shape overlap; the adapter is hand-written mapping logic, not a pass-through. Pin the mapping with fixture-based tests (archyContentAdapter.test.ts, listed as a Wave 0 gap in RESEARCH.md's Validation Architecture). FilmGrid.vue/SongGrid.vue themselves need zero code changes — D-12 is explicit that only the data source behind the existing props changes.


AIUI: packages/app/src/composables/useAI.ts (modified — replace direct-proxy calls)

Analog: itself. The current streamClaude/streamOpenRouter functions call ${BASE}api/claude/v1/messages / ${BASE}api/openrouter directly (the port-3142 claude-api-proxy.py passthrough, verified live and unauthenticated in RESEARCH.md). Replace these call sites with the new chat:request/chat:response postMessage exchange to contextBroker.ts, matching the shape useArchy.ts already uses for its existing readFile/tailLogs postMessage calls (grep useArchy.ts for its existing postMessage-send pattern and mirror it — do not invent a third transport convention on the AIUI side).


scripts/build-aiui.sh (new, config/build script)

Analog: scripts/deploy-to-target.sh (AIUI rsync section) and scripts/setup-aiui-server.sh — both already encode the VITE_BASE_PATH=/aiui/ requirement and the rsync-to-node path. D-15 requires this be made deliberate (enforced by the script, not remembered) with a post-deploy check that fetches a live asset — model the fetch-and-verify step on the project's general "grep the built bundle for new strings before shipping" convention from CLAUDE.md ("Frontend build — verify dist changed" feedback note), translated into an automated curl check rather than a manual grep.


Shared Patterns

Session/CSRF/RBAC gating (applies to every new assistant.* and content.* RPC)

Source: core/archipelago/src/api/rpc/mod.rs:264-330 Apply to: assistant_chat.rs, all new dispatcher entries. Every RPC method reaching the match in dispatcher.rs already passed session-cookie + CSRF + role.can_access(&method) checks upstream — no bespoke auth code needed in the new handlers themselves, only correct registration in the existing table.

Anti-spoofing confirm gate (D-11, applies to every destructive tool)

Source: neode-ui/src/services/contextBroker.ts:140-196 (browser half) + 13-AI-SPEC.md §4's execute_tool sketch (Rust half, no in-repo precedent). Apply to: confirm.rs, ToolConfirmModal.vue, assistant_chat.rs's confirm-tool handler.

Backend-key-at-rest pattern (D-01/D-04)

Source: core/archipelago/src/api/rpc/mesh/assistant.rs:27-30 (data_dir/secrets/claude-api-key) Apply to: backends/claude.rs — reuse the exact key path; do not introduce a parallel key location (this is also the fix for the port-3142 proxy's separate ANTHROPIC_API_KEY — see Open Question 1 in RESEARCH.md, which the plan must explicitly resolve).

Teleport-to-body modal (project-mandated pattern, repeatedly reinforced in CLAUDE.md)

Source: neode-ui/src/components/NostrSignConsent.vue Apply to: ToolConfirmModal.vue — full-screen backdrop, Teleport to="body", never rendered inside the iframe.

Scoped-token minting via authenticated RPC (never a long-lived credential in a URL)

Source: neode-ui/src/api/filebrowser-client.ts (pattern good) / same file (streamUrl, leak to avoid) Apply to: assistant-client.ts and any new content/tool streaming URL construction.

Budget-capped payment, never errors, degrades to None

Source: core/archipelago/src/swarm/payment.rs::auto_pay_token Apply to: backends/routstr.rs — reuse verbatim, do not reimplement Cashu token building.


No Analog Found

File Role Data Flow Reason
core/archipelago/src/assistant/loop_.rs service event-driven multi-turn First tool-calling agent loop in this codebase (confirmed by RESEARCH.md/AI-SPEC.md); build from the AI-SPEC §3/§4 sketch directly, not from an in-repo analog.
core/archipelago/src/assistant/tools.rs model/schema transform No curated-tool-registry precedent exists; D-06 explicitly rejects deriving it from dispatcher.rs. Build from AI-SPEC §4b.1's ToolDef/schemars sketch.
core/archipelago/src/assistant/backends/routstr.rs (HTTP client half) service request-response No OpenAI-compatible client exists in this codebase; wire format is CITED (medium confidence) from docs.routstr.com, not independently verified — RESEARCH.md recommends a live-relay spike before hand-writing this file.
core/archipelago/src/music/tags.rs utility transform New lofty dependency, no existing audio-tag-extraction code in this codebase; gate cargo add lofty behind checkpoint:human-verify per RESEARCH.md's package-legitimacy note.
neode-ui/src/composables/archyContentAdapter.ts utility (adapter) transform No shape-mapping precedent between Archy's ContentItem and any external metadata-rich type; must be hand-written and fixture-pinned (Pitfall 4).
Iframe sandbox enforcement mechanism (Chat.vue sandbox/CSP change, file TBD by the plan) config Open Question 2 in RESEARCH.md is explicitly unresolved — no existing sandbox/CSP-scoping code to copy; the plan must pick a mechanism (iframe sandbox attribute vs. connect-src scoping vs. accepted residual risk) before a file/pattern can be assigned.
Port-3142 claude-api-proxy.py retirement/gating (file TBD by the plan — nginx config edit, script edit, or deletion) config Open Question 1 in RESEARCH.md is explicitly unresolved (delete vs. gate vs. defer); no pattern to extract until the plan decides which.

Metadata

Analog search scope: core/archipelago/src/{mesh,api/rpc,swarm,streaming}/, neode-ui/src/{services,components,composables,api,types}/, /home/archipelago/Projects/AIUI/packages/{app,core}/src/ Files scanned: ~20 read/grepped directly across both repos Pattern extraction date: 2026-08-03