| 1 | //! Tauri bindings for [Ed](ed_agent::Ed). |
| 2 | //! |
| 3 | //! Ed's core is framework-agnostic and reports through a |
| 4 | //! [`StatusSink`](ed_agent::StatusSink). This crate is the Tauri implementation |
| 5 | //! of that sink plus the commands a webview calls. |
| 6 | //! |
| 7 | //! # Wiring it up |
| 8 | //! |
| 9 | //! Build an [`Ed`](ed_agent::Ed) around a [`TauriSink`] and manage it during |
| 10 | //! setup: |
| 11 | //! |
| 12 | //! ```no_run |
| 13 | //! use ed_agent::Ed; |
| 14 | //! use ed_agent_tauri::{EdState, TauriSink}; |
| 15 | //! use tauri::Manager; |
| 16 | //! |
| 17 | //! fn setup(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> { |
| 18 | //! let ed = Ed::with_sink(TauriSink::new(app.handle().clone())); |
| 19 | //! app.manage(EdState::new(ed)); |
| 20 | //! Ok(()) |
| 21 | //! } |
| 22 | //! ``` |
| 23 | //! |
| 24 | //! Then register the commands on the builder. This can't be a doc test — a |
| 25 | //! library has no `tauri.conf.json` for `generate_context!` to read: |
| 26 | //! |
| 27 | //! ```text |
| 28 | //! tauri::Builder::default() |
| 29 | //! .setup(|app| setup(app)) |
| 30 | //! .invoke_handler(tauri::generate_handler![ |
| 31 | //! ed_agent_tauri::chat_get_status, |
| 32 | //! ed_agent_tauri::chat_get_history, |
| 33 | //! ed_agent_tauri::chat_clear_history, |
| 34 | //! ed_agent_tauri::chat_send_message, |
| 35 | //! ed_agent_tauri::chat_run, |
| 36 | //! ed_agent_tauri::chat_cancel, |
| 37 | //! ]) |
| 38 | //! .run(tauri::generate_context!()) |
| 39 | //! .expect("run"); |
| 40 | //! ``` |
| 41 | //! |
| 42 | //! Loading a model is deliberately not a command here: which model, and from |
| 43 | //! where, is an application decision. Call [`Ed::load`](ed_agent::Ed::load) |
| 44 | //! from your own command or during setup. |
| 45 | //! |
| 46 | //! # Several conversations |
| 47 | //! |
| 48 | //! For an app with a session list, build an [`EdSessions`] instead of an |
| 49 | //! [`EdState`]. You supply a [`SessionStore`](ed_agent::SessionStore); it owns |
| 50 | //! the live session, the cache, the logout fence and approvals: |
| 51 | //! |
| 52 | //! ```text |
| 53 | //! let sessions = EdSessions::builder(app.handle().clone(), MyStore::new()) |
| 54 | //! .executor(my_executor) |
| 55 | //! .build(); |
| 56 | //! app.manage(sessions); |
| 57 | //! ``` |
| 58 | //! |
| 59 | //! Register `chat_list_sessions`, `chat_new_session`, `chat_switch_session`, |
| 60 | //! `chat_delete_session`, `chat_get_session_history`, `chat_submit` and |
| 61 | //! `chat_respond_approval`. `chat_submit` returns at once and the answer comes |
| 62 | //! as a `chat_reply` event `{id, session, reply, duration, error}`, with |
| 63 | //! `chat_text_delta {session, id, delta}` events while it generates. |
| 64 | //! |
| 65 | //! A host that has to rewrite the message first (to add context) or recover |
| 66 | //! from an engine failure wraps [`EdSessions::submit_with`] in a command of its |
| 67 | //! own. Call [`EdSessions::reset`] on logout or account switch. |
| 68 | //! |
| 69 | //! # Tools |
| 70 | //! |
| 71 | //! Tool calling works through this crate, but the host supplies the executor. |
| 72 | //! With [`EdSessions`] approvals are handled for you: [`TauriApprovals`] emits |
| 73 | //! `chat_approval_requested {request_id, session, call, risk}`, waits (120 s by |
| 74 | //! default) for `chat_respond_approval {request_id, decision}`, then emits |
| 75 | //! `chat_approval_resolved {request_id}`. A timeout or a reset denies. |
| 76 | //! |
| 77 | //! With [`EdState`], build the agent with |
| 78 | //! [`Ed::with_agent`](ed_agent::Ed::with_agent) and your own |
| 79 | //! [`ApprovalHandler`](ed_agent::ApprovalHandler); the default `Ed::with_sink` |
| 80 | //! rejects every tool and denies every approval. |
| 81 | //! |
| 82 | //! # Events |
| 83 | //! |
| 84 | //! The first two are named exactly as the existing applications already emit |
| 85 | //! them, so a frontend needs no change: |
| 86 | //! |
| 87 | //! - [`EVENT_CHAT_STATUS_CHANGED`] with a [`ChatStatusPayload`] |
| 88 | //! - [`EVENT_CHAT_REPLY`] with a [`ChatReply`](ed_agent::ChatReply) |
| 89 | //! - [`EVENT_CHAT_TOOL_REQUESTED`] with a [`ToolCall`](ed_agent::ToolCall) |
| 90 | //! - [`EVENT_CHAT_APPROVAL_REQUESTED`] with an |
| 91 | //! [`ApprovalRequest`](ed_agent::ApprovalRequest) |
| 92 | //! - [`EVENT_CHAT_TOOL_STARTED`] with a [`ToolCall`](ed_agent::ToolCall) |
| 93 | //! - [`EVENT_CHAT_TOOL_FINISHED`] with a |
| 94 | //! [`ToolExecutionResult`](ed_agent::ToolExecutionResult) |
| 95 | //! - [`EVENT_CHAT_AGENT_REPLY`] with an [`AgentReply`](ed_agent::AgentReply) |
| 96 | //! - [`EVENT_CHAT_WARNING`] with a string |
| 97 | //! - [`EVENT_CHAT_TEXT_DELTA`] with a [`TextDeltaPayload`] |
| 98 | //! - [`EVENT_CHAT_APPROVAL_RESOLVED`] with an [`ApprovalResolvedPayload`] |
| 99 | |
| 100 | #![forbid(unsafe_code)] |
| 101 | |
| 102 | mod approvals; |
| 103 | mod commands; |
| 104 | mod events; |
| 105 | mod scope; |
| 106 | mod sessions; |
| 107 | mod sink; |
| 108 | |
| 109 | pub use approvals::{TauriApprovals, DEFAULT_APPROVAL_TIMEOUT}; |
| 110 | pub use commands::{ |
| 111 | chat_cancel, chat_clear_history, chat_delete_session, chat_get_history, |
| 112 | chat_get_session_history, chat_get_status, chat_list_sessions, chat_new_session, |
| 113 | chat_respond_approval, chat_run, chat_send_message, chat_submit, chat_switch_session, EdState, |
| 114 | }; |
| 115 | pub use events::{ |
| 116 | ApprovalRequestedPayload, ApprovalResolvedPayload, ChatStatusPayload, SubmitReplyPayload, |
| 117 | TextDeltaPayload, EVENT_CHAT_AGENT_REPLY, EVENT_CHAT_APPROVAL_REQUESTED, |
| 118 | EVENT_CHAT_APPROVAL_RESOLVED, EVENT_CHAT_REPLY, EVENT_CHAT_STATUS_CHANGED, |
| 119 | EVENT_CHAT_TEXT_DELTA, EVENT_CHAT_TOOL_FINISHED, EVENT_CHAT_TOOL_REQUESTED, |
| 120 | EVENT_CHAT_TOOL_STARTED, EVENT_CHAT_WARNING, |
| 121 | }; |
| 122 | pub use sessions::{EdSessions, EdSessionsBuilder}; |
| 123 | pub use sink::TauriSink; |