Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

2.1.0 - 2026-10-15

Added

  • GatewayClient.execute() gains an optional attachments parameter: a list of the new Attachment objects (type, plus exactly one of url or raw data (bytes, bytearray or memoryview), and an optional plain-filename name) sent as attachments in the POST /api/v1/execute body. data is base64-encoded by the SDK. Invalid attachments raise NovaOSError before any request is made. Omitted or empty, the request body is unchanged.
  • AgentCatalogClient and CatalogAgent (novaos_client_sdk.catalog) — find published agents through the marketplace's public catalog (GET /api/agents) by the skills their Agent Card declares, with skill_match="all" or "any". This is the external counterpart to Agent-DNS, which is cluster-internal and carries no capabilities. Results follow the catalog's cursor, stop if the cursor does not advance, and map HTTP failures onto the SDK's exception hierarchy. CatalogAgent.gateway_url() and .card_url() give the gateway addresses to call an agent and read its card.
  • AgentExecutionContext.custom_headers — the caller's custom request headers, filtered and lower-cased. Every ctx.tools.* call now forwards them to the MCP Gateway; headers the SDK sets itself (Authorization, X-Tenant-ID, ...) always take precedence. New novaos_client_sdk.header_safety module (is_safe_custom_header_name, filter_custom_headers) applies the platform's header-name rules.
  • MemoryClient.update() and ctx.tools.memory.update() — edit one of the agent's own agent-scope memories in place, using the version read from query. Returns the new UpdateResult.
  • MemoryClient.hint_status() and ctx.tools.memory.hint_status() read the outcome of a submitted hint (new HintStatus); ctx.tools.memory.proposal_status() reads the outcome of a proposed write.
  • Optional memory_type (semantic, episodic, procedural) on memory write, query and assemble_context, and optional conversation_id on query and assemble_context, to filter recall by type or conversation. Omitted, requests are unchanged. MemoryType is exported and MemoryMetadata gains memory_type.
  • MemoryClient.write() gains ttl_seconds and classification; MemoryClient.query() gains token_budget and latency_budget_ms.
  • ctx.tools.memory now also exposes forget, hint and assemble, and write/query accept the same extra fields.

Security

  • AgentDiscoveryService gains guard_connect_ips (and allow_private_ips) so the Agent Card fetch itself can refuse a base_url that resolves to a private, loopback, link-local or reserved address, and connect to the address it checked. It is off by default so a local http://localhost:8000 agent keeps working; turn it on when base_url comes from a user or other untrusted source, since the request carries the auth headers.
  • validate_host_allowed (the NOVAOS_ALLOWED_HOSTS check used for the gateway and JWKS URLs) now compares hostnames in their IDNA wire form, so a Unicode host matches its punycode entry, and a host that cannot be encoded is rejected.
  • The JSON-RPC, HTTP+JSON and card-fetch HTTP clients now set follow_redirects=False explicitly instead of relying on the httpx default.
  • The gRPC transport now connects to the IDNA 2008 (punycode) form of a Unicode agent host, the same form validate_agent_url checks. gRPC's resolver does not encode Unicode hosts itself, so an internationalised agent host previously failed to resolve instead of connecting to the validated name. grpc_channel_factory receives the same normalised target. A host that cannot be IDNA-encoded is rejected when the transport is created.
  • Clients built by AgentDNSRecord.to_client(), AgentDNSClient.get_agent_client(), discover_and_connect() and AgentDiscoveryService.to_client() now re-check the address at connect time and connect to the address they checked, closing the gap in which a short-TTL DNS answer could be public for validate_agent_url and private for the real connection (DNS rebinding). The host name still drives TLS verification and the Host header. This applies to the JSON-RPC and HTTP+JSON transports, and to gRPC unless grpc_channel_factory builds the channel, which the SDK cannot pin. A Client built directly from a URL is unchanged, so localhost still works; it can opt in with the new guard_connect_ips=True. Requests routed through an HTTP(S)_PROXY environment variable connect to the proxy instead.
  • validate_agent_url (and so AgentDNSRecord.to_client and AgentDiscoveryService.to_client) now checks the hostname in the same IDNA 2008 form the HTTP transport connects to. Previously a Unicode host such as faß.example was resolved as fass.example while httpx dialled xn--fa-hia.example, so the private-IP guard could validate one destination and connect to another. Allowlist suffixes are normalised the same way, and a hostname that cannot be IDNA-encoded is rejected. idna>=3.0 is now a declared dependency.

