Skip to content

0.3.0

Choose a tag to compare

@jwcarman jwcarman released this 03 Oct 00:54
· 289 commits to main since this release

Upgrading from 0.2.0: recreate the database (the schema changed and old data cannot be read), give JdbcNotebook, JdbcPlans and JdbcChapters a CodecFactory, replace any ActionRenderer with a Stringifier on ToolConfig.action, and add a Truncated arm to any switch over InferenceResult. Chapters are on by default: an agent gets a chapter every 20 turns, summarised by its own model, unless withoutChapters() is set. Each breaking change is listed below.

Breaking changes

  • A database written by an earlier build cannot be read and must be
    recreated.

  • Every piece of content Nessy stores goes through the storage codec.
    A chapter's summary (nessy_chapter.summary), a note's hook and body
    (nessy_note.hook, nessy_note.body) and a plan task's title
    (nessy_plan_task.title) were TEXT; they are now BYTEA, encoded with
    the application's CodecFactory, so a StorageCodecConfigurer's compression
    or encryption covers them as it covers the events and the payloads.
    JdbcNotebook and JdbcPlans take a CodecFactory as a third constructor
    argument, JdbcChapters takes one as its second, and InMemoryChapters
    takes one, so each holds the encoded bytes of a summary as the in-memory
    payload store holds content. The constructors without one are removed. A
    database written by an earlier build must be recreated.

  • A custom InferenceProvider, or a switch over InferenceResult, needs
    the new arm.
    A provider returns Truncated when its vendor says the
    output limit was reached and the reply holds text.

  • ToolConfig.action takes a Stringifier of the tool's input. A line is
    one line of at most 1,000 characters; with none named it is cut at 255,
    keeping its start.

  • Exchange, ActionRequest.ToolCall and ToolSucceeded each gained
    components:
    actions and results, action, and rendered.

  • ActionRenderer is removed. A Stringifier<I> takes its place.

  • nessy-approval-intent is removed, with Intent, Intents,
    JdbcIntents, IntentTool, IntentEnricher, IntentPolicy and the
    nessy_intent table.

  • Embedder.dimension() returns Optional<Dimension>, not int (0 for
    not learned yet): empty until the first reply when no width was asked for.
    Embedding.dimension(), a vector's length, stays int.

  • EmbeddingOptions.dimension is Optional<Dimension>, not OptionalInt.

  • InferenceOptions and EmbeddingOptions gain a properties
    component.
    Their existing constructors and of(...) still work;
    a record pattern over either (InferenceOptions(var model, var max))
    needs the third component.

  • The openai wire is now openai-chat. A custom provider with
    nessy.providers.<id>.wire: openai fails at startup; write openai-chat.
    Presets are unaffected, and the startup report now prints openai-chat.

  • OpenAiInferenceProvider is now OpenAiChatInferenceProvider, and
    OpenAiProviderConfig is now OpenAiChatProviderConfig.
    Same factories,
    same setters, same behaviour.

  • OpenAiRequests is no longer public. It was the chat adapter's
    internal projection and is now the package-private OpenAiChatRequests.

  • DefaultEmbedderFactory's three constructors are gone; build it with
    DefaultEmbedderFactory.of(f -> f.provider(id, provider).embedding(id, EmbeddingOptions.of(model))). Every embedder it mints is now an
    ObservedEmbedder.

  • An embedding provider holds nothing about a model. model(...) and
    dimension(...) are gone from OpenAiEmbedderConfig,
    GeminiEmbedderConfig, BedrockEmbedderConfig and VoyageEmbedderConfig,
    and defaultModel() / defaultDimension() from the four providers. The
    DEFAULT_MODEL constants stay, to cite: EmbeddingOptions.of( OpenAiEmbedderConfig.DEFAULT_MODEL).

  • An embedder that asked for a width and got another fails, naming both
    (asked for 256 coordinates, the model returned 768), on every call.

  • A Bedrock embedder over a model of neither family fails when it is
    made
    , not at its first call.

  • nessy.embedding.* is replaced by nessy.embedders.*, with no
    aliases:

    old new
    nessy.embedding.openai.model nessy.embedder: openai + nessy.embedding-model: <model>
    nessy.embedding.openai.dimension nessy.embedding-dimension: <n>
    openai.api-key + openai.base-url + nessy.embedding.openai.model a custom embedder (nessy.embedders.local.wire: openai, .base-url, .api-key, .vendor) plus the pair naming it; or the openai preset with openai.base-url, plus the pair
    nessy.embedding.gemini.model nessy.embedder: gemini + nessy.embedding-model: <model>
    nessy.embedding.gemini.dimension nessy.embedding-dimension: <n>
    nessy.embedding.voyage.api-key voyage.api-key (VOYAGE_API_KEY), or nessy.embedders.voyage.api-key
    nessy.embedding.voyage.model nessy.embedder: voyage + nessy.embedding-model: <model>
    nessy.embedding.voyage.dimension nessy.embedding-dimension: <n>
    a key alone, with the vendor's default model a key registers the provider; minting an embedder needs a model, from the store or the pair
  • VoyageEmbeddingAutoConfiguration, OpenAiEmbeddingAutoConfiguration
    and GeminiEmbeddingAutoConfiguration are replaced by
    EmbeddingProvidersAutoConfiguration
    ; update any
    spring.autoconfigure.exclude naming them. Several keys set now register
    several embedders instead of the first in auto-configuration order.

  • The starter's EmbedderFactory bean is always present. A store that
    decided "no embeddings" by the factory's absence now asks whether
    nessy.embedder is set. An application EmbedderFactory bean still
    replaces the starter's factory, and no longer switches the presets off.

  • Anthropic's typed setters are removed. AnthropicProviderConfig
    thinking(boolean), thinkingBudget(int) and promptCaching(PromptCaching)
    are gone, as is the PromptCaching enum; so are AnthropicRequests.Features
    and AnthropicRequests.toParams(InferenceRequest, Features, JsonMapper). Use
    property(AnthropicProperties.THINKING_TYPE, AnthropicThinkingType.ENABLED),
    THINKING_BUDGET and CACHE_TTL. Thinking and caching stay off unless a
    property turns them on.

  • nessy-memory-summarizing is removed, with HeadSummarizer,
    JdbcSummaries and the table nessy_summary. Chapters replace it, and are on
    by default. Existing nessy_summary rows are no longer read.

  • nessy-memory-episodic is removed, with EpisodeSummarizer,
    EpisodeTools, JdbcEpisodes, the tools begin_episode and
    recall_episode, and the table nessy_episode. Chapters replace it; to let
    the model draw the boundaries, use DeclaredChapters.feature(histories) and
    its begin_chapter. Existing nessy_episode rows are no longer read.

  • Summarizer is a different interface. The read-side Summarizer
    (forAgent, summarizedThrough) is gone; org.jwcarman.nessy.api.Summarizer
    now has one method, String summarize(Chapter), and writes a chapter's
    summary. A source that supplied recollections to a turn is a MemorySource.

  • summaries(...) is removed from HarnessConfig and ContextConfig. Use
    chapterPolicy(...) and summarizer(...) to decide what stands in for old
    history, or memory(...) to offer recalled text.

  • Block.SummaryContent is removed. A Summary is now
    Summary(Chapter chapter, String text).

  • InferenceContext has new components: (summaries, tail, memory, state, activeTurn, ambient). A provider adapter that reads its parts must place
    the new strata; see the providers guide.

  • Ambient background is no longer in the system prompt. An adapter written
    against the old context, or an application that relied on ambient text
    being part of the system field, sees a different request.

  • SystemPromptSource is removed, with systemPrompt(SystemPromptSource)
    on both doors and on ReplConfig. Use systemPrompt(String), adding
    sections with instructions(String); anything that varied per agent or per
    call becomes a StateSource or an AmbientSource.

  • PromptVariableSource is removed. Its factories are on PromptVariables
    (of, supplied, firstOf, none), which no longer takes an agent.
    EnvironmentVariables.of(...) returns a PromptVariables.

  • TemplatedSystemPrompt.of(...) is replaced by render(...), which
    renders once and returns a SystemPrompt.

  • The starter's SystemPromptSource bean is now a SystemPrompt bean,
    and PromptVariableSource beans are no longer consulted: declare
    PromptVariables beans.

  • A backend implementation must provide chapters() and leases() on
    DirectBackend and QueuedBackend.

