Skip to content

feat: default value for logos chat client#151

Open
kaichaosun wants to merge 3 commits into
mainfrom
chat-client-config
Open

feat: default value for logos chat client#151
kaichaosun wants to merge 3 commits into
mainfrom
chat-client-config

Conversation

@kaichaosun

@kaichaosun kaichaosun commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Adds an opinionated LogosChatClient preconfigured with the Logos stack.

Changes:

  • ChatClientBuilder: collapsed the 8 defaulting build() impls into one. Two
    explicit constructors — new() (empty) and ephemeral() (random identity,
    ephemeral registry, in-memory storage, for tests/local).
  • LogosChatClient with open(transport, db_path, db_key, registry_url).
    db_path/db_key are caller-supplied;
    registry_url overrides the preconfigured endpoint (placeholder until deployed).
  • chat-cli builds via LogosChatClient::open, so it always uses the registry;
    --registry-url is now an endpoint override.
    Removed the dead run_logos_delivery.

Fix #126

Comment on lines -93 to 114
// All four explicitly provided.
/// `build()` exists only once every component has a concrete type. Any field
/// still `Unset` (always at least the transport, which has no default) fails the
/// bounds below, so an incomplete builder is a compile error rather than a
/// runtime one. Start from [`ephemeral`](ChatClientBuilder::ephemeral) to fill the
/// identity/registration/storage slots, then set the transport.
impl<I, T, R, S> ChatClientBuilder<I, T, R, S>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like what you are trying to do here, by deleting the other build() impls however this goes against the benefits of using the "builder" pattern.

[Boulder] Implementors must not be required to provide every component on construction. For maintainability Developers only choose the components that they care about, and the library provides sensible defaults for the rest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation actually fulfill this target as far as I can tell.
also checkout this comment #151 (comment) for the reason why I remove the many builders.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation actually fulfill this target as far as I can tell.

I was imprecise with my language, as I was assuming a requirement of no side effects.

Updated to: "Using a zero-side effect pathway like ::new() Implementors must not be required to provide every component on construction. "

pub fn ephemeral() -> ChatClientBuilder<DelegateSigner, Unset, EphemeralRegistry, ChatStorage> {
Self::new()
.ident(DelegateSigner::random())
.registration(EphemeralRegistry::new())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Pebble] EphemeralRegistry requires clients be within the same process to connect. For developers this will be confusing why they cannot connect. Developers expect a EphemeralClient to be fully functioning rather than a test impl

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chat-store is not fully deployed, once deployed, we can update it to be be http registry.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is then, using http registry would not be ephemeral, as the KeyPackages are persisted beyond restarts.

Ephemeral represents the current state well. However this is the primary entry point for developers. They do not want ephemeral services and messages. Public facing api structure need to be defined aspirationally, not based on the current feature sets.

Comment on lines +44 to +46
/// ephemeral (in-memory) registry, and in-memory storage. Intended for tests,
/// examples, and local runs that need no persistence or network services;
/// override any slot with the matching setter. A complete entry point on its

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Sand] If intended for tests then why expose it at the primary developer entry point? Consider moving to integration tests, or tests module

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to test modules sounds good, I kept it here in this PR to minimize the required changes. As I found it easier to progress with concentrate PRs.

