Skip to content

Releases: jwcarman/nessy

0.7.0

Choose a tag to compare

@jwcarman jwcarman released this 07 Oct 14:55

Breaking and upgrade notes

Read these before upgrading from 0.6.0.

  • nessy_agent_turn has a new label VARCHAR(1000) NOT NULL column. The schema never alters
    an existing table, so a database created by 0.6.0 must drop and recreate nessy_agent_turn (or
    be recreated) before 0.7.0 starts; otherwise every turn's ending fails to commit. An application
    with nessy.initialize-schema=false must add the column itself.
  • AgentTurn has a new label component after trajectoryJson. A custom AgentTurns
    implementation must store it and return it.
  • A label is a category. inputLabel names the kind of work an input starts, from a small set
    of values, and is stored plain, unencrypted. An application whose label carries input content
    must change it before upgrading: from 0.7.0 the label is written in plain text to
    nessy_agent_turn. A label containing NUL, or an unpaired surrogate, is stored with U+FFFD in that
    character's place.

Added

  • The task label on every turn row. The label an application gives an input is stored in
    nessy_agent_turn.label, so trajectories group by the kind of work.

0.6.0

Choose a tag to compare

@jwcarman jwcarman released this 07 Oct 12:49

Breaking and upgrade notes

  • DirectBackend and QueuedBackend have a new method, turns(), which returns the
    AgentTurns store. An application with its own backend implementation must add it; the JDBC and
    in-memory backends already do.
  • The database must use UTF8 encoding (the PostgreSQL default). nessy_agent_turn.trajectory
    stores tool names as text, so a name outside the database's character set would stop a turn's
    ending from committing.
  • An application with nessy.initialize-schema=false must create nessy_agent_turn, the
    table of completed turns, with the rest of its schema; the DDL is in nessy-schema.sql in the
    JDBC backend jar.
  • An application with nessy.initialize-schema=false must create nessy_note and
    nessy_plan_task
    , the notebook's and the plan's tables, with the rest of its schema; the DDL is
    nessy-schema.sql in the notebook jar and in the planning jar.
  • The notebook index is present when it is empty, and the remember tool's description says
    what a note is for.

Added

  • Trajectory fingerprints. Every completed turn gets a deterministic, versioned digest of its
    behavior: which tools ran in which rounds, what each came to, and how the turn ended, with
    inputs, arguments, results, timing and cost left out. It is computed in the fold and written to
    the new nessy_agent_turn table in the same transaction as the turn-ending event, with the
    turn's round, tool and inference counts beside it; the same values are tagged on the turn's span.
    TurnOutcome and Trajectory are the new public types; AgentTurns is the new backend store.
    See docs/concepts/trajectories.md.
  • The readable trajectory. Each nessy_agent_turn row now carries its trajectory as JSONB
    beside the hash: the rounds of tool calls with their outcomes, then how the turn ended. A query
    reads behavior directly, and @> finds turns by what they did.
  • The notebook installs itself. With nessy-memory-notebook and nessy-backend-jdbc on a Spring Boot application's classpath,
    every agent of both doors gets the notebook index and remember, revise, recall and
    forget. nessy.notebook.enabled=false turns it off, and an application's own Notebook bean
    keeps the starter's out.
  • The plan store installs itself. With nessy-planning and nessy-backend-jdbc on a Spring Boot application's classpath,
    every agent of both doors gets the ambient current plan and update_plan.
    nessy.plan.enabled=false turns it off, and an application's own Plans bean keeps the
    starter's out.
  • QueuedHarnessFactoryConfig.feature(...) adds a feature to the queued factory, as the direct
    factory's configuration already did.
  • Both harness auto-configurations collect every Customizer<HarnessConfig<?>> bean and apply
    it as a feature to each harness they create.
  • NotebookTools.feature(Notebook) installs the index and the four tools in one call.
  • PlanTools.feature(Plans) installs the ambient plan and update_plan in one call.

0.5.0

Choose a tag to compare

@jwcarman jwcarman released this 06 Oct 04:28