Added

  • InferencePurpose, what a model call is for, with the supplied values
    ANSWER and SUMMARY. InferenceRequest gains purpose (ANSWER unless
    it says otherwise) and withPurpose, and ProseSummarizer sends SUMMARY.
    The nessy.inference.purpose key is on the model-call span and on the
    gen_ai.client.token.usage and gen_ai.client.operation.duration metrics,
    so spend and cache use can be split by purpose. It is never sent to the
    vendor.
  • InferenceResult.Truncated, a reply the vendor cut off at the output
    limit. It holds the blocks written before it stopped, at least one of them
    text. All five adapters return it (Anthropic max_tokens, OpenAI chat
    length, OpenAI Responses max_output_tokens, Gemini MAX_TOKENS,
    Bedrock max_tokens). An agent's turn delivers it as the answer, logs a
    WARN and reports length as the finish reason.
  • AnthropicThinkingType.BETWEEN_TOOLS (anthropic.thinking.type: between_tools), sent as the vendor names it. It is how thinking is turned
    off on a model that thinks with no thinking field and refuses disabled, as
    Sonnet 5.5 does: the model does not think before it responds.
  • InferenceRequest.oneOff, set with asOneOff(): a request that
    belongs to no series, so nothing it sends will be sent again. The Anthropic
    adapter sends no cache markers for one, whatever
    anthropic.cache_control.ttl says, and ProseSummarizer sends one-offs, so
    a chapter's summary no longer pays for a cache write nobody reads.
  • JsonSchemaGenerator.generate(Type), which keeps type arguments:
    List<Item> is described with typed items. The Class form stays.
  • Spring Boot: strict tools by default on the measured chat-wire presets.
    xai, groq, mistral, openrouter, nvidia and cerebras now send
    strict tools by default, overridable with
    nessy.providers.<id>.properties.openai.tools.strict: false.
  • OpenAiResponsesInferenceProvider, an adapter for OpenAI's Responses
    API, in nessy-inference-openai beside the Chat Completions one. It is
    stateless (store: false, the whole context on every call), keeps each
    encrypted reasoning item as a Block.Provider block, and sends function
    tools in strict mode, falling back per tool with a warning when a schema
    cannot be expressed strictly. Reasoning models such as GPT-6 call tools
    through it.
  • Spring Boot: a cerebras preset, lit by cerebras.api-key, at
    https://api.cerebras.ai/v1 on the openai-chat wire, sending strict tools
    by default (openai.tools.strict=true).
  • Spring Boot: the openai-responses wire. A custom provider, or the
    openai preset with nessy.providers.openai.wire: openai-responses,
    builds the Responses adapter.
  • Vendor properties. InferenceConfig.property(name, value) sets a
    vendor-prefixed setting on an agent type (openai.reasoning.effort,
    anthropic.thinking.budget_tokens,
    gemini.generationConfig.thinkingConfig.thinkingBudget,
    bedrock.inferenceConfig.temperature); each adapter supports the names it
    lists, parsed into the SDK's typed fields with a bad value refused when the
    harness is built. Any other name under the adapter's own prefix is ignored
    and logged at WARN, once, naming the property and the supported names;
    another adapter's prefix is logged at DEBUG. Every provider config and
    EmbedderConfig take properties too, though no embedding adapter supports
    one yet.
  • Typed vendor properties. VendorProperty<T> in nessy-api declares a
    property's name and type once (ofInteger, ofBoolean, ofFloat,
    ofEnum), and property(VendorProperty<T>, T) on
    InferenceConfig, EmbedderConfig and each provider config sets it in
    code, stored as the same text the by-name form carries. Each inference
    adapter publishes its constants (OpenAiProperties, AnthropicProperties, GeminiProperties,
    BedrockProperties), and every fixed value set is an enum:
    OpenAiReasoningEffort, OpenAiReasoningSummary, OpenAiServiceTier,
    AnthropicThinkingType, AnthropicCacheTtl, AnthropicServiceTier and
    GeminiThinkingLevel. A value outside an enum fails at build listing the
    spellings; openai.service_tier=ultrafast is carried by the Responses
    wire only, and the chat wire refuses it at build. Boot binding is
    unchanged: YAML values are still text.
  • InferenceProvider.validate(InferenceOptions) and
    EmbeddingProvider.validate(EmbeddingOptions), default no-ops, which
    the factories call when a harness or embedder is built.
  • VendorProperties in nessy-api, which adapters read properties
    through. The BOM now also lists nessy-inference-spi and
    nessy-embedding-spi.
  • openai.tools.strict on the chat adapter: function tools go out in
    strict mode over a rewritten schema.
  • Spring Boot: nessy.providers.<id>.properties.*.
  • Named embedders. DefaultEmbedderFactory.of(...) over
    EmbedderFactoryConfig: provider(ProviderId, EmbeddingProvider)
    registers an embedding provider by name, embedding(ProviderId, EmbeddingOptions) sets the default, observations(...) gives every
    embedder it mints its spans. EmbedderConfig.provider(...) names the
    provider a store wants; an unknown or missing one fails when the embedder
    is made, listing what is registered.
  • Spring Boot: nessy.embedders.<id>. Presets openai, gemini and
    voyage light from a vendor key when the vendor's embedding module is on
    the classpath; custom embedders state wire (openai, gemini,
    voyage), base-url and api-key; application EmbeddingProvider
    beans join under their bean names; nessy.embedder +
    nessy.embedding-model (+ optional nessy.embedding-dimension) name the
    default; EmbeddingReport logs what lit. VOYAGE_API_KEY lights the
    voyage preset.
  • OpenAiEmbedderConfig.vendor(String), for a custom openai-wire
    embedder that is somebody else.
  • Dimension, in org.jwcarman.nessy.api.embedding: how many
    coordinates a vector has, at least 1. EmbedderConfig.dimension(Dimension)
    takes it; dimension(int) remains as the convenience.
  • Chapters. History is cut into chapters when a turn ends, and a closed
    chapter is replaced in the context by a summary. ChapterPolicy says where
    a chapter ends (ends(OpenTurns), with ChapterPolicy.every(int)) and
    Summarizer writes the text that stands in for one
    (summarize(Chapter)); Chapter and Summary are in
    org.jwcarman.nessy.api.turn. Both run off the agent's thread, under a
    nessy.chapters lease, driven by the engine's ChapterKeeper. Closed
    chapters and their summaries are kept by the backend, as Chapters in
    nessy-backend-spi (reached as backend.chapters()) and, for JDBC, the
    table nessy_chapter, which Schemas.initialize creates. A chapter that is
    closed but not yet summarised is still shown as turns, and a failed summary
    is tried again at a later turn end.
  • Chapter settings. chapterPolicy(...) and summarizer(...) on the
    harness config; maxChapterLength(int) (default 30),
    chapterLeaseTtl(Duration) (default two minutes), maxTail(int) and
    withoutChapters() on the context config. A harness refuses to build when
    maxTail is not greater than maxChapterLength with chapters on.
  • ProseSummarizer, the default summariser: a prose record of the
    chapter, written by the agent's own provider and model.
  • DeclaredChapters, which lets the model say where chapters begin:
    DeclaredChapters.feature(histories) installs the tool begin_chapter and
    a policy that closes the chapter before any turn that called it.
  • The context has six strata, in a fixed order: instructions; history
    (summaries of closed chapters, then the tail of completed turns); memory;
    state; the active turn; ambient. InferenceContext is now
    (summaries, tail, memory, state, activeTurn, ambient).
  • MemorySource and StateSource, beside AmbientSource, with the
    records Memory and State and memory(...) and state(...) on the
    harness and context configs. Both are asked with
    forAgent(AgentId, Turn current), once per call, and nothing holds an
    earlier answer for them. Two sources of the same stratum may not offer the
    same kind.
  • instructions(String) on HarnessConfig, which adds a section after the
    system prompt, fixed when the harness is built.
  • PromptVariables.supplied(...), firstOf(...) and none(), and
    TemplatedSystemPrompt.render(...), which renders a template once.
  • Spans for chapters and sources: nessy.summary for each summary
    written, nessy.chapter.policy for each question put to the policy, and
    nessy.context memory <kind> and nessy.context state <kind> beside the
    ambient ones.
  • nessy-examples/chapter-lab, a command-line lab that replays a long
    recorded conversation under different chapter policies and summarisers.
  • Stringifier<T>, String stringify(T value), with dropTail,
    dropHead and dropMiddle to bound what it writes, truncated to bound it
    with a Truncator of your own, Stringifier.byToString() and
    Stringifier.json(mapper). Truncator cuts a string to a limit and
    supplies the three droppers.
  • ToolConfig.result, a Stringifier<ToolResult.Success> that says what a
    call returned, with ToolConfig.resultText() as the default, plus
    ToolConfig.LINE_CAP (1,000) and ToolConfig.DEFAULT_LINE_LIMIT (255).
  • Two lines stored for each tool call: ActionRequest.ToolCall.action,
    written when the model asks, and ToolSucceeded.rendered, written when the
    call succeeds. Each is at most 1,000 characters and is never worked out
    again.
  • Exchange.actionOf(CallId) and Exchange.resultOf(CallId), the two
    lines for a call in a turn's history.
  • chat-web: chat.summary-model (CHAT_SUMMARY_MODEL_ID) names a second
    model on the same provider to write chapter summaries. The local default
    model of chat-web, watchman and chat-cli is qwen/qwen3-coder-30b.