/// own — only the transport is left to set before [`build`](ChatClientBuilder::build).
pub fn ephemeral() -> ChatClientBuilder<DelegateSigner, Unset, EphemeralRegistry, ChatStorage> {
Self::new()
.ident(DelegateSigner::random())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using named configurations i think is a smart idea.
However in this case it results in two undesirable paths for developers.

  1. builder::new() + provide every single field, so it will build.
    • This breaks all clients if a new service is added to the client.
    • Requires developers to specify implementations they don't care about, which becomes failure points in the future
  2. builder::<named_default>() + modifications.
    • Instantiates components which potentially have side effects, and are ultimately not used.

The goal here is Developer choice + sensible defaults.

I would leave ChatClientBuilder as is and focus on providing named defaults via LogosChatClient

@kaichaosun kaichaosun Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builder::new() + provide every single field, so it will build.

I think the defaults() and override is the way to go in most scenarios. And leaving new() as a fully controlled way to initiate the client with the "pain" to update along with library.

Instantiates components which potentially have side effects, and are ultimately not used.

Good point, and I don't have a good solution yet, maybe we should defer the side effects. I also don't see it's a blocker as we are mostly using memory storage etc.

The refactor here is to solve the bloated builder methods, as it will be growing even worse when there are new dependency added. Macros is another way to try, but I'd like to try this solution first.

@jazzz jazzz Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, and I don't have a good solution yet, maybe we should defer the side effects.

That is the intention of the initial design. Initializing services will produce effects (registration, state initialization, logging, etc. ) As the services improve and become more production ready, the effects will increase. This interface is entirely focused on developers, so services like in memory storage will be replaced ASAP.

The refactor here is to solve the bloated builder methods, as it will be growing even worse when there are new dependency added.

I don't see "bloated builder methods" as an issue that requires fixing at this moment. The code is relatively straight forward, and symmetric. Zero side-effect, compile-time type enforcement is worth it at this moment

As I found it easier to progress with concentrate PRs.
Lets let it become an issue before attempting to resolve it

Comment on lines +42 to +43
/// `db_path` is a per-client location and `db_key` is a secret, so both are
/// caller-supplied — never baked into the library.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Sand] this will be awkward for a while as all convos are missing persistence support, for the next month.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I suppose it should be fine as not many code depends on this path yet.

transport: T,
db_path: impl Into<String>,
db_key: impl Into<String>,
registry_url: Option<&str>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Sand] Consider removing registry_url.

For clients to interop developers need to be using the same registry service. Providing configuration here means that clients will be fragmented.

In general we want to be very opinionated in our "opinionated" implementations - such that two developers using LogosChatClient can always communicate. Same Delivery, same registration, same account.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added here to facilitate locally running apps so before we have a production ready registry, it can be override for such apps.
It will also be useful if we have logos-delivery broadcast capability for publish records in registry implement, so multiple instance can be supported.

/// `db_path` is a per-client location and `db_key` is a secret, so both are
/// caller-supplied — never baked into the library.
pub fn open(
transport: T,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Pebble] This should be opinionated about the transport being used. Developers should not have to choose this parameter, it should be configured for them if they choose the LogosChatClient

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the current logos delivery is not in the proper place, and hard to instantiate it in logos.rs, needs another PR to first move logos delivery transport in extentions.

Comment on lines -9 to 12
let (mut saro, saro_events) = ChatClientBuilder::new()
let (mut saro, saro_events) = ChatClientBuilder::ephemeral()
.transport(InProcessDelivery::new(bus.clone()))
.registration(reg.clone())
.build()

@jazzz jazzz Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Dust] As a developer; Using Ephemeral to then potentially define .storage(sql) seems more confusing than building the client the way I want via ChatClientBuilder::new()

eg.

ChatClientBuilder::new().transport(t).storage(sql)
vs
ChatClientBuilder::ephemeral().transport(t).storage(sql)

Is it ephemeral or does it use, sql peristence?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantic looks a bit different,
ChatClientBuilder::ephemeral().transport(t).storage(sql) means you decide to go with default first and override later,
ChatClientBuilder::new().transport(t).storage(sql).xyz() are also supported, but devs needs to provide all the required ones.

Maybe we can rename ephemeral to defaults later when we have registry and persistence ready. I explicitly name is ephemeral to reflect its current usage and internal state.

@kaichaosun kaichaosun requested review from jazzz and osmaczko June 26, 2026 03:02

@jazzz jazzz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My previous concerns still remain.

  • Blocker: Changing ChatClientBuilder to require an ephemeral base configuration has negative impacts on code maintenance and will be hard to undo in the future.
    • This change does not seem necessary at this time, as it does not add any required functionality while removing beneficial properties.
    • The motivation of removing a few lines of code doesn't justify the increased maintenance costs in developer applications.
  • Forcing Ephemeral be the root of the builder pathway introduces fragmentation, which will become developer pain.
    • The focus should be on creating an API that we want in the future( rather that what is available now), and working towards it with little to no breaking code changes in demos and developer applications.

@@ -0,0 +1,61 @@
//! The opinionated Logos client.
//!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having LogosChatClient located within the "Generic client" seems like it will be full of friction as the objects have separate concerns.

the "GenericClient will tend to be more generic/abstract" and the LogosChatClient will tend towards importing specific implementations and services.

[Sand] LogosChatClient would benefit from being a separate crate, to keep imports separated. That way it can import LogosCore specific implementations and GenericClient could remain free of these implementation details

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create a LogosChatClient, which is default prefconfigured with Logos Specific services

2 participants