novo agents

Result handlers

A result handler receives either durable artifact exports or oversized tool-result archives from a run. Signed, idempotent, one artifact per POST.

Use cases: copy generated media or explicit workspace exports into your storage, archive oversized tool outputs, and provide a fallback archive when workspace spill fails.

Methods

| Method | Route | |---|---| | novo.resultHandlers.register({...}) | POST /v1/result-handlers | | novo.resultHandlers.list() | GET /v1/result-handlers | | novo.resultHandlers.get(id) | GET /v1/result-handlers/:id | | novo.resultHandlers.update(id, {...}) | PATCH /v1/result-handlers/:id | | novo.resultHandlers.delete(id) | DELETE /v1/result-handlers/:id | | novo.resultHandlers.rotateSecret(id, { secret }) | POST /v1/result-handlers/:id/rotate-secret |

Type signature

type ResultHandlersNamespace = {
  register(input: {
    kind: 'artifact';
    name: string;
    url: string;
    secret: string;
    requestTimeoutMs?: number | null;
    metadata?: Metadata;
  }): Promise<ResultHandler>;
  list(): Promise<Page<ResultHandler>>;
  get(resultHandlerId: string): Promise<ResultHandler>;
  update(resultHandlerId: string, input: {
    name?: string;
    url?: string;
    requestTimeoutMs?: number | null;
    metadata?: MetadataPatch;
  }): Promise<ResultHandler>;
  delete(resultHandlerId: string): Promise<{ id: string; object: 'result_handler'; deleted: true }>;
  rotateSecret(resultHandlerId: string, input: { secret: string }): Promise<ResultHandler>;
};

Register

const handler = await novo.resultHandlers.register({
  kind: 'artifact',
  name: 'archive',
  url: 'https://api.acme.dev/novo/results',
  secret: 'rh_secret_…',
});

Attach to an agent via agents.update:

await novo.agents.update(agent.id, {
  artifacts: {
    exports: { resultHandlerId: handler.id },
  },
});

artifacts.exports receives generated media deliverables and explicit workspace_export_artifact({ path }) outputs by default. Ordinary document create/edit/fill/export outputs, document previews, extracted images, render artifacts, and oversized tool-output overflow do not use this export path. Browser screenshots are not exported unless artifacts.exports.include.browserScreenshots is true; browser downloads are not exported unless artifacts.exports.include.browserDownloads is true.

artifacts.oversizedToolResults is only for oversized tool-output recovery/archive. When the agent has a spill-capable workspace, oversized tool results first land in the workspace so the agent can inspect them; the result handler receives an archival copy and is used as a fallback if workspace spill fails. Without workspace spill, the handler is the primary recovery store.

Agent artifact refs default to artifacts.stream.urls: 'durable-only': Novo emits your handler's durable URL when one is returned, otherwise it emits an id-only transfer handle. Set artifacts.stream.urls: 'include-novo-signed' only when your consumer explicitly wants temporary Novo signed URLs in public/model-visible tool results.

The delivery shape

For every handler dispatch, Novo POSTs to your URL:

POST {url} HTTP/1.1
Content-Type: application/json
Novo-Signature: t=…,v1=…
Novo-Timestamp:
Novo-Request-Id: req_…
Novo-Org-Id: org_…
Novo-Agent-Id: agent_…
Novo-Run-Id: run_…
Novo-Result-Handler-Id: rh_…
Novo-Result-Kind: artifact

Oversized tool-result artifacts use this body:

{
  "runId": "run_…",
  "threadId": "thread_…",
  "agentId": "agent_…",
  "toolName": "mcp__docs__search",
  "toolCallId": "tc_…",
  "stepNumber": 2,
  "mediaType": "application/json",
  "bytes": 7340032,
  "sha256": "…",
  "content": "{\"large\":true}"
}

Return a JSON object describing your durable artifact, the object you saved on your side:

{
  "artifactId": "tra_123",
  "url": "https://storage.acme.dev/novo/tra_123",
  "ref": {
    "kind": "drive",
    "id": "drive_ref_42",
    "block": "[[block:drive:drive_ref_42]]",
    "url": "https://storage.acme.dev/novo/tra_123"
  },
  "metadata": { "bucket": "agent-archives" }
}

Every artifact producer shares one response contract: return at least one durable locator, artifactId or url. Returned URLs and references must be durable; Novo never calls the handler again merely to refresh a replayed result. The complete JSON response is limited to 256 KiB; metadata is optional and limited to 64 KiB, and paste-ready reference fields are also bounded. A response with neither durable locator is rejected as result_handler_response_invalid; ref/block alone is not enough. (The oversized tool-result recovery path is stricter: it needs an artifactId to persist.)

For artifact exports, ref describes the same customer-side object in a form the agent can cite or paste: optional kind, id, url, and a ready-to-paste block. Novo projects it onto model-visible artifact.ref and, when present, artifact.block on generated media, explicit workspace exports, browser screenshots, and browser downloads. Handlers that still emit a top-level block scalar or transitional suggested_block_refs: { drive: "..." } are accepted and normalized into the same projection. When you configure no export handler for one of these surfaces, Novo populates artifact.ref with its own native { kind: 'novo', id } handle instead, so the agent always has a durable reference to hand off rather than an expiring signed URL.

Generated media and explicit workspace_export_artifact calls send the same top-level fields, but content is structured:

type ManagedArtifactContent =
  | { kind: 'base64'; base64: string; mimeType: string; [key: string]: unknown }
  | { kind: 'url'; url: string; mimeType: string; novoArtifactId: string; [key: string]: unknown };

content.novoArtifactId is the Novo-managed transfer handle: an opaque, retention-scoped id you can use to fetch the bytes (GET /v1/artifacts/:id, or novo.artifacts.get(id) in the SDK) while you save them. It is not a durable identity: return your own durable artifactId/url in the response, and that is what surfaces back to the agent (for example as export.url on workspace_export_artifact). Return ref or block when the agent should paste a product-specific durable reference instead of a raw storage URL.

Document tools write their output files back to the workspace and return Novo transfer handles for downloads/previews. To promote a document to durable storage, have the agent call workspace_export_artifact({ path }) after it decides the file is ready to hand off.

Idempotency

Novo-Request-Id is stable for the same logical artifact. Deduplicate on that header. Return 2xx to acknowledge the export; a non-2xx response, timeout, or invalid JSON makes the current export fail. For oversized tool-result recovery, Novo reports that failure on the data-novo-tool-result-overflow chunk.

Errors

  • 404 result_handler_not_found: bad ID.
  • 400 result_handler_invalid_kind: only artifact is supported today.
  • 400 result_handler_secret_missing: registration requires a secret.