Changed

  • Listeners hear an agent's events after the step that wrote them commits.
    Both doors write an agent's events in short locked steps, which on a
    database are transactions, and used to tell listeners inside the step. What a
    step narrates is now held until its lock returns and delivered then, in the
    order the steps committed in for that agent; narration from outside any
    step, such as streamed deltas, keeps its place behind a step reserved
    before it. Another agent's narration is never held up. The chapter keeper
    no longer waits for the turn it was told about to become visible, because
    it already is.
  • A reply cut off at the output limit is Truncated. It was an Answer.
  • A tool call cut off at the output limit is a Fault. It was Actions,
    carrying arguments that could parse as {} and run.
  • Gemini's MALFORMED_FUNCTION_CALL is a Fault that says a tool call was
    malformed and was not run.
    It was the empty-answer fault. Gemini reports a
    tool call cut off at the output limit this way.
  • A chapter summary cut off at the output limit is refused. It used to be
    stored. The chapter stays unsummarised and is tried again when a later turn
    ends, so an agent type's maxTokens has to leave room for the summary.
    While a summary does not fit, that chapter and every chapter after it stay
    unsummarised, and once more than maxTail turns have completed since the
    last written summary, the oldest of them are sent neither as a summary nor
    in the tail.
  • The openai preset sends strict function tools
    (openai.tools.strict=true by default). Set
    nessy.providers.openai.properties.openai.tools.strict: "false" to turn
    it off, for instance when openai.base-url points at a server that
    rejects strict mode.
  • Anthropic enabled thinking with no budget sends 1024 tokens instead of
    refusing at build; the budget must still be below maxTokens.
  • Ambient no longer travels in the system prompt, on any provider. The
    system field carries the instructions alone. Memory and state are text at the
    head of the active turn's first message, tagged <memory kind="..."> and
    <state kind="...">, and ambient is text at the very end of the request. On
    Anthropic the cache markers are chosen before the ambient text is appended,
    so none sits on it. A consequence on Anthropic: with any ambient background
    present, the vendor drops replayed thinking blocks on later calls.
  • Chapters are on by default. An agent that sets nothing gets a chapter
    every 20 turns, summarised by its own provider and model, so it spends
    tokens on summaries unless withoutChapters() is set.
  • The default maxTail is 40 on both doors. It was 50 on the direct door
    and 20 on the queued one, so a queued harness now shows more. It counts
    completed turns; the turn being answered is sent besides.
  • The system prompt is fixed when a harness is built. It was resolved on
    every call. What varies by agent belongs in a StateSource, and what
    varies by the moment in an AmbientSource.
  • Spring Boot renders nessy.system-prompt once, at startup, from every
    PromptVariables bean and then the Environment, and publishes a
    SystemPrompt bean. A hole nothing fills fails startup.
  • A chapter is summarised from text. ProseSummarizer writes the chapter's
    turns out as lines, each call as assistant did: <action> -- succeeded: <result>, and sends them as one user message. Nothing changes for the turn
    being answered: the adapters still receive every call and result whole.
  • A failed call's message is at most 1,000 characters, its middle dropped
    and ... in the gap. That is the text the model reads back for the call.
  • A refused turn reads as one line in a chapter's transcript,
    (a message was withdrawn), and a line that runs onto more than one is
    indented four spaces after its first, so the summariser can tell where each
    line ends.

