Skip to main content

Paperclip

Run Zihin agents from Paperclip with the zihin-paperclip-adapter external adapter. On each run the adapter invokes a hosted agent (over @zihin/agent-client), which runs the full loop on Zihin's side — tools, memory, governance — and maps the result back to Paperclip's adapter contract.

Adapter type: zihin.

info

Current version: zihin-paperclip-adapter 0.6.0 (npm), over @zihin/agent-client 0.7.0. Upgrading from 0.5.x? Read What changed in 0.6.0.

Installation​

In Paperclip: Settings → Adapters → Install from npm → zihin-paperclip-adapter.

Or via the Paperclip API:

curl -X POST http://localhost:3102/api/adapters \
-H "Content-Type: application/json" \
-d '{"packageName": "zihin-paperclip-adapter"}'

Configuration​

FieldDefaultDescription
API Key—Required. zhn_live_* / zhn_test_* from Settings → API Keys (the tenant is embedded). Stored as a secret.
Agent—Required. The agent to invoke — populated from your tenant.
Base URLhttps://llm.zihin.aiOverride only for a private deployment.
Timeout (ms)200000Max total time for the call (above the backend's 180 s stream budget).
Allow custom base URLoffLets the Base URL point outside the Zihin domain whitelist. Your API key is sent to that host — only for a deployment you trust.
Allow HTTPoffhttp/localhost, development only (the key travels in plaintext).
Paperclip base URLhttp://localhost:3100Used to fetch the issue (title/description) by id to build the message.
Message—Optional fixed message (for testing). When empty, the message is built from the run's issue.
Send run contextoffSends the run's issueId/taskId/taskKey/projectId/wakeReason as the agent context (the model reads it and it is stored with the conversation). Needs an admin/owner API key: with any other key the run fails with a config error instead of running without the context.
Log internalsoffMirrors the model's thinking into the run log. Internal deliberation — keep it off outside debugging.

testEnvironment() validates the connection by listing your agents.

Allow custom base URL

Keep Allow custom base URL off unless you target a trusted private deployment. With it on, the built-in domain whitelist no longer protects your API key.

How it works​

On each run the adapter renders the Paperclip context into a message, calls streamAgent (streaming deltas to the Paperclip transcript via onLog), and consolidates the final result into an AdapterExecutionResult — summary, model, usage, costUsd, and sessionParams.session_id for conversation continuity. The agent runs under your tenant's RBAC, quota and audit.

Result​

resultJson carries content, contentSource (always 'response'), sources, blocks, iterations, executionTimeMs, executionId (from the stream metadata, so it is present even when the turn fails early), outcome, truncated and idempotentReplay (true when the backend replayed a stored answer).

Only a turn that actually answered is a successful run (exitCode 0). Everything else is exitCode 1, keeping sessionParams so the next run continues the same conversation:

SituationerrorCodetimedOut
Stream cut, or closed without a responseprotocolfalse
Turn ended by a server clock (done.outcome: TIMEOUT)TURN_TIMEOUT (errorMeta.timeoutClock)true
Engine failure or refusal (ERROR, BUDGET_EXCEEDED, REFUSED, CONTEXT_LIMIT, …)the outcome or block reasonfalse
Session busy (another turn in flight)SESSION_BUSY (errorMeta.retryable: true)false
Platform gate (human handoff, denylist, suspended session)GATEDfalse
Waiting for a human approval in Zihin (action_proposed/action_forwarded, or done.outcome: PAUSED)AWAITING_APPROVALfalse

An in-stream NOT_FOUND is reported as not_found and clears sessionParams (same as an HTTP 404). Errors surface Zihin's sanitized message and a requestId for support.

Idempotency

The adapter does not expose a Client Message ID setting. idempotentReplay in resultJson reports a replay when the backend performs one. See Agent Client SDK for how idempotency works in the core.

What changed in 0.6.0​

Upgrading from 0.5.x (the adapter now uses @zihin/agent-client 0.7.0):

  • Runs that used to succeed can now fail. REFUSED (model provider refusal) and CONTEXT_LIMIT end the run with exitCode 1 and that errorCode; before, the backend's neutral sentence was the summary.
  • SESSION_BUSY arrives as a terminal error event and is reported with errorFamily: transient_upstream and errorMeta.retryable: true, so the host may retry.
  • A stream cut mid-way is not retryable any more (errorMeta.retryable: false): the backend cancels the turn, so a retry would run the agent's tools again.
  • NOT_FOUND sent in-stream is now not_found and clears sessionParams (same as a 404), instead of a generic server error that kept the session.
  • action_forwarded (approval forwarded to a human) is AWAITING_APPROVAL, like action_proposed.
  • executionId comes from the stream metadata, so it is present even when the turn fails early.