Skip to content

Releases: leonardopottmayer/tars

v0.0.19

Choose a tag to compare

0.0.19 (2026-10-26)

Bug Fixes

  • deps: bump dependencies within their major versions (#12) (36b4694)

tars v0.0.18

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 24 Oct 02:51

v0.0.18

A security hardening release. It applies the fixes from a security review of the framework, mostly in the Security/Identity family: optional rate limiting on the sign-in endpoints, atomic refresh token rotation, hashed magic link tokens, stricter JWT validation, open-redirect protection on returnUrl, a timing-safe hook for unknown users, and a context-bound ISecretProtector. The Telegram client stops leaking the bot token into logs and traces, and MailKit moves off versions with known vulnerabilities.

All packages are published at 0.0.18 (lockstep versioning). The packages that change are the Security packages (Identity.Abstractions, Identity, Identity.AspNetCore, DataProtection.Abstractions, DataProtection), Communication.Telegram, Communication.Email.MailKit, Observability.Abstractions, Observability.AspNetCore, Core.Primitives and Caching.Redis. Every other package is republished at 0.0.18 for version alignment.

Highlights

Identity

  • Optional rate limiting on the anonymous endpoints.
    IdentityEndpointsOptions.RateLimitingPolicy attaches a named ASP.NET Core rate limiting policy to password and API key sign-in, magic link request and consume, refresh and the OAuth challenge. Tars only attaches the policy; the application defines the limits with the native AddRateLimiter / UseRateLimiter. null (the default) applies none.

    builder.Services.AddRateLimiter(o => o.AddPolicy("identity-auth", http =>
        RateLimitPartition.GetFixedWindowLimiter(http.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            _ => new FixedWindowRateLimiterOptions { PermitLimit = 10, Window = TimeSpan.FromMinutes(1) })));
    // "Tars:Security:Identity:Endpoints:RateLimitingPolicy": "identity-auth"
  • Atomic refresh token consumption.
    With rotation on, two concurrent refreshes with the same token could both succeed. InMemoryRefreshTokenStore now removes with a compare-and-remove, and the IRefreshTokenStore.GetAndRemoveAsync contract requires persistent stores to be atomic too (e.g. DELETE ... RETURNING, Redis GETDEL).

  • Magic link tokens are stored hashed.
    The store now receives only the SHA-256 hash of each token, so reading the store (a database table) no longer lets anyone sign in. The link still carries the raw token.

  • Stricter JWT validation.

    • Validation accepts only HS256, the algorithm the issuer signs with, in both JwtTokenValidator and the ASP.NET Core JwtBearer setup.
    • A SigningKey shorter than 32 bytes (UTF-8) now stops the host at startup instead of failing at the first token issuance.
    • The options validation messages now name the rule that failed (SecurityIdentityOptions, IdentityAspNetCoreOptions, DataProtectionOptions).
  • returnUrl cannot be an open redirect.
    The magic link request and the OAuth challenge accept a local path (/dashboard, but not //host or /\host) or an absolute URL whose origin is listed in the new Endpoints:AllowedReturnUrlOrigins. Anything else returns 400.

  • Timing-safe hook for unknown users.
    PasswordAuthenticatorBase calls the new protected virtual VerifyPasswordForMissingUserAsync when no user matches. Override it to run your hasher against a fixed dummy hash, so an unknown username takes as long as a wrong password. The default does nothing, since the hasher belongs to the app.

  • Refresh cookie follows the configured lifetime.
    The refresh token cookie now expires with RefreshToken.Lifetime instead of a fixed 7 days. The new optional Cookie:RefreshTokenCookiePath scopes it to, say, /identity, so only the refresh and sign-out endpoints receive it. Sign-out deletes each cookie on the path it was written with.

  • Smaller fixes.

    • The OAuth challenge redirects to the configured Endpoints:OAuthCallbackPath instead of the default one.
    • A one-time startup warning appears when no ITokenRevocationService is registered (revoked tokens otherwise stay valid until they expire).
    • The docs now warn against reading API keys from the query string.

DataProtection: context-bound secrets

  • ISecretProtector gains Protect(plaintext, context) and Unprotect(ciphertext, context). In AesGcmSecretProtector the context (e.g. table, column and row id) is AES-GCM associated data, so a ciphertext copied into another record fails authentication instead of decrypting there. The one-argument methods use an empty context, which equals no associated data: values already stored keep decrypting.

Telegram: the bot token no longer leaks into logs and traces

  • The Bot API carries the bot token in the URL path. The Telegram HttpClient now logs requests with the token masked (POST https://api.telegram.org/bot***/sendMessage responded 200 ...) instead of the default request logging. The masked URL also rides on the request as TarsTelemetry.RedactedRequestUrl, which AddTarsHttpClientTracing writes to the span's url.full. Spans are kept, just masked.
  • On by default. Tars:Communication:Telegram:RedactBotToken: false turns it off.
  • A host that calls OpenTelemetry's AddHttpClientInstrumentation directly can apply the same enrich (see docs/communication/telegram.md).

Dependencies

Compatibility

  • Breaking for custom ISecretProtector implementations. They must implement the two new overloads with a context.
  • Magic links issued before the upgrade stop working once you upgrade, because the store now keys by hash. They are short-lived (15 minutes by default). Custom IMagicLinkTokenStore implementations need no code change: same signature, the parameter now carries the hash.
  • Custom persistent IRefreshTokenStore implementations should make GetAndRemoveAsync atomic.
  • Behaviour changes that can surface on upgrade:
    • a SigningKey under 32 bytes now fails at startup;
    • an absolute returnUrl now returns 400 unless its origin is in AllowedReturnUrlOrigins;
    • JWTs signed with anything but HS256 are rejected (Tars only issues HS256).
  • Recommended: override VerifyPasswordForMissingUserAsync in your PasswordAuthenticatorBase.
  • Telegram: request logs use new message templates under System.Net.Http.HttpClient.Tars.Communication.Telegram, and the package now depends on Observability.Abstractions (no dependencies of its own).
  • Recompile against 0.0.18. Some public constructors gained optional parameters (e.g. JwtTokenValidator).

CI

  • Both workflows declare permissions: contents: read, and pack.yml pins its actions to commit hashes.

Docs

  • endpoints.md (rate limiting, revocation warning), configuration.md (refresh cookie path, return URL origins, signing key minimum, API key query string risk), oauth.md, authentication-flows.md (dummy-hash override), overview.md, scenarios.md (atomic Redis store), telegram.md and the Communication and Observability configuration docs.

Tests

  • The unit suites are green at 543 tests. The Security suite went from 29 to 61, with new tests for each fix. Each one fails on the code before the fix.
  • The Telegram span test runs a real HttpClient call against a local Kestrel server, since .NET only creates the span on a real network call.

Full Changelog: v0.0.17...v0.0.18

tars v0.0.17

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 24 Oct 02:48

v0.0.17

A small, additive release in the Communication family: Telegram albums. An incoming message now says which album it belongs to, so a caller can carry the album's single caption across every file in it.

All packages are published at 0.0.17 (lockstep versioning). Only the Telegram packages change (Communication.Telegram.Abstractions, Communication.Telegram). Every other package is republished at 0.0.17 for version alignment.

Highlights

Media group id on incoming messages

  • Albums are recognisable.
    Several photos or documents sent together arrive from the Bot API as one message each, and only one of them (the first, from the official apps) carries the caption. TelegramIncomingMessage gains an optional MediaGroupId, mapped from the Bot API's media_group_id, so a caller can group those messages and apply the caption to every file.

    if (message.MediaGroupId is { } album)
        captions.TryAdd(album, message.Caption); // carry the album's caption to its other files

Compatibility

  • Source-compatible for normal use. Messages that are not part of an album carry MediaGroupId = null, and nothing else in the contract changes.
  • Recompile against 0.0.17. TelegramIncomingMessage is a positional record, so its constructor and Deconstruct gained a parameter. Binaries compiled against 0.0.16 must be rebuilt. Code that deconstructs it into variables needs one more (_).

Docs

  • docs/communication/telegram.md: the Media section explains how albums arrive and that only one message carries the caption.

Tests

  • New wire-mapper test: media_group_id is carried to TelegramIncomingMessage.MediaGroupId. The Communication unit suite is green at 126 tests.

Full Changelog: v0.0.16...v0.0.17

tars v0.0.16

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 22 Sep 23:51

v0.0.16

A small, additive release in the AI family: multimodal chat input. A user turn can now carry binary content (a voice note, a photo) alongside its text. Transcribing audio or reading an image becomes an ordinary chat completion: no new capability package, no separate transcription client.

All packages are published at 0.0.16 (lockstep versioning). Only the AI chat packages change (Ai.Chat.Abstractions, Ai.Chat.Gemini). Every other package is republished at 0.0.16 for version alignment.

Highlights

Multimodal input on the chat contract (ChatAttachment)

  • Binary content travels with the user turn.
    The new ChatAttachment(ReadOnlyMemory<byte> Data, string MimeType) in Pottmayer.Tars.Ai.Chat.Abstractions pairs the bytes with the MIME type the provider needs to read them (audio/ogg, image/jpeg, …). ChatMessage gains an optional Attachments list and a ChatMessage.User(content, params attachments) factory, so a caller builds a multimodal turn the same way it builds a text one.

  • Transcription is just a completion.
    Sending a voice note with a "transcribe this verbatim" instruction and no tools returns the transcript as completion.Message.Content. A caller that wants speech → action runs two calls: transcribe, then feed the transcript to its normal tool-calling pipeline. The spoken utterance is then logged and echoed as text, and everything downstream stays identical to typed input.

    var transcription = await client.CompleteAsync(new ChatRequest(
        "gemini-2.5-flash",
        [ChatMessage.User("Transcribe this voice note verbatim.", new ChatAttachment(audioBytes, "audio/ogg"))],
        Temperature: 0), ct);

Gemini maps attachments to inlineData

  • Each attachment becomes an inline part after the text.
    Pottmayer.Tars.Ai.Chat.Gemini sends every ChatAttachment as an inlineData part (MIME type + base64 data) following the turn's text part. Turns without attachments serialize exactly as before.
  • Inline means small. Gemini caps an inline request at 20 MB, so callers should bound what they attach. For example, Pandora's assistant refuses voice notes over 5 MB before calling the provider.

Compatibility

  • Source-compatible for normal use. Existing new ChatMessage(role, content, toolCalls) calls and the ChatMessage.System / ChatMessage.User(content) factories are unchanged. Providers ignore Attachments when it is null.
  • Recompile against 0.0.16. ChatMessage is a positional record, so its constructor and Deconstruct gained a fourth parameter. Binaries compiled against 0.0.15 must be rebuilt. Code that deconstructs a ChatMessage into three variables needs a fourth (_).

Docs

  • New "Multimodal input" section in docs/ai/overview.md, with the transcription example and the inline-size caveat. ChatAttachment is listed among the main contracts.

Tests

  • The Ai.Chat unit suite is green at 34 tests. A new wire-mapper test asserts that an attachment is sent as an inlineData part after the text, with the MIME type preserved and the bytes base64-roundtripped.
  • First consumer: Pandora's Assistant uses it for Telegram voice notes (download → transcribe → interpret), covered by that repo's unit tests. It has not yet been validated against the live Gemini API.

Full Changelog: v0.0.15...v0.0.16

tars v.0.0.15

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 19 Sep 14:04

v0.0.15

A new axis in the data family: a MongoDB document provider that implements the
same provider-agnostic contracts as the relational axis, so application and domain
code — repository interfaces, units of work, handlers — is identical whether a database
key is backed by PostgreSQL or MongoDB. To get there, the shared runtime was extracted
into a neutral base package that neither provider leaks into.

All packages are published at 0.0.15 (lockstep versioning). The data family changes
(new Data, Data.Document.Abstractions, Data.Document.MongoDB; reshaped
Data.Abstractions, Data.Relational.Abstractions, Data.Relational); every other
package is republished at 0.0.15 for version alignment.

Highlights

MongoDB document provider (same contracts, no IMongoRepository)

  • One repository contract for every backend.
    IStandardRepository<TEntity, TKey> is expressed entirely in BCL types
    (Expression<Func<T, bool>>, QueryParams), so MongoDB implements it exactly as EF
    Core does. A domain interface stays neutral (IProductRepository : IStandardRepository<Product, string>);
    switching a key from relational to MongoDB means changing which pipeline owns the key
    and which concrete repository is registered — never the domain interface. The
    relational-only surface (Queryable(), raw EF IQueryable) moved to a separate
    IRelationalRepository, opted into only when composable LINQ is truly needed.

  • A real unit of work with read-your-writes and atomic commit.
    MongoDataContext runs every read and write through a single IClientSessionHandle
    with an open transaction, started lazily on first use: reads see the unit's own
    uncommitted writes, CommitAsync flushes them atomically, and disposing without
    committing aborts. Domain events are collected explicitly (no change tracker) via
    Track() / CollectDomainEvents and dispatched inside the transaction, before
    commit
    , so an aggregate change and its integration event commit together or not at
    all. Transactions require a replica set (a single-node one is enough for dev).

  • Its own connection resolution, mirroring the relational precedence.
    A dedicated IMongoConnectionResolver reads Tars:Data:Mongo:* — per-tenant
    connections, tenant templates with {tenantKey}/{tenantCode} placeholders, then
    static/shared — resolving a database name alongside the connection string. As with
    the relational axis, the app talks to the resolver interface, so the config-backed
    reader is one swappable strategy (vault, tenant catalog, etc.).

Neutral base runtime (Pottmayer.Tars.Data)

  • A provider-clean shared layer.
    The runtime that both providers share — context accessor, composite/keyed context
    factory, unit of work, repository resolver, multi-database coordination, QueryParams
    → Expression translation — moved out of the relational package into a new
    Pottmayer.Tars.Data base that references only Data.Abstractions + Core.Ddd +
    DI (zero EF, zero MongoDB). This mirrors the Caching family's shape (Abstractions +
    base runtime + provider packages).

  • Any mix of providers, side by side, multitenant.
    A MongoDB key and a relational key resolve through the same composite factory, so one
    application can register N SQL + N Mongo keys behind a single IUnitOfWorkFactory, each
    multitenant, with IMultiDatabaseCoordinator coordinating a best-effort (Level 1)
    cross-provider commit.

Compatibility

  • Breaking for 0.0.14 relational-data consumers (this release is a data-family
    refactor, not purely additive):
    • AddTarsData<TDbContext> is renamed to AddTarsRelationalData<TDbContext>, and
      AddTarsRelationalUnitOfWorkFactory to AddTarsUnitOfWorkFactory, for
      provider/base naming consistency (provider prefix when the piece is provider-specific,
      no prefix in the neutral base).
    • IStandardRepository, IDataContextFactory, IKeyedDataContextFactory and the
      multi-database coordination contracts moved namespace into
      Pottmayer.Tars.Data.Abstractions; consumers update their using directives.
    • The relational-only Queryable() moved from IStandardRepository to
      IRelationalRepository; repositories that need raw IQueryable extend that
      interface.
    • Applications must now also add a reference to the new Pottmayer.Tars.Data base
      package (the shared infrastructure registrations live there).
  • Everything else is unchanged. Query semantics, connection-resolution precedence,
    multitenancy behavior and the coordinator are identical; the renames and moves are
    mechanical.

Tests

  • The Data unit suite is green at 30 tests, including new coverage for
    multi-provider coexistence (a composite factory routing relational and Mongo keys side
    by side), the context accessor, unit of work and the Mongo connection resolver.
  • The MongoDB provider was additionally validated end-to-end in tars-sandbox
    (Testcontainers) against a real MongoDB replica set + PostgreSQL: CRUD roundtrip,
    transactional rollback, read-your-writes, domain-event dispatch, dynamic
    filter/sort/paginate, database-per-tenant isolation, and a cross-provider
    coordinator
    committing MongoDB and SQL together — 7 integration tests green.

Full Changelog: v0.0.14...v0.0.15

tars v0.0.14

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 19 Sep 00:46

v0.0.14

Two additions to the messaging family, both behind the existing
IIntegrationEventBus seam so application and domain code are untouched: publishing
across several brokers in one process, and a transactional outbox that delivers
to a broker
— the piece that makes a transactional outbox on Kafka work.

All packages are published at 0.0.14 (lockstep versioning). The messaging family
changes (Messaging.Broker, Messaging.MassTransit, Messaging.MassTransit.RabbitMq,
Messaging.MassTransit.Kafka, Messaging.EntityFrameworkCore); every other package is
republished at 0.0.14 for version alignment.

Highlights

Multi-transport composite (N brokers, fan-out)

  • One process, several brokers, one seam.
    A CompositeIntegrationEventBus resolves a keyed transport per event from a
    declarative event→transport map, so producers keep calling PublishAsync and never
    learn a second transport exists. An event can fan out to many brokers at once;
    failures across the targets are gathered into a single AggregateException. The
    transport-agnostic core (EventTransportMap, IEventTransportSelector,
    CompositeIntegrationEventBus) lives in Messaging.Broker and has no MassTransit
    dependency.

  • The orchestrator references no concrete broker.
    AddTarsMassTransitComposite lives in the core Messaging.MassTransit package and
    composes whatever transports are contributed to it through the IMassTransitTransport
    seam — RabbitMQ as a full/primary (or multibus) transport, Kafka as a rider. Each
    transport's wiring (AddRabbitMq, AddRabbitMq<TBus>, AddKafka) ships in its own
    package, so an application references only the transports it uses — a Kafka + SQS
    app never drags in RabbitMQ. Adding a new transport (e.g. SQS) is one new package with
    an IMassTransitTransport contributor; the orchestrator does not change.

  • Multibus is first class.
    A second full transport (AddRabbitMq<TBus>(key, cfg)) becomes a MassTransit multibus
    keyed on its marker interface — closure-based, no reflection. One full transport with no
    marker is the primary; a second unmarked primary fails fast at startup with a pointer to
    the multibus overload.

Transactional outbox delivery to a broker ("outbox on Kafka")

  • The relay's last mile is now pluggable.
    The in-process outbox relay delivers through IOutboxRelayDelivery:
    LocalHandlerOutboxDelivery (the default — unchanged in-process dispatch to
    IIntegrationEventHandler<T>) or BrokerOutboxDelivery, which forwards each drained
    event to a keyed transport bus. AddTarsOutboxBrokerDelivery(transportKey) switches
    it on. The row is still written in the producer's transaction and drained the same way —
    only the destination moves from local handlers to the broker.

  • The one clean way to get a transactional outbox on Kafka.
    MassTransit's own bus outbox (UseBusOutbox) cannot cover Kafka, because the Kafka path
    publishes through ITopicProducer<T>, not the IPublishEndpoint the bus outbox
    intercepts. The relay sidesteps that: it stores at the seam and republishes through the
    keyed transport bus afterwards, at-least-once, with the relay's existing retry, backoff
    and dead-lettering.

  • Same table, or Debezium — your choice.
    Because the outbox table is a durable, transactionally-written log, an external CDC reader
    (Debezium) can drain it from the PostgreSQL WAL instead of the relay — no framework
    code on that path; the columns already fit Debezium's outbox EventRouter. The two are
    mutually exclusive per table (both would double-publish) and differ in wire format — the
    relay emits the MassTransit envelope (for your MassTransit consumers), Debezium the raw
    payload (for external/raw-JSON consumers) — so a natural split is each on its own topics.
    docs/messaging/outbox.md documents both.

Compatibility

  • Non-breaking for released 0.0.13 consumers. Both features are additive: the
    composite APIs, the keyed provider helpers (AddTarsKeyed{RabbitMq,Kafka}IntegrationEventBus)
    and AddTarsOutboxBrokerDelivery are new. Existing single-broker and in-process-outbox
    registrations behave exactly as before, with no code or configuration change.
  • One internal caveat: the outbox relay's public types OutboxRelayProcessor and
    OutboxRelayService gained an IOutboxRelayDelivery constructor parameter in place of the
    dispatcher. Applications that register the relay through AddTarsOutboxRelay are
    unaffected; only code that constructs the relay directly needs the new argument.
  • Broker delivery targets a keyed transport bus by design: AddTarsOutboxBus replaces
    the ambient (unkeyed) bus with the outbox writer, so the keyed bus is the real transport
    and there is no loop back into the outbox.

Tests

  • The Messaging unit suite is green at 147 tests, including new coverage for the
    composite (arbitrary keys, single/zero transports, second-primary rejection with a
    multibus hint, marked secondary as a keyed bus, a custom transport contributor composing
    as the primary — proving the drop-in seam) and for the broker delivery (BrokerOutboxDeliveryTests:
    forwards to the keyed bus; clear error when the key is unregistered).
  • The outbox-on-Kafka path was additionally validated end-to-end against a real Kafka
    broker
    (Testcontainers): the relay's broker delivery publishes a drained event to a real
    topic and a real consumer receives it.

Full Changelog: v0.0.13...v0.0.14

tars v0.0.13

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 14 Sep 22:28

v0.0.13

Makes the Gemini chat provider resilient to transient failures. When Gemini answers 503
("the model is overloaded") — or rate-limits with 429, or has any other bad server moment — the
call is now retried automatically with exponential backoff before the failure ever reaches the
caller, instead of surfacing as an error on the first hiccup.

All packages are published at 0.0.13 (lockstep versioning). Only Ai.Chat.Gemini changes; the
rest are republished at 0.0.13 for version alignment.

Highlights

  • Transient Gemini failures are retried, transparently.
    The Gemini typed HttpClient gets a retry handler (AddTarsAiChatGeminiHttpClient now chains a
    resilience handler) that retries the transient set — 503, 429, other 5xx, request timeouts
    and network errors — with exponential backoff and jitter. Retrying sits below the client:
    only the final response is classified into an AiException, so GeminiAiChatCompletionClient,
    its callers, and the existing tests are untouched.

  • The server's Retry-After wins.
    On 429/503 Gemini often tells you how long to wait; the handler honours that header
    (ShouldRetryAfterHeader) in preference to its own computed backoff, so rate limits are respected
    rather than hammered. The retry predicate reuses the framework default
    (HttpClientResiliencePredicates.IsTransient), which covers exactly the 408/429/5xx/timeout set
    the error classifier already treats as transient — one source of truth for "what is transient".

  • Configurable, and switchable off.
    GeminiAiOptions gains MaxRetryAttempts (default 3) and RetryBaseDelay (default 1s), bound
    from Tars:Ai:Chat:Gemini and validated in IsValid(). Setting MaxRetryAttempts to 0
    disables retrying entirely — the handler adds no strategy, so it never trips Polly's
    "attempts must be ≥ 1" validation.

  • Built on Microsoft's resilience stack.
    Uses Microsoft.Extensions.Http.Resilience (the Microsoft-published wrapper over Polly v8), not a
    hand-rolled loop.

Compatibility

  • Non-breaking for Ai.Chat.Gemini consumers. The new options carry sensible defaults, so
    existing registrations behave the same — now with retries — with no code or configuration change.
    Retry is transparent: nothing about the IAiChatCompletionClient contract changes.
  • One caveat: this project's Microsoft.Extensions.* references move from 10.0.2 to 10.0.11
    to satisfy the resilience package's transitive dependencies. This is a patch-level bump within the
    same 10.0.x line.
  • Every other package is unchanged; upgrading them from 0.0.12 is a version bump.

Tests

  • Five DI-level resilience tests exercise the handler through the real pipeline (only the primary
    HTTP handler is stubbed, counting attempts): a 503 retried then succeeding, a 429 retried,
    a permanent 400 not retried, giving up after the configured attempts, and
    MaxRetryAttempts = 0 disabling retries. The Gemini suite is green at 33 tests.

Full Changelog: v0.0.12...v0.0.13

tars v0.0.12

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 05 Sep 23:53

v0.0.12

A cross-platform correctness fix for options validation. No API changes.

All packages are published at 0.0.12 (lockstep versioning).

Fix

  • URL validation is now consistent across platforms.
    GeminiAiOptions.IsValid() and TelegramBotOptions.IsValid() accepted a relative, leading-slash
    path (e.g. /relative/only) as a valid base URL on Unix, because Uri.TryCreate(…, UriKind.Absolute, …)
    parses such a path as an absolute file URI there while rejecting it on Windows. Both now require
    the scheme to be http or https, so an invalid base URL is rejected the same way on every OS.
    This also fixes a CI test that passed on Windows but failed on Linux.

Compatibility

  • Purely a fix; no behavior change for any valid (http/https) configuration. Upgrading from 0.0.11
    is a version bump.

Tests

  • The Gemini and Telegram option suites assert that a non-http(s) base URL — empty, not-a-url, or a
    relative /relative/only path — is invalid on all platforms.

Full Changelog: v0.0.11...v0.0.12

tars v0.0.11

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 05 Sep 23:53

v0.0.11

Turns the Telegram client into a multi-bot factory: one host can now run several bots
(e.g. one for notifications, one as an assistant gateway) behind a single contract, each with its
own token and configuration. A reserved default bot keeps single-bot apps unchanged in spirit.

All packages are published at 0.0.11 (lockstep versioning). Only Communication.Telegram and its
Abstractions change; the rest are republished at 0.0.11 for version alignment.

Highlights

  • One contract, many bots, resolved by name.
    Callers depend on ITelegramClientFactory and resolve a bot with
    factory.GetClient("notifications"). Bot selection is data-driven, not keyed DI: each bot is an
    entry in TelegramOptions.Bots (a name → TelegramBotOptions map), and the factory looks it up
    — adding a bot is a line of configuration, not a code change. Names resolve case-insensitively,
    and an unconfigured name throws a permanent TelegramException rather than failing silently.

  • Per-bot configuration under one section.
    TelegramOptions becomes a container bound from Tars:Communication:Telegram, holding a Bots
    dictionary; each TelegramBotOptions carries its own BotToken, ApiBaseUrl, RequestTimeout
    and PollTimeoutGrace. IsValid() moves per-bot: an empty set of bots is valid, but any bot
    without a token — or with a non-http(s) base URL — is not.

  • One method per registration.
    The DI surface follows the framework convention: AddTarsTelegramOptions (binds the container,
    optional custom section and configure callback), AddTarsTelegramHttpClient (the shared
    HttpClient with the handler timeout disabled for long polling), and AddTarsTelegramClientFactory
    (the factory) — each does one thing, the same shape as Communication.Email.MailKit.

  • A reserved default bot for single-bot apps.
    ITelegramClientFactory.GetClient() with no name resolves the bot registered under
    ITelegramClientFactory.DefaultBotName ("default"), so an app that runs exactly one bot never
    has to name it. Calling GetClient() with no default configured throws a permanent error.

Compatibility

  • Breaking for Communication.Telegram consumers. The options shape changes from a single-bot
    object to a Bots container, and DI is now composed from AddTarsTelegramOptions +
    AddTarsTelegramHttpClient + AddTarsTelegramClientFactory, with ITelegramClientFactory as the
    resolution entry point. Migrating is a configuration move (wrap the old values under
    Bots:<name>) plus swapping the registration calls.
  • Every other package is unchanged; upgrading them from 0.0.10 is a version bump.

Tests

  • The Telegram registration/options suite is rewritten around the container/factory model: factory
    resolution (by name, case-insensitive, default bot, unconfigured-bot error), per-bot binding from
    configuration, the shared long-polling HttpClient, and per-bot validation including a non-http
    base URL.

Full Changelog: v0.0.10...v0.0.11

tars v0.0.10

Choose a tag to compare

@leonardopottmayer leonardopottmayer released this 04 Sep 10:40

v0.0.10

Adds an AI family: chat completion with tool calling behind one contract, with the
provider chosen per call, and a Gemini provider.

All packages are published at 0.0.10 (lockstep versioning). Only the new packages add
code; the rest are republished at 0.0.10 for version alignment.

Highlights

  • One chat-completion contract, provider chosen per call.
    Callers depend only on IAiChatCompletionClient, or resolve one through
    IAiChatCompletionClientFactory.GetClient("gemini"). Provider selection is keyed DI: each
    provider registers its client under a service key equal to its name, and the factory looks it
    up — adding a provider is a registration, not a change to callers. The Model and the
    ApiKey travel on each ChatRequest, not as app-wide defaults, so one instance serves
    whatever model a request names and a multi-user host can pass each user's own key per call
    (ChatRequest.ToString() masks it). Pass Temperature: 0 for deterministic output. The
    family is namespaced by capability (Ai.Chat.*), leaving room for Ai.Transcription.*
    and Ai.Embedding.* later without reorganizing; AddTarsAiClientFactory registers the
    factory separately, one method per concern.

  • Tool calling with the LLM as chooser, not executor.
    A request carries ToolDefinitions (name, description, and a raw JSON-Schema string for the
    arguments); the reply's meaningful output is ChatCompletion.ToolCalls — each a tool name
    plus its arguments as raw JSON — or prose when the model called nothing. The model never
    invokes anything itself: the caller validates ToolCall.Arguments against the schema and
    decides whether to execute. Failures surface as AiException with the one field callers act
    on, IsPermanent: an unknown model, a malformed request, a missing/invalid key or an invalid
    tool schema are permanent (a retry cannot help); an unreachable endpoint, a timeout or a
    malformed response are transient. AiException lives in Ai.Abstractions because it is
    shared across every capability, not just chat.

  • A Gemini provider that carries the key per request.
    GeminiAiChatCompletionClient calls v1beta/models/{model}:generateContent, sending the key
    in the x-goog-api-key header (never in the URL) — from ChatRequest.ApiKey when present,
    otherwise the configured GeminiAiOptions.ApiKey, and throwing a permanent AiException when
    neither exists rather than reaching the endpoint unusable. It maps the shared message/tool
    model to Gemini's wire shape (systemInstruction, functionDeclarations /
    functionCall / functionResponse) and classifies HTTP failures into permanent vs transient.
    Options bind from Tars:Ai:Chat:Gemini (AddTarsAiChatGeminiOptions, validated on start), and
    registration is AddTarsAiChatGeminiHttpClient + AddTarsAiChatCompletionClientGemini — one
    method per registration, the same shape as Communication.Email.MailKit or Caching.Redis.

Compatibility

  • Purely additive; no behavior or API changes to existing packages. Upgrading from 0.0.9 is a
    version bump.

New packages

  • Pottmayer.Tars.Ai.Abstractions
  • Pottmayer.Tars.Ai.Chat.Abstractions
  • Pottmayer.Tars.Ai.Chat
  • Pottmayer.Tars.Ai.Chat.Gemini

New dependencies

  • No third-party AI SDK: the Gemini provider talks to the Generative Language API directly over
    HttpClient + System.Text.Json.
  • The new packages add only Microsoft.Extensions.* primitives (DI, Hosting and Configuration
    abstractions, Http, and Options binding) for their DI helpers and typed HttpClient.

Tests

  • 28 unit tests: the wire mapper (tools, system → systemInstruction, function-call
    round-trips), the error classifier (permanent vs transient by status), the Gemini client via
    an HttpMessageHandler stub (including per-request key override), options binding and
    validation, and the keyed client factory resolution.

Full Changelog: v0.0.9...v0.0.10