Breaking changes

  • Outcome is AskOutcome. DirectHarness.ask returns AskOutcome<O>, with the same arms
    Answered, Refused, Failed and Busy, and a fifth, Terminated. Asking a terminated agent
    returns AskOutcome.Terminated, not Refused with the category terminated: no turn runs,
    nothing is appended, and the outcome carries no TurnStats. A switch over the outcome must
    handle Terminated.
  • QueuedHarness.tell returns a TellOutcome. Accepted means the agent took the input: it
    was handed to the backlog policy, whatever the policy then did with it, and a turn started at
    once when the agent was idle and the policy left something waiting. Terminated means the agent has been terminated, including one whose last turn is still in
    progress: the input was dropped, nothing was stored and the dispatcher was not nudged. It used to
    return nothing and drop the input without a word. An implementation of QueuedHarness must
    return one.
  • A terminated agent is called terminated. TerminationOutcome.Ended is
    TerminationOutcome.Terminated and TerminationOutcome.AlreadyEnded is
    TerminationOutcome.AlreadyTerminated. Code that switches on the outcome must use the new
    names.
  • The reply token is removed. ReplyToken, ApprovalRequest.replyToken() and its record
    component, ToolCallRequest.replyToken(), the engine's ReplyTokens,
    QueuedHarnessFactoryConfig.replyTokens(...) and the nessy.reply-token-encryption-keys
    property are gone. ApprovalRequest's constructors lose the replyToken argument. Nessy does
    not check who is answering: an application guards the endpoint that calls Replies.
  • A late answer is addressed by agent type, agent id and the call's key. Replies.approve and
    Replies.complete take an AgentType, an AgentId and an IdempotencyKey, all required,
    where they took a ReplyToken. ReplyOutcome is Applied or Ignored: Applied means the
    answer changed the agent's state, and Ignored means it did not, whatever the reason. Settled,
    NotAwaiting and Unreadable are gone. An approval request and a tool call request already
    carry the three values; a desk that stored a token stores them instead.
  • An approval's facts are stored on its events. ToolApproved, ToolDenied and ToolFailed
    gain a facts field, a JSON object, before idempotencyKey, and the stored
    failure response on an effect row gains it too. Stored events and failure responses change
    shape; recreate the database. A custom StoryContent must implement approvalFacts.
  • Stored events and stored effect attempts changed shape. The five model-call events gain a
    manifest field, and a failed attempt kept on an effect row gains manifest. Recreate the
    database.
  • nessy_agent_effect gained a nullable parked_at column, and Effects gains
    park(effectId, attemptsMade, at), so a custom implementation must add it. Recreate the
    database.
  • A custom Effects must add liveFor and parkedNow. Effects.liveFor(type, agent) lists
    one agent's live effect rows and Effects.parkedNow(type, now, after, limit) pages the rows that
    are waiting on an answer at now, both as the new LiveEffect. A row that is parked, claimed
    and not yet at its deadline is the one LiveEffect.parkedNow calls waiting.
  • A custom QueuedBackend must add queued(type, agent), the number of inputs waiting in the
    agent's queue.
  • A custom QueuedHarnessFactory and DirectHarnessFactory must add work(), which returns
    the factory's AgentWork.
  • A model response that repeats a call id is a failed inference. Two tool calls in one
    response with the same call id make the response invalid. The turn fails as it does for any
    failed inference, with a permanent failure that names the repeated id, and the response's usage
    is on the record. Nothing of the response is stored, narrated or requested, and no approver is
    asked and no tool runs. The engine logs one error naming the agent, the turn and the id.
  • nessy_payload gained a kind column. A payload holds message blocks or a JSON document,
    and kind says which: BLOCKS or DOCUMENT. Payloads gains two abstract methods, so a
    custom implementation must add them. Recreate the database.
  • Narration is reshaped into the agent's story. Narration has two groups, Story (stored
    events) and Live (signals heard only as they happen). TurnEnded is removed: a turn ends in
    exactly one TurnEnding event (Answered, TurnRefused, TurnFailed or TurnStopped), and
    NarrationListenerConfig.onTurnEnding hears all four. A turn a policy stopped is TurnStopped,
    no longer TurnFailed. Answered, TurnRefused, TurnFailed and ActionsRequested carry
    their turn and the model call's Usage; TurnFailed carries a FailureKind; each requested
    call carries its IdempotencyKey. A retried model call is told as InferenceRetried.
  • A deferral is a stored event and a story event. AgentEvent gains ApprovalDeferred(seq, turn, callId, until, facts, idempotencyKey) and ToolDeferred(seq, turn, callId, until, idempotencyKey), stored as approval-deferred and tool-deferred; recreate the database. The
    narrations ApprovalDeferred and CallDeferred are Story events, told from those stored
    events, and have the shape (callId, idempotencyKey, until): they carry no action or
    toolName, and a watcher joins by key to the ActionsRequested.Call that has them.
  • Every call event carries the call's IdempotencyKey. ToolApproved, ToolDenied,
    ToolSucceeded and ToolFailed gain a trailing idempotencyKey, the key the call was
    requested with; the stored shape changes, so recreate the database. The four narration records
    CallApproved, CallDenied, CallFinished and CallFailed gain idempotencyKey after
    callId.
  • A failed call says why it failed. AgentEvent.ToolFailed, EffectOutcome.ToolFailed,
    AgentCommand.ToolOutcome.Failed and Narration.CallFailed gain a CallFailure kind before
    message: FAILED, PAST_DEADLINE or NOT_AUTHORISED. The stored tool-failed event has a
    kind field; recreate the database. PAST_DEADLINE is what a tool call that does not finish in
    time reads on both doors: the queued door's expiry, and the direct door cutting a running call
    off.
  • An answer says when it was cut off. AgentEvent.InferenceAnswered,
    EffectOutcome.InferenceAnswered, AgentCommand.InferenceOutcome.Answered and
    Narration.Answered gain a boolean truncated, true when the model was cut off at its output
    limit. The stored inference-answered event has a truncated field; recreate the database. The
    caller of ask receives a cut-off reply exactly as before.
  • An approval records who decided, not a reference. ApprovalResult.reference() is
    decidedBy(), and approvedBy and deniedBy take a decidedBy: who or what decided, as the
    application says it. Nessy never interprets it. The same rename runs through EffectOutcome.ToolApproved
    and ToolDenied, AgentCommand.ApprovalOutcome.Approved and Denied, and the stored
    ToolApproved and ToolDenied events, whose JSON field is now decidedBy; recreate the
    database. The narration records CallApproved and CallDenied gain a trailing decidedBy. The
    argument to approvedBy and deniedBy used to be a reference and is now who decided: code that
    passed a ticket or decision id still compiles, and now records that id as the decider.
  • NarrationListener.on takes a Narrated envelope: the agent, the event, and for a story
    event its position (seq and time written). NarrationListenerConfig.Handler is
    on(Narrated narrated, E event), and Narrator.narrate takes a Narrated. A story event's
    time is the engine's clock, passed to the store: AgentEvents.append takes a trailing
    Instant at, which a store writes as given rather than reading a clock of its own, so an event
    heard live and read back later carry the same instant. InMemoryAgentEvents no longer takes a
    Clock. AgentNarrator.narrate takes only a Narration.Live signal.
  • The JSON and SSE names of the turn endings changed. turn-ended is gone; turn-stopped and
    inference-retried are new. A browser client that listened for turn-ended must listen for the
    four endings: answered, turn-refused, turn-failed and turn-stopped.
  • The stored event for a turn a policy stopped is turn-stopped; it was turn-failed. It is
    AgentEvent.TurnStopped, no longer AgentEvent.TurnFailed. Recreate the database.
  • NarrationListenerConfig.on(Class, handler) hears every member of a group. Given a group
    type (Narration.TurnEnding, Narration.Story, Narration.Live or Narration), it now hears
    each event of that group, not only an event of exactly that class.
  • A stored event's written_at is the engine's clock reading for its step, to the microsecond,
    not the database's now().
  • A turn's start says what started it and when its input arrived. AgentEvent.TurnStarted
    gains label and arrivedAt before startedAt; AgentCommand.StartTurn gains label and
    arrivedAt before at; Narration.TurnStarted gains label and arrivedAt. The stored
    turn-started event has label and arrivedAt fields; recreate the database. On the queued
    door arrivedAt is when tell was called, and on the direct door when ask read its clock.

