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.
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
| Field | Default | Description |
|---|---|---|
| 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 URL | https://llm.zihin.ai | Override only for a private deployment. |
| Timeout (ms) | 200000 | Max total time for the call (above the backend's 180 s stream budget). |
| Allow custom base URL | off | Lets the Base URL point outside the Zihin domain whitelist. Your API key is sent to that host — only for a deployment you trust. |
| Allow HTTP | off | http/localhost, development only (the key travels in plaintext). |
| Paperclip base URL | http://localhost:3100 | Used 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 context | off | Sends 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 internals | off | Mirrors the model's thinking into the run log. Internal deliberation — keep it off outside debugging. |
testEnvironment() validates the connection by listing your agents.
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:
| Situation | errorCode | timedOut |
|---|---|---|
Stream cut, or closed without a response | protocol | false |
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 reason | false |
| Session busy (another turn in flight) | SESSION_BUSY (errorMeta.retryable: true) | false |
| Platform gate (human handoff, denylist, suspended session) | GATED | false |
Waiting for a human approval in Zihin (action_proposed/action_forwarded, or done.outcome: PAUSED) | AWAITING_APPROVAL | false |
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.
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) andCONTEXT_LIMITend the run withexitCode 1and thaterrorCode; before, the backend's neutral sentence was thesummary. SESSION_BUSYarrives as a terminalerrorevent and is reported witherrorFamily: transient_upstreamanderrorMeta.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_FOUNDsent in-stream is nownot_foundand clearssessionParams(same as a 404), instead of a generic server error that kept the session.action_forwarded(approval forwarded to a human) isAWAITING_APPROVAL, likeaction_proposed.executionIdcomes from the streammetadata, so it is present even when the turn fails early.