Fixed

  • A listener could be told about a step that then rolled back, or that failed
    to commit. A step that does not commit is now never heard, unless the harness
    was called inside an application's own transaction, which its steps then join.
  • A chapter that was due at a turn's end could be left open, or unsummarised,
    until a later turn when two turns ended close together.
    The keeper that
    found the agent's lease held walked away, and the keeper holding it had read
    the history before the later turn, or had already listed the chapters to
    summarise. The keeper that held the lease now reads the history again when it
    lets go and goes round again for anything that became due meanwhile, and
    re-reads the unsummarised chapters after each summary. Nothing waits on a
    lease.
  • A chapter whose turns called a tool could not be summarised on Anthropic.
    The summary request carried tool history and offered no tools, and the model
    answered nothing.
  • Anthropic: a changed prefix no longer rejects a request that replays
    thinking.
    With thinking on, the adapter now sends
    thinking.block_binding.prefix_mismatch_behavior: drop_block and the
    thinking-binding-controls-2026-08-01 beta header. Claude Fable 5.1,
    Opus 5.5 and Sonnet 5.5 bind a thinking block to everything before it, and
    accounts created since 2026-08-31 got a 400 when a request replayed
    thinking and the tail had moved or the background had changed. The
    request now asks the vendor to drop a thinking block whose prefix has
    changed, which is what makes a moved tail or changed background an answer
    and not a refusal. On older accounts, where such blocks used to reach the model
    unchanged, they are dropped too. The adapter logs how many blocks were
    dropped, at DEBUG. A request that does not think, a summariser's for
    instance, no longer replays thinking at all.
  • Anthropic: prompt caching no longer fails a turn that uses tools. With
    anthropic.cache_control.ttl set, a turn's third model call (two tool
    rounds in), or any request after a turn that used tools twice or that
    failed, threw ArrayIndexOutOfBoundsException while the request was being
    built. The conversation's two cache markers now sit on the last block of
    the request, which in a tool loop is the newest tool result, and on the last
    block of the user-side message before it, which is where the previous
    request ended.
  • Answers whose type is a list, enum, string or sealed type work on OpenAI
    and Azure.
    A schema whose root is not an object travels wrapped as
    {"value": ...} and is unwrapped before the caller sees it, and generic
    answer types keep their generics.
  • Episode ranking no longer fails on a stored summary of another width.
    A summary embedded under the same model name at a different width ranks
    last, like another model's, instead of failing the question.
  • A width of zero or less is refused where it is set. dimension(0) and
    nessy.embedding-dimension: 0 fail at once; the property's failure names it.
  • A late answer to a call could be recorded as the answer to a later call
    with the same id.
    A call id can repeat across two requests of one turn,
    and the Gemini adapter mints ids by position when the vendor sends none. A
    tool that finished after its deadline had been given up on, or an approval
    that came back late, was taken for the second request's call. An answer to
    a tool call or an approval now names the request it answers, and one for
    another request is ignored.

Documentation

  • Context, a concepts page for the six strata, chapters, and how to keep
    a conversation cacheable: what caching costs and saves, each Claude model's
    minimum cacheable size, and how to choose a chapter size, with measured runs.
  • 12-Factor Agents, a factor-by-factor check of Nessy against the
    12-Factor Agents principles.
  • Every example on the site compiles, and every command runs.

Known issues

  • The Spring Boot starter does not close inference clients at shutdown. Fixed for 0.4.0.
  • LM Studio returns an empty answer when a reasoning model is asked for a structured answer (lmstudio-bug-tracker #1773). Set openai.reasoning.effort: none on the provider; see the providers guide.