Added

  • AgentWork says what an agent is doing and what waits on a person. status(type, id)
    returns an AgentStatus: an Activity (IDLE, WORKING, WAITING or TERMINATED), the number
    queued, the current turn, the approvals waiting, and the number of waiting tool calls.
    waitingApprovals() and waitingApprovals(type) list the approvals parked on people, each as
    the ApprovalRequest its approver...
Read more

0.4.0

Choose a tag to compare

@jwcarman jwcarman released this 03 Oct 23:20

Breaking changes

  • ApprovalRequest.callKey() is replaced by idempotencyKey(), a typed
    IdempotencyKey wrapping a UUID. callKey() was turn/callId: unique only
    within one agent, and not even there when a model repeats a call id within
    a turn. The new key is made once per call when the model's request is
    recorded, stays the same across retries and restarts, and is unique across
    every agent. ToolCallRequest.idempotencyKey() carries the same key, so a
    tool can deduplicate on it and find its own approval. Stored events and
    effects change shape; recreate the database.
  • DirectHarness.ask refuses to run inside a caller's transaction. It
    throws IllegalStateException before writing anything. A turn makes a model
    call, and a transaction should not stay open across a network call; on JDBC
    the call failed anyway, with "no event at 1 for agent ...". Suspend the
    transaction for the call (PROPAGATION_NOT_SUPPORTED). The queued door's
    tell still joins the caller's transaction. nessy-engine now depends on
    spring-tx for this check, as it already does on spring-context.
  • Anthropic prompt caching is on by default. With
    anthropic.cache_control.ttl absent, a request now carries FIVE_MINUTES
    cache markers; before, it carried none. A one-off request still carries
    none, and neither does a request that asks for an answer
    (ToolChoice.Answer), which is sent without its tools.
  • Narration.ActionsRequested carries one Call per requested call
    (callId, toolName, action) in place of tool names alone, so a watcher
    can join each later call event, which names only the id, to its tool.
  • Turn has no tokens field. It was always 0 and nothing read it.
    Usage is reported as a whole, through UsageReports.

