265 lines
16 KiB
Markdown
265 lines
16 KiB
Markdown
# 04 — API Contracts
|
|
|
|
Two contract surfaces:
|
|
1. **Frontend ⇄ Rust** — Tauri **commands** (request/response) and **events** (Rust→UI push).
|
|
2. **Rust internal** — service **traits** that decouple callers from concrete engines (NFR-MNT-1/2).
|
|
|
|
All payloads are `serde`-serializable; timestamps are unix epoch ms unless noted. Errors are typed:
|
|
each command returns `Result<T, WaError>` where `WaError` carries a `kind` (machine-readable) and a
|
|
`message` (human-readable).
|
|
|
|
## 1. Tauri commands (frontend → Rust)
|
|
|
|
```ts
|
|
// ---- Recording lifecycle ----
|
|
// `record` (default false) controls audio RETENTION (ADR-0009). When false, working audio is
|
|
// deleted on finalize and only the transcript/notes persist. It can be toggled mid-meeting.
|
|
// templateId (Phase 8, T8.1, FR-NOTE-5) picks a NoteTemplate — see list_note_templates below.
|
|
start_recording(input: { meetingTitle?: string; calendarEventId?: string; record?: boolean; templateId?: string }): MeetingId
|
|
stop_recording(input: { meetingId: MeetingId }): MeetingSummaryRef
|
|
pause_recording(input: { meetingId: MeetingId }): void
|
|
resume_recording(input: { meetingId: MeetingId }): void
|
|
set_recording_retention(input: { meetingId: MeetingId; record: boolean }): void // toggle mid-meeting (FR-REC-1)
|
|
acknowledge_recording_consent(): void // one-time (FR-REC-2)
|
|
|
|
// ---- Hardware ----
|
|
hardware_status(): { backends: BackendInfo[]; active: BackendId; modelSize: string; estRtf: number }
|
|
set_preferred_backend(input: { backend: BackendId | "auto" }): void
|
|
|
|
// ---- Transcription / models ----
|
|
reprocess_transcript(input: { meetingId: MeetingId; model: string }): void // batch mode (FR-TRX-3)
|
|
list_models(): ModelInfo[]
|
|
list_diarization_models(): ModelInfo[] // fixed seg+emb pair (T4.7, FR-MODEL-1)
|
|
download_model(input: { kind: "whisper" | "diar-seg" | "diar-emb"; id: string }): void // emits progress events
|
|
remove_model(input: { id: string }): void // disambiguated by id, not kind — ids never collide across catalogs
|
|
|
|
// ---- Speakers ----
|
|
rename_speaker(input: { meetingId: MeetingId; label: string; name: string }): void
|
|
merge_speakers(input: { meetingId: MeetingId; from: string[]; into: string }): void
|
|
map_speaker_to_participant(input: { meetingId: MeetingId; label: string; participantId: string }): void
|
|
|
|
// ---- Meetings / storage ----
|
|
// MeetingListItem gained a `tags: string[]` field (Phase 8, FR-SEARCH-2).
|
|
// list_meetings dropped limit/offset (never implemented — no pagination need
|
|
// yet at local-desktop meeting counts) and gained from/to date filters, per
|
|
// FR-SEARCH-2's "filter by date, tag, or participant".
|
|
list_meetings(input: { query?: string; tag?: string; participantId?: string; from?: number; to?: number }): MeetingListItem[]
|
|
get_meeting(input: { meetingId: MeetingId }): Meeting // includes transcript + speakers + summary (null until generated)
|
|
delete_meeting(input: { meetingId: MeetingId }): void
|
|
export_meeting(input: { meetingId: MeetingId; dest: string; format: "md" | "pdf" | "docx" | "bundle" }): string
|
|
update_notes(input: { meetingId: MeetingId; markdown: string }): void
|
|
// SearchHit = MeetingListItem fields (id, title, started_at, duration_secs, status, tags) + snippet: string
|
|
search(input: { query: string }): SearchHit[] // FTS (FR-SEARCH-1)
|
|
set_tags(input: { meetingId: MeetingId; tags: string[] }): void
|
|
list_tags(): string[] // all known tag names, sorted
|
|
// NoteTemplate = { id: string, name: string, sections: string[] } (T8.1, FR-NOTE-5). Meeting
|
|
// gained `template_id: string | null` — the template picked at start_recording.
|
|
list_note_templates(): NoteTemplate[]
|
|
// Bulk export (T8.5, FR-STORE-4): every meeting matching tag/from/to, one file (or bundle
|
|
// folder) per meeting under destDir. Returns the count actually exported.
|
|
bulk_export_meetings(input: { destDir: string; format: "md" | "pdf" | "docx" | "bundle"; tag?: string; from?: number; to?: number }): number
|
|
|
|
// ---- LLM / AI provider (ADR-0007/0011) ----
|
|
// provider ∈ ollama | custom | anthropic | openai | off. Hosted-provider API keys are passed to
|
|
// set_llm_provider but stored only in the OS credential store; never returned by llm_status.
|
|
llm_status(): { provider: string; reachable: boolean; isLocal: boolean; models: string[] }
|
|
set_llm_provider(input: { provider: string; endpoint?: string; model?: string; apiKey?: string }): void // FR-AI-1/2
|
|
generate_summary(input: { meetingId: MeetingId; templateId?: string }): void // streams via events (FR-LLM-2/4)
|
|
// ActionItem gained `reminder_set: boolean` (T8.6, FR-CAL-5). confirm_action_items now also
|
|
// schedules/cancels each item's local reminder (Windows scheduled toast notification — the OS
|
|
// itself delivers it at due_at, no app-side polling timer; see src-tauri/src/reminders.rs).
|
|
confirm_action_items(input: { meetingId: MeetingId; items: ActionItem[] }): void
|
|
llm_setup_suggestions(): { ollamaInstalled: boolean; installUrl: string; suggestedModel: { id: string; label: string; approxSizeGb: number } } // T5.7, FR-LLM-5
|
|
pull_ollama_model(input: { model: string }): void // guided download via Ollama's own /api/pull; emits model://progress (T5.7)
|
|
|
|
// ---- Calendar / .pst ----
|
|
import_pst(input: { path: string; password?: string }): number // eventsImported; emits pst://progress (FR-CAL-1)
|
|
list_calendar_events(input: { from?: number; to?: number }): CalendarEvent[]
|
|
get_calendar_event(input: { eventId: string }): { event: CalendarEvent; participants: Participant[] } // pre-meeting panel + naming dropdown (FR-CAL-3, FR-SPK-4)
|
|
attach_meeting_to_event(input: { meetingId: MeetingId; eventId: string }): void
|
|
|
|
// ---- Sync / upload (ADR-0010) ---- secrets are passed to add/update but stored only in the OS
|
|
// credential store; they are NEVER returned by list_sync_targets.
|
|
list_sync_targets(): SyncTargetInfo[]
|
|
add_sync_target(input: SyncTargetConfig & { secret: string }): SyncTargetInfo // FR-SYNC-2/9
|
|
update_sync_target(input: { id: string } & Partial<SyncTargetConfig> & { secret?: string }): SyncTargetInfo
|
|
remove_sync_target(input: { id: string }): void
|
|
test_sync_target(input: { id: string } | SyncTargetConfig & { secret: string }): { ok: boolean; message: string } // FR-SYNC-4
|
|
set_sync_enabled(input: { enabled: boolean }): void // master switch (FR-SYNC-1)
|
|
sync_meeting(input: { meetingId: MeetingId; targetId?: string }): void // manual "Upload now" (FR-SYNC-3)
|
|
sync_status(input?: { meetingId?: MeetingId }): SyncJobInfo[] // queue state (FR-SYNC-5)
|
|
retry_sync_job(input: { jobId: string }): void
|
|
// OAuth (secondary targets, FR-SYNC-9): begins loopback-redirect PKCE flow, returns when linked.
|
|
begin_oauth_link(input: { kind: "onedrive" | "dropbox" | "box" }): { ok: boolean; account?: string }
|
|
|
|
// ---- Feature briefs + MCP server (ADR-0011) ----
|
|
create_feature_brief(input: { meetingId: MeetingId; targetRepo?: string }): FeatureBrief // FR-MCP-4 (LLM-distilled)
|
|
list_feature_briefs(input: { meetingId?: MeetingId }): FeatureBriefInfo[]
|
|
get_feature_brief(input: { id: string }): FeatureBrief
|
|
set_brief_exposed(input: { id: string; exposed: boolean }): void // scope control (FR-MCP-3)
|
|
mcp_status(): { enabled: boolean; transport: "http" | "stdio"; endpoint: string; tokenSet: boolean; exposeScope: string }
|
|
set_mcp_enabled(input: { enabled: boolean; transport?: "http" | "stdio"; port?: number }): { endpoint: string; token: string } // FR-MCP-1/6
|
|
set_mcp_scope(input: { expose: "none" | "selected" | "all"; exposeRecordings?: boolean }): void // FR-MCP-3
|
|
mcp_access_log(input?: { limit?: number }): McpAccessEntry[] // audit (FR-MCP-5)
|
|
// (Layer 3, later) push handoff — spawn a local agent CLI / open a tracker issue from a brief.
|
|
run_agent(input: { briefId: string; tool: "claude" | "codex" | "opencode" | "copilot"; repoPath: string }): { ok: boolean } // FR-AGENT-1
|
|
create_issue_from_brief(input: { briefId: string; tracker: "github"; assignCopilot?: boolean }): { url: string } // FR-AGENT-2
|
|
|
|
// ---- Settings ----
|
|
get_settings(): Settings
|
|
update_settings(input: Partial<Settings>): Settings
|
|
// Reports the full egress allowlist so the UI can prove exactly what may leave the device (FR-SEC-2).
|
|
privacy_self_check(): {
|
|
llmEndpoint: string; llmIsLocal: boolean;
|
|
syncEnabled: boolean;
|
|
syncTargets: { name: string; host: string; thirdParty: boolean; tls: boolean }[];
|
|
allowlistedHosts: string[];
|
|
}
|
|
```
|
|
|
|
### Conventions
|
|
- Commands return promptly; anything long-running (recording, transcription, summary, model
|
|
download, PST import) reports progress/results through **events** below.
|
|
- `MeetingId` is a uuid string. `BackendId` ∈ `"npu" | "nvidia" | "amd" | "intel" | "cpu"`.
|
|
|
|
## 2. Tauri events (Rust → frontend)
|
|
|
|
```ts
|
|
"recording://state" { meetingId, state: "recording"|"paused"|"stopped", elapsedMs }
|
|
"recording://level" { meetingId, rms: number, peak: number } // waveform (FR-CAP-5)
|
|
"recording://device" { meetingId, recovered: boolean, message: string } // capture device change (FR-CAP-6)
|
|
"transcript://segment" { meetingId, segment: TranscriptSegment } // live segments (FR-TRX-2)
|
|
"transcript://finalized" { meetingId, segmentCount }
|
|
"diarization://updated" { meetingId, speakers: SpeakerInfo[] } // after post-pass (FR-SPK)
|
|
"llm://token" { meetingId, text } // streamed summary (FR-LLM-4)
|
|
"llm://done" { meetingId, summary: SummaryFile } // full summary.json contents, not just a pointer
|
|
"model://progress" { id, receivedBytes, totalBytes }
|
|
"pst://progress" { processed, total }
|
|
"hardware://changed" { active: BackendId, reason: string } // fallback occurred (FR-HW-4)
|
|
"recording://retention" { meetingId, record: boolean } // retention toggled (FR-REC-1/3)
|
|
"sync://job" { jobId, meetingId, targetId, artifact, status, bytesSent, bytesTotal } // FR-SYNC-5
|
|
"sync://done" { meetingId, targetId, uploaded: number, failed: number }
|
|
"mcp://access" { at, tool, meetingId?, client? } // agent read something (FR-MCP-5)
|
|
"agent://progress" { briefId, tool, line } // push run output (FR-AGENT-1)
|
|
"error" { kind, message, context? }
|
|
```
|
|
|
|
## 3. Internal Rust service traits
|
|
|
|
These define the seams that let engines be swapped without touching callers. Signatures are
|
|
indicative (async where I/O-bound).
|
|
|
|
```rust
|
|
// audio/mod.rs
|
|
pub trait AudioCapture: Send + Sync {
|
|
/// Begin WASAPI loopback capture, writing PCM to `wav_path`; frames also pushed to `sink`.
|
|
fn start(&self, wav_path: &Path, sink: FrameSink) -> Result<CaptureHandle, AudioError>;
|
|
fn pause(&self, h: &CaptureHandle) -> Result<(), AudioError>;
|
|
fn resume(&self, h: &CaptureHandle) -> Result<(), AudioError>;
|
|
fn stop(&self, h: CaptureHandle) -> Result<CaptureSummary, AudioError>;
|
|
}
|
|
|
|
// hardware/mod.rs
|
|
pub trait HardwareDetector: Send + Sync {
|
|
fn detect(&self) -> Vec<BackendInfo>; // ranked NPU→NVIDIA→AMD→Intel→CPU
|
|
fn best(&self, preferred: Option<BackendId>) -> BackendInfo;
|
|
}
|
|
|
|
// transcription/mod.rs
|
|
pub trait Transcriber: Send + Sync {
|
|
fn load(model: &Path, backend: BackendId) -> Result<Self, TrxError> where Self: Sized;
|
|
/// Stream interim + final segments for an audio window.
|
|
fn transcribe_stream(&self, audio: AudioWindow, out: SegmentSink) -> Result<(), TrxError>;
|
|
/// One-shot batch transcription (higher accuracy).
|
|
fn transcribe_file(&self, wav: &Path) -> Result<Vec<TranscriptSegment>, TrxError>;
|
|
}
|
|
|
|
// diarization/mod.rs
|
|
pub trait Diarizer: Send + Sync {
|
|
fn diarize(&self, wav: &Path) -> Result<Vec<SpeakerSpan>, DiarError>;
|
|
fn assign(&self, segments: &mut [TranscriptSegment], spans: &[SpeakerSpan]);
|
|
}
|
|
|
|
// storage/mod.rs (async, sqlx)
|
|
#[async_trait] pub trait Store: Send + Sync {
|
|
async fn create_meeting(&self, m: NewMeeting) -> Result<MeetingId, StoreError>;
|
|
async fn finalize_meeting(&self, id: &MeetingId, s: FinalizeMeeting) -> Result<(), StoreError>;
|
|
async fn list_meetings(&self, f: MeetingFilter) -> Result<Vec<MeetingListItem>, StoreError>;
|
|
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError>;
|
|
async fn delete_meeting(&self, id: &MeetingId) -> Result<(), StoreError>;
|
|
async fn search(&self, q: &str) -> Result<Vec<SearchHit>, StoreError>;
|
|
async fn recover_scan(&self) -> Result<Vec<MeetingId>, StoreError>; // FR-REL-1
|
|
async fn enforce_retention(&self, policy: Retention) -> Result<u32, StoreError>;
|
|
}
|
|
|
|
// llm/mod.rs
|
|
#[async_trait] pub trait LlmProvider: Send + Sync {
|
|
async fn status(&self) -> LlmStatus; // reachable? local? models
|
|
async fn summarize(&self, prompt: Prompt, out: TokenSink) -> Result<Summary, LlmError>;
|
|
fn is_local(&self) -> bool; // FR-LLM-6 guard
|
|
}
|
|
|
|
// calendar/mod.rs
|
|
pub trait CalendarSource: Send + Sync {
|
|
fn import(&self, input: CalImport) -> Result<Vec<CalendarEvent>, CalError>; // pst|graph|ics
|
|
fn attendees(&self, event_id: &str) -> Result<Vec<Participant>, CalError>;
|
|
}
|
|
|
|
// notes/mod.rs
|
|
pub trait NotesRenderer: Send + Sync {
|
|
fn to_markdown(&self, t: &Transcript, speakers: &[SpeakerInfo], s: Option<&Summary>) -> String;
|
|
fn export(&self, md: &str, dest: &Path, fmt: ExportFormat) -> Result<PathBuf, NotesError>;
|
|
}
|
|
|
|
// sync/mod.rs
|
|
// One impl per provider; `WebDavTarget` covers Nextcloud/ownCloud/Cloudreve/Seafile/Synology.
|
|
// Secondary OAuth impls: OneDriveTarget, DropboxTarget, BoxTarget (ADR-0010).
|
|
#[async_trait] pub trait SyncTarget: Send + Sync {
|
|
fn kind(&self) -> SyncKind;
|
|
fn is_third_party(&self) -> bool; // false for self-hosted WebDAV
|
|
async fn test(&self) -> Result<(), SyncError>; // reachability + auth (FR-SYNC-4)
|
|
async fn ensure_dir(&self, remote_dir: &str) -> Result<(), SyncError>;
|
|
async fn exists(&self, remote_path: &str, sha256: &str) -> Result<bool, SyncError>; // skip-if-unchanged
|
|
/// Upload a file; resumable/chunked for large artifacts. Reports progress via `prog`.
|
|
async fn put(&self, local: &Path, remote_path: &str, prog: ProgressSink) -> Result<(), SyncError>;
|
|
}
|
|
|
|
// Owns the durable queue, retry/backoff, credential resolution, and TLS enforcement.
|
|
#[async_trait] pub trait SyncManager: Send + Sync {
|
|
async fn enqueue_meeting(&self, meeting_id: &MeetingId, target_id: Option<&str>) -> Result<(), SyncError>;
|
|
async fn pump(&self) -> Result<(), SyncError>; // drive pending jobs (called on finalize + startup + timer)
|
|
async fn status(&self, meeting_id: Option<&MeetingId>) -> Result<Vec<SyncJobInfo>, SyncError>;
|
|
async fn retry(&self, job_id: &str) -> Result<(), SyncError>;
|
|
}
|
|
|
|
// llm/mod.rs — `LlmProvider` (above) gains hosted impls behind the same trait (ADR-0011):
|
|
// OllamaProvider (local) · OpenAiCompatProvider (/v1/chat/completions) · AnthropicProvider (/v1/messages)
|
|
// `is_local()` stays the egress guard; hosted impls return false and require an API key from the keychain.
|
|
|
|
// mcp/mod.rs — WA as an MCP server (loopback, off by default). Tools-first (FR-MCP-2).
|
|
#[async_trait] pub trait McpServer: Send + Sync {
|
|
async fn start(&self, cfg: McpConfig) -> Result<McpHandle, McpError>; // returns endpoint + token
|
|
async fn stop(&self, h: McpHandle) -> Result<(), McpError>;
|
|
fn tools(&self) -> Vec<McpToolDescriptor>; // list_recent_meetings, get_transcript, get_action_items, get_feature_brief
|
|
}
|
|
// Builds the agent-ready spec from a transcript via the configured LlmProvider.
|
|
#[async_trait] pub trait FeatureBriefBuilder: Send + Sync {
|
|
async fn build(&self, meeting_id: &MeetingId, target_repo: Option<&str>) -> Result<FeatureBrief, BriefError>;
|
|
}
|
|
|
|
// agent/mod.rs — Layer 3 (later). Push handoff; one impl per CLI / tracker.
|
|
#[async_trait] pub trait AgentRunner: Send + Sync {
|
|
async fn run(&self, brief: &FeatureBrief, repo: &Path, out: LineSink) -> Result<RunOutcome, AgentError>; // claude -p / codex exec / …
|
|
}
|
|
#[async_trait] pub trait IssueTracker: Send + Sync {
|
|
async fn create_issue(&self, brief: &FeatureBrief, assign_copilot: bool) -> Result<String /*url*/, AgentError>;
|
|
}
|
|
```
|
|
|
|
## Versioning
|
|
|
|
- Command/event names and payload shapes are versioned implicitly by `schema` fields in persisted
|
|
JSON (`03-data-model.md`) and explicitly in a `CONTRACTS_VERSION` constant. Breaking a command
|
|
shape requires bumping it and updating the frontend client in the same change (see `CLAUDE.md`
|
|
"source of truth" rule).
|