Changed

  • Breaking: AgentDiscoveryService.to_client() now requires allowed_host_suffixes naming the domains the Agent Card's URL may point at. A card is remote input, and the client's headers/auth are sent to whatever host it names, so a call without an allowlist now raises NovaOSError before any request is made. There is no opt-out. The private, loopback and link-local IP block still applies on top of the allowlist. To migrate, pass your deployment's or partner's domains: await discovery.to_client(allowed_host_suffixes=[".partner.example"]).

Fixed

  • ObservabilityClient.get_run_events no longer fails with HTTP 403 when a user reads their own run. A new route argument ("auto" by default, "user", "service") selects the endpoint; "auto" falls back to the user route on a 403.
  • RunEvent gains optional sequence, trace_id and span_id (None when the platform does not return them).
  • A skill that raises an uncaught exception now reports <type>: <message> as its failure text instead of str(exception) alone, so the exception type reaches the platform and an exception with no message (TimeoutError()) no longer yields an empty failure. The exception is also logged with its traceback, except when mask_output_pii is on, where the traceback is omitted because it carries the message.
  • NovaOSServerError messages raised for non-2xx responses from the Memory Service, MCP Gateway, Gateway observability and agent-ops clients now append a short excerpt of the response body (RFC 9457 errors[].detail, JSON-RPC error.message, a detail field, or the first ~300 characters), so the reason a call was rejected is visible. The ... returned HTTP <code> prefix is unchanged, the httpx documentation link is dropped, and the parsed or raw body is available as details["response_body"].
  • A duplicate knowledge upload (identical content already indexed) is reported as a completed job whose status carries duplicate_of, the original document's id. KnowledgeTool.poll_until_complete returns it like any completed job instead of waiting for a duplicate status, and IngestJobStatus (from get_ingest_status and poll_batch_until_complete) gains duplicate_of and is_duplicate.
  • KnowledgeTool.poll_until_complete and poll_batch_until_complete now stop on a cancelled ingestion job (its collection was deleted before it finished) instead of polling until the timeout.

Documentation

  • AgentDiscoveryService documents passing allowed_host_suffixes when connecting to third-party partner agents, and why.
  • The memory guide now covers typed memories, conversation recall, which writes need human approval, and the null score for run- and task-scope results. The observability note on trace_id/span_id availability is corrected.

2.0.0 - 2026-09-30

The SDK adopts protocol-per-major versioning: 1.x lines track ACP, 2.x lines track A2A. The A2A wire protocol itself already replaced ACP outright in an earlier 0.x release — AgentBuilder/Client rewritten onto A2A v1.0.0 JSON-RPC, handle_acp_request and every ACP-shaped type removed, with no compatibility shim — and this line has shipped nothing but A2A since. 2.0.0 draws the major-version boundary under that work and folds in everything accumulated since: the migrate CLI's three pipeline stages (analyze/transform/verify) for porting an ACP agent onto the A2A shell, a malformed-request-body fix, SSE reconnect support, an Agent Card emitter CLI, SendMessage content negotiation/history-length/reference-task handling, and the SkillResult status-vocabulary and metadata-content-type fixes below. It also carries forward, as its ACP-line baseline, fixes shipped on the 1.x line after this branch diverged: a KnowledgeTool routing-prefix rename, a fail-closed behavior change for a tool with no backend, and configurable per-tool MCP call timeouts.

Version line: 2.0.0 is a breaking change from the 1.x ACP line (ACP surface removed, A2A JSON-RPC surface added). See the versioning and support page in the documentation.

Removed

  • MemoryTool (ctx.tools.memory.*) is deleted. It routed through the MCP Gateway, which never had a matching backend ("tool not found"), and there were zero production consumers. MemoryClient (direct HTTP to the memory service) is, and remains, the canonical path.