Added

  • AnthropicCacheTtl.DISABLED turns Anthropic prompt caching off. Set it on
    the provider, or on one agent type to override a provider that caches.
  • UsageReports reads an agent's usage over its whole history, by model:
    of(type, id) returns one ModelUsage per model, never added across
    models. It is projected from the agent's stored events, so it counts every
    inference, retries and failures included, and is fit for cost accounting,
    which narration is not. The starter offers a UsageReports bean.

Changed

  • The OPA adapter and the policy example are tested against OPA 1.21.1. Every
    response shape the adapter relies on is the same as on 0.68.0.

Fixed

  • The watchman example no longer hides a repeated call id. Its approvals
    board was keyed on the model's call id, so once an agent's call_1 was
    answered, a later call_1 from the same agent was never shown and timed
    out. The board is now keyed on the call's IdempotencyKey, and its
    approve and deny URLs carry that key. Drop watchman_pending_approval (or
    recreate the watchman's database) before running it.
  • Gemini no longer returns half a reply as the answer. A stream that
    stopped before its finish reason was read as a complete answer. It is now a
    fault.
  • A stream cut short is Failure.Unknown, not Permanent, on Anthropic,
    OpenAI Chat, OpenAI Responses, Bedrock and Gemini. A server that closes the
    stream mid-answer is a dropped connection, so a retry policy can now try
    again. A stream that delivers no answer at all is still Permanent.
  • A dropped connection to the model can be retried. Every adapter
    reports a dropped connection as Failure.Unknown, and the dispatcher sent
    only Failure.Transient to the retry policy, so a dropped connection ended
    the turn whatever the policy said. An Unknown model failure now reaches
    the policy too: a model call that runs twice changes nothing but the bill.
    The default policy is still Never; set one with
    InferenceConfig.retryPolicy for the queued door to try again.
  • The Boot starter closes every inference provider's client at shutdown.
    Each provider bean is an ObservedInferenceProvider, which was not
    AutoCloseable, so the container never closed the adapter inside it and
    the adapter's HTTP client leaked. The wrapper now closes what it wraps, as
    ObservedEmbedder already did.
  • A generated answer type works on Anthropic. Anthropic refuses an answer
    schema with an object that does not say "additionalProperties": false,
    and the schema generator writes none, so every generated answer type failed
    with a 400. The adapter now closes every object in the answer schema, and
    leaves one that already says what it allows as written.
  • The queued dispatcher can no longer stop for good. A claim that returned
    more rows than its batch made the dispatcher release a negative count of
    permits; the semaphore threw and the agent type never dispatched again. The
    dispatcher now runs at most its batch and puts any surplus straight back on
    the queue, and the JDBC claim picks and locks its rows in a MATERIALIZED
    CTE, evaluated exactly once.

Known issues

  • 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.

0.3.0

Choose a tag to compare

@jwcarman jwcarman released this 03 Oct 00:54

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
    Sonne...
Read more

0.2.0

Choose a tag to compare

@jwcarman jwcarman released this 29 Sep 23:53

Breaking changes

  • InferenceProvider.providerName() is now vendor(), and so are
    Embedder.providerName() and EmbeddingProvider.providerName(). The value
    is unchanged: the OpenTelemetry gen_ai.provider.name (openai, x_ai,
    gcp.gemini, ...), which two providers can share. OpenAiProviderConfig.provider(String)
    is now vendor(String). Traces are byte-identical.
  • A harness factory holds its providers by name.
    DirectHarnessFactoryConfig.provider(InferenceProvider) and
    QueuedHarnessFactoryConfig.inference(InferenceProvider, InferenceOptions)
    are replaced by a repeatable provider(ProviderId, InferenceProvider) on
    both configs, plus inference(ProviderId, InferenceOptions) for the
    factory's defaults. Migrate
    f.provider(p) to f.provider(ProviderId.of("openai"), p).inference(ProviderId.of("openai"), InferenceOptions.of("<model>")).
  • Every agent type needs a provider and a model, its own
    (inference(in -> in.provider("openai").model("..."))) or the factory's
    defaults. A missing or unregistered one fails when the harness is built,
    naming the agent type and listing the registered providers.
  • DirectHarnessFactory.providerName() is removed: a factory holding
    several providers has no single answer.
  • Spring Boot: nessy.provider and nessy.model are a pair. Set both,
    or neither and name them on each agent type; setting one fails at startup.
    Several vendor keys set no longer means one provider picked by
    auto-configuration order: every lit provider is registered.
  • Spring Boot: an application's own InferenceProvider beans no longer
    switch the vendor presets off.
    They are registered beside them under
    their bean names. A bean named like a lit preset fails startup.
  • Spring Boot: OpenAiAutoConfiguration, AnthropicAutoConfiguration and
    GeminiAutoConfiguration are replaced by InferenceProvidersAutoConfiguration.

    Update any spring.autoconfigure.exclude that names them.

Added

  • ProviderId, the name an application gives a provider; InferenceConfig.provider(ProviderId)
    and provider(String).
  • Spring Boot presets. A hosted preset is registered when its key is set:
    openai (OPENAI_API_KEY), xai (XAI_API_KEY), anthropic
    (ANTHROPIC_API_KEY), gemini (GEMINI_API_KEY or GOOGLE_API_KEY),
    openrouter (OPENROUTER_API_KEY), nvidia (NVIDIA_API_KEY). A local one
    when switched on: lmstudio and ollama (nessy.providers.<id>.enabled: true).
    Every preset was measured against its real endpoint before it shipped: a
    tool call, reported usage, and a forced answer.
  • nessy.providers.<id>.* (api-key, enabled, wire, base-url,
    vendor) overrides any preset field; an id that is not a preset declares a
    custom provider and must set wire (openai, anthropic or gemini) and
    base-url. enabled: false turns any provider off, key or no key.
  • The startup report lists every registered provider (id, wire, endpoint,
    vendor, never the key), and each harness logs the provider and model its
    agent type resolved to when it is built.
  • PresetCandidatesLiveTest (@Tag("live")) measures OpenAI-shaped vendors
    and the existing presets against their real endpoints from the keys in the
    environment, and writes target/preset-measurements.md.

Changed

  • An agent type built from factory defaults that carry no max-tokens gets
    4096 on both doors.
  • The console: NESSY_PROVIDER and NESSY_MODEL are required together; a
    mistyped NESSY_PROVIDER is reported, not thrown. /config shows the
    provider only when the console chose it.
  • The examples name their provider: chat-web and watchman use the lmstudio
    preset by default; watchman's scripted mode is WATCHMAN_PROVIDER=scripted.
  • nessy-bom keeps its parent for the build and is flattened when published:
    it manages only Nessy's own artifacts. Every module publishes the
    repository's own URL and SCM connection.
  • Dependencies: openai-java 4.69.2, anthropic-java 2.65.0, google-genai
    1.73.0, AWS SDK 2.55.5, Jackson 3.2.3 and 2.22.3, Netty 4.2.18, Tomcat
    11.0.26, HikariCP 7.1.0, SLF4J 2.0.20.

0.1.1

Choose a tag to compare

@jwcarman jwcarman released this 27 Sep 20:22

Fixed

  • A turn ended by a TurnPolicy returning FailTurn could not be replayed,
    which left the agent unusable rather than merely ended: every later
    ask, terminate, or any other read reconstitutes from the event stream
    and threw TurnFailed cannot happen in Inferring. The default policy is
    TurnPolicy.calls(20, 25), so this reached any turn that made
    twenty-five model calls without finishing.

    AgentState.Inferring now accepts AgentEvent.TurnFailed and returns to
    Idle. The event is emitted by AwaitingActions but arrives in
    Inferring, because the discharge that freed the last outstanding call is
    applied first.

0.1.0

Choose a tag to compare

@jwcarman jwcarman released this 27 Sep 19:38

Nessy is an agent harness framework for Java. This is the first release.

Added

  • Two doors. DirectHarness<I, O>.ask(agentId, input) runs a turn on the
    calling thread and returns an Outcome<O> — Answered, Refused,
    Failed, or Busy if another turn is already running for that agent.
    QueuedHarness<I>.tell(agentId, input) always accepts and returns nothing;
    the turn runs later. Both doors exclude each other per agent with a lock
    (Locks.TURN), so only one turn ever runs for a given agent at a time.
  • A decide/accept fold. AgentState is a sealed type — Idle,
    Inferring, AwaitingActions, Terminal — that decides a command and
    folds the resulting events. State is rebuilt by replaying events, so
    recovery after a crash is the same code path as normal operation.
  • Storage backends. DirectBackend and QueuedBackend are implemented
    by nessy-backend-jdbc (PostgreSQL, via Schemas.initialize) and
    nessy-backend-inmemory. On the queued door, effects are rows performed
    by a dispatcher; on the direct door, the calling thread performs them.
  • Turn policy and cost. TurnPolicy.calls(20, 25) is the default: the
    model is asked to answer at twenty calls and the turn fails at
    twenty-five, communicated to providers as ToolChoice.Answer. Every
    Outcome.Answered/Refused/Failed carries a TurnStats tally built
    from Usage (one model call) and Tokens, which is either Counted or
    Uncounted depending on what the vendor reports.
  • Retries. Every retry default is RetryPolicy.Never. A model call
    reaches a configured retry policy only when its adapter classifies the
    failure as Failure.Transient.
  • Structured output. OutputReader<O> reads a model's answer into a
    typed result; JsonSchema and JsonSchemaGenerator derive the schema
    sent to the provider.
  • Model providers. Inference adapters for Anthropic, OpenAI (and any
    OpenAI-compatible endpoint), Gemini, and Bedrock. Embedding adapters for
    OpenAI, Gemini, Voyage, and Bedrock.
  • Tools and approvals. Tool<I> binds a typed input and executes
    against a ToolCallRequest<I>. Awaited lets a call answer immediately
    or defer, and every call goes through an Approver. nessy-approval-risk
    scores a call with NIST SP 800-30's likelihood/impact matrix and routes
    it to auto-approve, auto-deny, or a human desk. nessy-approval-policy
    delegates the decision to a PolicyEngine, with nessy-approval-policy-opa
    speaking Rego to Open Policy Agent. nessy-approval-intent is a claim
    channel for a model to declare what it is about to do before it does it.
  • Memory. nessy-memory-episodic cuts a story into named episodes and
    summarizes each as it closes. nessy-memory-notebook gives an agent notes
    it recalls by heading. nessy-memory-summarizing writes a background
    summary of the head of a long conversation.
  • Planning. nessy-planning gives an agent a plan it writes, holds
    across turns, and works through, resent wholesale on every turn.
  • MCP tools. nessy-tool-mcp imports a remote MCP server's tools as
    ordinary Tools, over the official MCP Java SDK.
  • Observability. Narration and NarrationListener report what an
    agent is doing as it happens; nessy-narration-odyssey journals events
    to an Odyssey stream so a page can subscribe, resume, and replay them as
    Server-Sent Events. Async listeners run outside a turn's trace by design.
  • Spring Boot. nessy-spring-boot-starter is a single dependency that
    assembles a harness from nessy.* properties and beans. Each door,
    backend, provider, and embedder has its own @ConditionalOnMissingBean
    auto-configuration in nessy-spring-boot-autoconfigure, so an
    application can override any bean without the starter's opinions.
  • Console. nessy-console builds a working terminal agent from a main
    method in one call.

Requirements

  • Java 25.
  • Spring Boot 4.1 (optional — only needed for nessy-spring-boot-starter).