@ondeinference / ed / commits / 73b632f

Rename to ed-agent, add load_with, rewrite the README

The crates publish as ed-agent and ed-agent-tauri. The repository URL was wrong too: it said ondeinference/onde-ed, and the repo is ondeinference/ed. Both are permanent once published. load_with runs any load closure and reports Loading then Ready, or Loading then Error, around it. Wrapping onde's load paths by name would have put smbcloud_gresiq_sdk::Environment in Ed's public signature, and that type changed crates between onde 1.1 and 1.2, so every host would have had to agree on Ed's version of it. Ed::load is now a thin call into load_with. The tests assert the recorded sequence on both outcomes, since the ordering is the point. The README no longer says "local-Lllm-first". It leads with the pitch and tells the four-apps-wrote-it-four-times story. SplitFire Audio Separator is gone from "See it live": that app runs Demucs, not a language model, so Ed isn't in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Seto committed Sep 8, 2026 at 19:26 UTC 73b632f7f97da4782ae85a9bae6c231efae748f3
15 files changed +284 -75
Cargo.lock
+20 -20
@@ -1684,6 +1684,26 @@ version = "0.1.3"
1684 source = "registry+https://github.com/rust-lang/crates.io-index"
1685 checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9"
1686
1687 +[[package]]
1688 +name = "ed-agent"
1689 +version = "0.1.0"
1690 +dependencies = [
1691 + "log",
1692 + "onde",
1693 + "serde",
1694 + "tokio",
1695 +]
1696 +
1697 +[[package]]
1698 +name = "ed-agent-tauri"
1699 +version = "0.1.0"
1700 +dependencies = [
1701 + "ed-agent",
1702 + "log",
1703 + "serde",
1704 + "tauri",
1705 +]
1706 +
1707 [[package]]
1708 name = "ego-tree"
1709 version = "0.10.0"
@@ -4548,26 +4568,6 @@ dependencies = [
4568 "thiserror 2.0.20",
4569 ]
4570
4551 -[[package]]
4552 -name = "onde-ed"
4553 -version = "0.1.0"
4554 -dependencies = [
4555 - "log",
4556 - "onde",
4557 - "serde",
4558 - "tokio",
4559 -]
4560 -
4561 -[[package]]
4562 -name = "onde-ed-tauri"
4563 -version = "0.1.0"
4564 -dependencies = [
4565 - "log",
4566 - "onde-ed",
4567 - "serde",
4568 - "tauri",
4569 -]
4570 -
4571 [[package]]
4572 name = "onde-mistralrs"
4573 version = "0.9.3"
Cargo.toml
+3 -3
@@ -1,6 +1,6 @@
1 [workspace]
2 resolver = "2"
3 -members = ["crates/onde-ed", "crates/onde-ed-tauri"]
3 +members = ["crates/ed-agent", "crates/ed-agent-tauri"]
4
5 # Shared across every crate in the workspace so a release moves one number.
6 # `sync-version` tooling (see the Makefile) reads these.
@@ -10,7 +10,7 @@ edition = "2021"
10 rust-version = "1.82"
11 license = "Apache-2.0"
12 authors = ["Splitfire AB"]
13 -repository = "https://github.com/ondeinference/onde-ed"
13 +repository = "https://github.com/ondeinference/ed"
14 homepage = "https://ondeinference.com"
15
16 [workspace.dependencies]
@@ -24,4 +24,4 @@ tokio = { version = "1", features = ["sync"] }
24 tauri = { version = "2", features = [] }
25
26 # Internal
27 -onde-ed = { version = "0.1.0", path = "crates/onde-ed" }
27 +ed-agent = { version = "0.1.0", path = "crates/ed-agent" }
README.md
+65 -3
@@ -1,13 +1,75 @@
1 # Ed
2
3 -Embed Ed, a local-Lllm-first AI agent, into your application. Free the intelligence.
3 +**Embed Ed.** Your app gets a brain. Your users keep their data.
4 +
5 +Ed is a chat agent you drop into an application. It loads an open-weights model,
6 +tells your interface when that model is ready, keeps the conversation, and hands
7 +back replies. The model runs on the user's device — there is no API key, no
8 +metered token, no round trip, and nothing to leak.
9 +
10 +## Why it exists
11 +
12 +Four applications needed the same thing: load a model, know whether it's ready,
13 +keep a conversation, hand back a reply. Four times somebody wrote it again. Two
14 +of them ended up byte-identical in three files.
15 +
16 +None of that code was interesting. It was the wiring between an inference engine
17 +that reports status by polling and a user interface that needs to be told —
18 +plus the flattening of `Result<InferenceResult, _>` into one payload a view can
19 +render either way. Ed is that layer, written once.
20 +
21 +It deliberately doesn't own sessions, persistence, model catalogues, or
22 +downloads. Those differ per application and stay yours.
23 +
24 +## Onde runs the model. Ed makes it a feature.
25 +
26 +[Onde](https://ondeinference.com) is the inference engine: weights, quantisation,
27 +Metal and CPU backends, tokens out. Ed is the part above it your users touch.
28
29 ## See it live
30
7 -- [smbCloud Mail](https://smbcloudmail.com/en-us).
8 -- [SplitFire AI Audio Separator](https://splitfire.ai/).
31 +- **[smbCloud MailX](https://smbcloudmail.com/en-us)** — siMail drafts replies,
32 + summarises long threads, and helps triage the inbox. The mail never leaves the
33 + machine to get read.
34 +- **[Siti AI](https://github.com/getsigit/siti)** — a private assistant for
35 + macOS, iOS, and Android, and the open reference app: its source is public, so
36 + it's the codebase to read for a full integration.
37 +- **[Pepak Basa Jawa Komplit](https://apps.apple.com/se/app/pepak-basa-jawa-komplit/id6752241812)**
38 + — Joko, a Javanese tutor who answers offline, which matters where the app is
39 + used.
40 +- **[Rumi Learn Persian](https://apps.apple.com/se/app/rumi-l%C3%A4r-dig-persiska/id6753832408)**
41 + — Rumi, a Persian tutor, same model, same device.
42 +
43 +## Quick start
44 +
45 +```toml
46 +[dependencies]
47 +ed-agent = "0.1"
48 +```
49 +
50 +```rust
51 +use ed_agent::{Ed, GgufModelConfig};
52 +
53 +let ed = Ed::new();
54 +ed.load(GgufModelConfig::qwen25_1_5b(), None, None).await?;
55 +
56 +let reply = ed.send("Summarise this thread.").await;
57 +println!("{}", reply.reply.unwrap_or_default());
58 +```
59 +
60 +That's a working agent. To hear about state changes rather than polling for
61 +them, implement `StatusSink` and build with `Ed::with_sink`.
62 +
63 +## Crates
64 +
65 +| Crate | What it's for |
66 +| --- | --- |
67 +| [`ed-agent`](crates/ed-agent) | The agent. No framework dependency. |
68 +| [`ed-agent-tauri`](crates/ed-agent-tauri) | Tauri bindings: a sink that emits to the webview, plus the commands it invokes. |
69
70 +Free the intelligence.
71
72 ## Copyright
73
74 © 2026 [Splitfire AB](https://5mb.app) ([Onde Inference](https://ondeinference.com)).
75 +Licensed under Apache-2.0.
crates/ed-agent-tauri/Cargo.toml renamed
+3 -2
@@ -1,8 +1,9 @@
1 [package]
2 -name = "onde-ed-tauri"
2 +name = "ed-agent-tauri"
3 description = "Tauri bindings for Ed: emits chat lifecycle and reply events to the webview."
4 keywords = ["tauri", "llm", "ai", "agent", "on-device"]
5 categories = ["api-bindings", "gui"]
6 +readme = "README.md"
7 version.workspace = true
8 edition.workspace = true
9 rust-version.workspace = true
@@ -12,7 +13,7 @@ repository.workspace = true
13 homepage.workspace = true
14
15 [dependencies]
15 -onde-ed.workspace = true
16 +ed-agent.workspace = true
17 tauri.workspace = true
18 serde.workspace = true
19 log.workspace = true
crates/ed-agent-tauri/README.md new
+29
@@ -0,0 +1,29 @@
1 +# ed-agent-tauri
2 +
3 +Tauri bindings for [Ed](https://crates.io/crates/ed-agent), the embeddable
4 +local-model chat agent.
5 +
6 +Ed's core is framework-agnostic and reports through a `StatusSink`. This crate
7 +is the Tauri implementation of that sink plus the commands a webview calls.
8 +
9 +```rust
10 +use ed_agent::Ed;
11 +use ed_agent_tauri::{EdState, TauriSink};
12 +use tauri::Manager;
13 +
14 +let ed = Ed::with_sink(TauriSink::new(app.handle().clone()));
15 +app.manage(EdState::new(ed));
16 +```
17 +
18 +Then register `chat_get_status`, `chat_get_history`, `chat_clear_history`, and
19 +`chat_send_message` on the builder's `invoke_handler`. Two events reach the
20 +webview: `chat_status_changed` and `chat_reply`.
21 +
22 +Loading a model is deliberately not a command here — which model, and from
23 +where, is an application decision. Call `Ed::load` or `Ed::load_with` from your
24 +own command.
25 +
26 +## Copyright
27 +
28 +© 2026 [Splitfire AB](https://5mb.app) ([Onde Inference](https://ondeinference.com)).
29 +Licensed under Apache-2.0.
crates/ed-agent-tauri/src/commands.rs renamed
+1 -1
@@ -1,6 +1,6 @@
1 //! The commands a webview invokes.
2
3 -use onde_ed::{ChatMessage, ChatReply, Ed, EngineInfo};
3 +use ed_agent::{ChatMessage, ChatReply, Ed, EngineInfo};
4 use tauri::State;
5
6 use crate::sink::TauriSink;
crates/ed-agent-tauri/src/events.rs renamed
+1 -1
@@ -1,6 +1,6 @@
1 //! Event names and payloads emitted to the webview.
2
3 -use onde_ed::EngineStatus;
3 +use ed_agent::EngineStatus;
4 use serde::{Deserialize, Serialize};
5
6 /// Emitted whenever the engine's lifecycle status changes.
crates/ed-agent-tauri/src/lib.rs renamed
+11 -11
@@ -1,17 +1,17 @@
1 -//! Tauri bindings for [Ed](onde_ed::Ed).
1 +//! Tauri bindings for [Ed](ed_agent::Ed).
2 //!
3 //! Ed's core is framework-agnostic and reports through a
4 -//! [`StatusSink`](onde_ed::StatusSink). This crate is the Tauri implementation
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`](onde_ed::Ed) around a [`TauriSink`] and manage it during
9 +//! Build an [`Ed`](ed_agent::Ed) around a [`TauriSink`] and manage it during
10 //! setup:
11 //!
12 //! ```no_run
13 -//! use onde_ed::Ed;
14 -//! use onde_ed_tauri::{EdState, TauriSink};
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>> {
@@ -28,17 +28,17 @@
28 //! tauri::Builder::default()
29 //! .setup(|app| setup(app))
30 //! .invoke_handler(tauri::generate_handler![
31 -//! onde_ed_tauri::chat_get_status,
32 -//! onde_ed_tauri::chat_get_history,
33 -//! onde_ed_tauri::chat_clear_history,
34 -//! onde_ed_tauri::chat_send_message,
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 //! ])
36 //! .run(tauri::generate_context!())
37 //! .expect("run");
38 //! ```
39 //!
40 //! Loading a model is deliberately not a command here: which model, and from
41 -//! where, is an application decision. Call [`Ed::load`](onde_ed::Ed::load)
41 +//! where, is an application decision. Call [`Ed::load`](ed_agent::Ed::load)
42 //! from your own command or during setup.
43 //!
44 //! # Events
@@ -47,7 +47,7 @@
47 //! already emit them so a frontend needs no change:
48 //!
49 //! - [`EVENT_CHAT_STATUS_CHANGED`] with a [`ChatStatusPayload`]
50 -//! - [`EVENT_CHAT_REPLY`] with a [`ChatReply`](onde_ed::ChatReply)
50 +//! - [`EVENT_CHAT_REPLY`] with a [`ChatReply`](ed_agent::ChatReply)
51
52 #![forbid(unsafe_code)]
53
crates/ed-agent-tauri/src/sink.rs renamed
+1 -1
@@ -1,7 +1,7 @@
1 //! Bridges Ed's notifications onto Tauri's event bus.
2
3 +use ed_agent::{ChatReply, EngineStatus, StatusSink};
4 use log::warn;
4 -use onde_ed::{ChatReply, EngineStatus, StatusSink};
5 use tauri::{AppHandle, Emitter};
6
7 use crate::events::{ChatStatusPayload, EVENT_CHAT_REPLY, EVENT_CHAT_STATUS_CHANGED};
crates/ed-agent/Cargo.toml renamed
+1 -1
@@ -1,5 +1,5 @@
1 [package]
2 -name = "onde-ed"
2 +name = "ed-agent"
3 description = "Embeddable local-LLM-first chat agent: engine lifecycle, history, and status over the Onde inference SDK."
4 keywords = ["llm", "ai", "agent", "local", "on-device"]
5 categories = ["api-bindings", "asynchronous"]
crates/ed-agent/README.md renamed
+27 -7
@@ -1,10 +1,11 @@
1 -# onde-ed
1 +# ed-agent
2
3 -Embed Ed, a local-LLM-first AI agent, into your application.
3 +**Embed Ed.** Your app gets a brain. Your users keep their data.
4
5 -Ed sits one layer above the [`onde`](https://crates.io/crates/onde) inference
6 -SDK. `onde` gives you an engine to load a model into and send messages to; Ed
7 -adds the part every application built on it was writing for itself.
5 +Ed is a chat agent you drop into an application. It sits one layer above the
6 +[`onde`](https://crates.io/crates/onde) inference SDK: `onde` gives you an
7 +engine to load a model into and send messages to, and Ed adds the part every
8 +application built on it was writing for itself.
9
10 - **Lifecycle orchestration.** Loading a model is a sequence of state changes,
11 not one call. Ed drives `Loading -> Ready` (or `-> Error`) and reports each
@@ -20,7 +21,7 @@ downloads. Those differ per application and belong to you.
21 ## Usage
22
23 ```rust
23 -use onde_ed::{Ed, GgufModelConfig};
24 +use ed_agent::{Ed, GgufModelConfig};
25
26 let ed = Ed::new();
27 ed.load(GgufModelConfig::qwen25_1_5b(), None, None).await?;
@@ -30,9 +31,28 @@ println!("{}", reply.reply.unwrap_or_default());
31 ```
32
33 To hear about state changes, implement `StatusSink` and construct with
33 -`Ed::with_sink`. For Tauri applications, `onde-ed-tauri` provides that
34 +`Ed::with_sink`. For Tauri applications,
35 +[`ed-agent-tauri`](https://crates.io/crates/ed-agent-tauri) provides that
36 implementation plus the commands a webview invokes.
37
38 +## Any load path, same transitions
39 +
40 +`Ed::load` covers GGUF. For anything else — an assigned model from a server, a
41 +UQFF build, a load API that doesn't exist yet — hand the call to `load_with` and
42 +Ed reports the transitions around it:
43 +
44 +```rust
45 +ed.load_with("Onde model", |engine| {
46 + engine.load_assigned_model(env, app_id, secret, prompt, sampling)
47 +})
48 +.await?;
49 +```
50 +
51 +Ed takes a closure rather than wrapping each path by name on purpose. Those APIs
52 +take types that would drag their crates into Ed's public signature and pin every
53 +host to Ed's version of them — and at least one of those types has already moved
54 +between `onde` releases. A closure keeps Ed's dependency surface to `onde` alone.
55 +
56 ## Platforms
57
58 Ed has no `cfg(target_os)` gates. `onde` already splits real and fallback
crates/ed-agent/src/ed.rs renamed
+120 -23
@@ -1,5 +1,6 @@
1 //! The agent handle.
2
3 +use std::future::Future;
4 use std::time::Duration;
5
6 use log::{error, info};
@@ -62,48 +63,85 @@ impl<S: StatusSink> Ed<S> {
63 &self.engine
64 }
65
65 - /// Load a GGUF model, reporting each transition.
66 + /// Run any load, reporting the transitions around it.
67 ///
67 - /// Emits [`EngineStatus::Loading`] before starting and then either
68 + /// Emits [`EngineStatus::Loading`] before `load` starts and then either
69 /// [`Ready`](EngineStatus::Ready) or [`Error`](EngineStatus::Error). That
69 - /// sequence is the whole reason this method exists: calling
70 - /// [`ChatEngine::load_gguf_model`] directly leaves the UI with no way to
71 - /// know a multi-minute download has begun.
70 + /// sequence is the whole reason this method exists: calling a
71 + /// [`ChatEngine`] load path directly leaves the UI with no way to know a
72 + /// multi-minute download has begun.
73 ///
73 - /// Downloads the weights first if they aren't in the local cache, so the
74 - /// `Loading` state can last a long time on a cold start.
75 - pub async fn load(
76 - &self,
77 - config: GgufModelConfig,
78 - system_prompt: Option<String>,
79 - sampling: Option<SamplingConfig>,
80 - ) -> Result<Duration, InferenceError> {
81 - let model_name = config.display_name.clone();
74 + /// Ed takes a closure rather than wrapping each load path by name because
75 + /// `onde`'s load APIs take types Ed has no business in its public
76 + /// signature — `ChatEngine::load_assigned_model` takes an `Environment`
77 + /// from the smbCloud SDK, and *which* crate that type comes from changed
78 + /// between `onde` releases. Naming it here would drag that dependency into
79 + /// every host and pin them all to Ed's version of it. Passing a closure
80 + /// keeps Ed's dependency surface to `onde` alone and works for load paths
81 + /// that don't exist yet.
82 + ///
83 + /// ```no_run
84 + /// # use ed_agent::{Ed, GgufModelConfig};
85 + /// # async fn example(ed: Ed) {
86 + /// ed.load_with("Qwen 2.5", |engine| {
87 + /// engine.load_gguf_model(GgufModelConfig::qwen25_1_5b(), None, None)
88 + /// })
89 + /// .await
90 + /// .expect("load");
91 + /// # }
92 + /// ```
93 + ///
94 + /// `model_name` is what the host wants the UI to show while loading; it is
95 + /// reported with each transition and needn't match what the engine ends up
96 + /// calling the model.
97 + pub async fn load_with<'a, F, Fut>(
98 + &'a self,
99 + model_name: &str,
100 + load: F,
101 + ) -> Result<Duration, InferenceError>
102 + where
103 + F: FnOnce(&'a ChatEngine) -> Fut,
104 + Fut: Future<Output = Result<Duration, InferenceError>>,
105 + {
106 info!("ed: loading model {model_name}");
107 self.sink
84 - .status_changed(EngineStatus::Loading, Some(&model_name), None);
108 + .status_changed(EngineStatus::Loading, Some(model_name), None);
109
86 - match self
87 - .engine
88 - .load_gguf_model(config, system_prompt, sampling)
89 - .await
90 - {
110 + match load(&self.engine).await {
111 Ok(elapsed) => {
112 info!("ed: loaded {model_name} in {}", format_duration(elapsed));
113 self.sink
94 - .status_changed(EngineStatus::Ready, Some(&model_name), None);
114 + .status_changed(EngineStatus::Ready, Some(model_name), None);
115 Ok(elapsed)
116 }
117 Err(err) => {
118 error!("ed: failed to load {model_name}: {err}");
119 let message = err.to_string();
120 self.sink
101 - .status_changed(EngineStatus::Error, Some(&model_name), Some(&message));
121 + .status_changed(EngineStatus::Error, Some(model_name), Some(&message));
122 Err(err)
123 }
124 }
125 }
126
127 + /// Load a GGUF model, reporting each transition.
128 + ///
129 + /// A convenience over [`load_with`](Ed::load_with) for the most common
130 + /// path. Downloads the weights first if they aren't in the local cache, so
131 + /// the `Loading` state can last a long time on a cold start.
132 + pub async fn load(
133 + &self,
134 + config: GgufModelConfig,
135 + system_prompt: Option<String>,
136 + sampling: Option<SamplingConfig>,
137 + ) -> Result<Duration, InferenceError> {
138 + let model_name = config.display_name.clone();
139 + self.load_with(&model_name, |engine| {
140 + engine.load_gguf_model(config, system_prompt, sampling)
141 + })
142 + .await
143 + }
144 +
145 /// Unload the current model, returning its display name if one was loaded.
146 pub async fn unload(&self) -> Option<String> {
147 let unloaded = self.engine.unload_model().await;
@@ -170,6 +208,7 @@ mod tests {
208 #[derive(Default)]
209 struct RecordingSink {
210 statuses: Mutex<Vec<(EngineStatus, Option<String>)>>,
211 + errors: Mutex<Vec<Option<String>>>,
212 replies: Mutex<Vec<ChatReply>>,
213 }
214
@@ -177,14 +216,24 @@ mod tests {
216 fn statuses(&self) -> Vec<(EngineStatus, Option<String>)> {
217 self.statuses.lock().unwrap().clone()
218 }
219 +
220 + fn errors(&self) -> Vec<Option<String>> {
221 + self.errors.lock().unwrap().clone()
222 + }
223 }
224
225 impl StatusSink for &'static RecordingSink {
183 - fn status_changed(&self, status: EngineStatus, model_name: Option<&str>, _: Option<&str>) {
226 + fn status_changed(
227 + &self,
228 + status: EngineStatus,
229 + model_name: Option<&str>,
230 + error: Option<&str>,
231 + ) {
232 self.statuses
233 .lock()
234 .unwrap()
235 .push((status, model_name.map(str::to_owned)));
236 + self.errors.lock().unwrap().push(error.map(str::to_owned));
237 }
238
239 fn replied(&self, reply: &ChatReply) {
@@ -234,4 +283,52 @@ mod tests {
283 ed.unload().await;
284 assert_eq!(ed.info().await.status, EngineStatus::Unloaded);
285 }
286 +
287 + #[tokio::test]
288 + async fn a_successful_load_reports_loading_then_ready() {
289 + // The ordering is the feature: a UI that only ever sees the final
290 + // state has no way to put up a spinner for a load that can take
291 + // minutes.
292 + let sink = sink();
293 + let ed = Ed::with_sink(sink);
294 +
295 + let elapsed = ed
296 + .load_with("Test model", |_| async { Ok(Duration::from_secs(2)) })
297 + .await
298 + .expect("the closure succeeded");
299 +
300 + assert_eq!(elapsed, Duration::from_secs(2));
301 + assert_eq!(
302 + sink.statuses(),
303 + vec![
304 + (EngineStatus::Loading, Some("Test model".to_owned())),
305 + (EngineStatus::Ready, Some("Test model".to_owned())),
306 + ]
307 + );
308 + }
309 +
310 + #[tokio::test]
311 + async fn a_failed_load_reports_loading_then_error_with_the_reason() {
312 + let sink = sink();
313 + let ed = Ed::with_sink(sink);
314 +
315 + let err = ed
316 + .load_with("Test model", |_| async {
317 + Err(InferenceError::Other {
318 + reason: "no weights".to_owned(),
319 + })
320 + })
321 + .await
322 + .expect_err("the closure failed");
323 +
324 + assert!(err.to_string().contains("no weights"));
325 + assert_eq!(
326 + sink.statuses(),
327 + vec![
328 + (EngineStatus::Loading, Some("Test model".to_owned())),
329 + (EngineStatus::Error, Some("Test model".to_owned())),
330 + ]
331 + );
332 + assert_eq!(sink.errors(), vec![None, Some("no weights".to_owned())]);
333 + }
334 }
crates/ed-agent/src/lib.rs renamed
+2 -2
@@ -29,7 +29,7 @@
29 //! # Example
30 //!
31 //! ```no_run
32 -//! use onde_ed::{Ed, GgufModelConfig};
32 +//! use ed_agent::{Ed, GgufModelConfig};
33 //!
34 //! # async fn example() {
35 //! let ed = Ed::new();
@@ -51,7 +51,7 @@ pub use ed::Ed;
51 pub use reply::ChatReply;
52 pub use sink::{NoopSink, StatusSink};
53
54 -// Re-exported so a host depends on `onde-ed` alone and never has to match a
54 +// Re-exported so a host depends on `ed-agent` alone and never has to match a
55 // second `onde` version against this crate's.
56 pub use onde::inference::types::ChatMessage;
57 pub use onde::inference::{
crates/ed-agent/src/reply.rs renamed
crates/ed-agent/src/sink.rs renamed