Added

  • HITLTool.requires_write_hitl(tool_fqn) — a synchronous, no-network classification of whether a specific MCP tool call is likely to need write approval, mirroring the TypeScript agent SDK's requiresWriteHitl and the platform's own Gate A classifier. Lets a skill decide up front whether to build a pause before calling a write-class tool, instead of guessing or hand-rolling a fail-then-recover loop.
  • AgentExecutionContext.mcp_write_hitl_active — a coarse, run-level mint-time hint (does this run's token carry any write-capable mcp:* scope?), read from the platform's execution_hints. Always False for a service-account run, since an SA has no interactive HITL resume path.
  • ConnectorTool.call() gains a tier argument ("standard" / "licensed" / "ext", default "standard") so licensed and third-party connectors are reachable through the SDK's governed surface. Licensed/ext-tier connectors are registered one segment longer than standard ones; previously, calling one through ConnectorTool.call() always built the standard-tier name and failed with tool-not-found. Backward-compatible: every existing caller, which never passes tier, is unaffected.
  • 2.x documentation set: an A2A primer, a migration guide from 1.x, and a migrate CLI reference; guides updated to the A2A client and agent surfaces.
  • python -m novaos_client_sdk.migrate verify runs a migrated agent's own pre-migration fixtures against it and checks its Agent Card. Where analyze only reads an agent's source and transform only rewrites it, verify is the pipeline stage that actually calls the migrated agent: it converts each fixture into the A2A SendMessage request an equivalent caller would send today, fires it at a live agent process over this SDK's own Client, and asserts the resulting Task reached the expected state. --stream also probes a skill with SendStreamingMessage, and an Agent Card check compares the agent's declared skills against its manifest in both directions.
  • python -m novaos_client_sdk.migrate transform mechanically rewrites an ACP agent onto the A2A shell, using a lossless concrete-syntax-tree rewriter (not ast) so it can edit a file in place while preserving every comment and formatting choice elsewhere in it. It refuses rather than guesses when a migration plan is not eligible or contains an action with no handler, and a second run against an already-migrated file is a no-op (idempotent).
  • python -m novaos_client_sdk.migrate analyze classifies an ACP agent ahead of its A2A migration. It determines which of three request-handling shapes an agent's server implements, surfaces every ACP-shaped construct in its source, and flags anything that would crash or silently misbehave after a mechanical port — all from pure source analysis; the target agent is never imported or executed. --fleet runs the same analysis over a directory of agents at once.
  • AgentBuilder.handle_a2a_bytes()/handle_a2a_bytes_with_headers() and asgi_endpoint() — a malformed request body now answers JSON-RPC -32700, not an HTTP 500. Previously every entry point took an already-decoded body, so a pod route had to decode it itself before reaching the SDK, and invalid JSON there surfaced as an unhandled 500. asgi_endpoint() returns a ready-made ASGI callable that dispatches SendStreamingMessage/SubscribeToTask to an SSE response and everything else to JSON, so a template's entire A2A route collapses to one line.
  • GatewayClient.events() can now resume a dropped SSE connection, and events_with_reconnect() does it automatically. StreamEvent gains an id field carrying the SSE event id; events() gains a last_event_id argument. events_with_reconnect() tracks the last id seen and reconnects on a retryable disconnect with exponential backoff; a non-retryable error (e.g. an authentication failure) still propagates immediately.
  • python -m novaos_client_sdk.agent_card emits a live agent's Agent Card as JSON, so tooling outside the Python process (a CLI publish step, an activation pipeline) can read it without hand-rolling the shape. Exits non-zero with a one-line reason on stderr for a bad module, a missing attribute, or an attribute that isn't an AgentBuilder.
  • AgentBuilder now negotiates content types server-side, honours SendMessage's nested historyLength, and resolves referenceTaskIds. A SendMessage/SendStreamingMessage whose accepted output modes share nothing with the resolved skill's supported modes now answers a request-level ContentTypeNotSupportedError (-32005) before any task is created or persisted. configuration.historyLength now bounds Task.history on SendMessage/SendStreamingMessage themselves, not just GetTask/ListTasks. Behaviour change: a SendMessage/SendStreamingMessage naming a still-open task's id now accumulates onto that task's stored history instead of resetting it — fixing a gap that silently discarded the turn a paused INPUT_REQUIRED/AUTH_REQUIRED task exists to let a caller answer.
  • A dedicated "Agent memory" guide (docs/public/guides/memory.md) covering both doors to Memory Service — ctx.memory.client() (direct) and ctx.tools.memory (through the MCP Gateway) — with a comparison of when to use which, the id table, write idempotency, pre-fetched context, degradation-state fields, and consistency guarantees.
  • ctx.tools.memory is back: ToolBindings.memory (MemoryTool) with write() and query(), calling the real novaos.memory.write / novaos.memory.query MCP Gateway tools. This is a new binding, not a revival of the one 2.0.0 deleted (which targeted Gateway tools with no backend). It is governed like every other ctx.tools binding — Gateway policy, audit, metering and tool.call/tool.result stream events — and needs mcp:tools:memory (or mcp:tools:*) on the execution token in addition to memory:* scopes.
  • Memory pre-fetch before a skill runs. When the platform attaches a memory_budget to a request (it does for agents that declare memory in permissions.tools), AgentBuilder calls assemble_context() once per turn before the skill handler starts and exposes the result as ctx.memory.context; ctx.memory_budget exposes the budget itself. A pre-fetch failure is logged and never fails the turn. Each successful pre-fetch emits a memory.assembled platform event — as a WORKING status frame on streaming requests, and folded into the terminal message's platform metadata part on non-streaming ones. ctx.memory is now cached per execution so the pre-fetched value is visible to the skill.
  • MemoryClient.hint() defaults conversation_id from ctx.context_id, assemble_context() defaults request_id/trace_id from ctx.run_id/ctx.trace_id (it takes no conversation_id — Memory Service scopes live messages from the token), and write() defaults its idempotency key from ctx.idempotency_key plus a per-execution ordinal, when a ctx is passed. The ordinal is tracked on ctx itself (not on the MemoryClient instance, which is typically constructed fresh per call), so repeated writes across one execution get distinct, stable keys instead of a fresh random UUID every time. Backward-compatible: ctx is optional and explicit arguments still override the derived defaults.

Changed

  • Removed internal ticket, decision-record, repository and customer references from shipped docstrings, comments and CLI help; no behaviour change.

Fixed

  • BREAKING: SkillResult.status outside the known vocabulary no longer silently maps to TASK_STATE_COMPLETED. Four legacy spellings (QUEUED, RUNNING, IN_PROGRESS, ERROR) are now mapped to the A2A state they always meant; anything still outside the vocabulary now fails the task as TASK_STATE_FAILED, with the bad value named in both an error-level log and the task's own status message. Behaviour change: an agent relying on the old silent-COMPLETED fallback for an out-of-vocabulary status now gets a failed task instead. A skill returning QUEUED/RUNNING/IN_PROGRESS for a non-idempotent effect should deduplicate on ctx.idempotency_key, since these states are not in INTERRUPT_TASK_STATES and a caller cannot distinguish a deliberate open task from a dropped connection.
  • A skill's own SkillResult.metadata no longer collides with platform metadata on the wire, so it reaches the run-artifacts API. It was previously folded into a DataPart tagged with the same content-type the platform uses for its own internal metadata, so the platform's event pipeline silently excluded it from caller-visible run data. A new NOVAOS_SKILL_METADATA_CONTENT_TYPE marks a skill's own folded-in result distinctly. Wire-shape change: an agent's SkillResult.metadata is now carried in a DataPart tagged a different content-type than before — a minor-version-bump-worthy change.
  • AgentExecutionContext.trace_id now carries the bare W3C trace id, not the whole traceparent header. It previously stored the full header string, so anything correlating on the trace id alone — logs, spans, downstream lookups — never matched. The raw header is available as a new traceparent field for propagation, and tool-call spans now also carry novaos.run_id. Behaviour change: code reading trace_id and expecting a 00-<trace>-<span>-01 string should read traceparent instead.
  • MemoryClient(max_retries=0) now issues one attempt instead of none. Previously the retry loop ranged directly over max_retries, so 0 skipped the loop body entirely — the only place that calls the memory service — and the method raised a TypeError from an unset error variable instead of ever reaching the service or surfacing its real response. max_retries=0 now means "no retries", matching its documented intent, not "no attempts": the one attempt still runs, and its real error (or result) is what a caller sees on failure.