PUNKthe adaptive runtime

//DOCS SDK

TypeScript client surface for chat, tools, feedback, web fetch, and sessions.

@punktechnologies/sdk API reference

For the shortest hosted setup and exact first-run lookup, start with the 5-Minute Quickstart.

The TypeScript client for the Punk gateway. Zero dependencies; works in Bun and Node 18+ with global fetch. For a guided tour, read Onboarding.

import { Punk } from "@punktechnologies/sdk";

Common response types (Run, Pattern, Artifact, SavingsSummary, SomSnapshot, and related results) are exported from the package.


Install

npm install @punktechnologies/sdk
# or
bun add @punktechnologies/sdk

For a runnable starter project:

npm create @punktechnologies/punk-agent@latest my-agent
cd my-agent
cp .env.example .env
# edit .env and set PUNK_API_KEY from https://punktechnologies.com/signup
npm install
npm run smoke

Use the SDK when you need tool tracing, side-effect declarations, feedback, SOM web fetches, evidence reads, or learning/artifact APIs. If you only need model traffic to pass through Punk, a gateway-only base URL change may be enough:

Existing stackGuide
Official OpenAI SDK or OpenAI-compatible clientOpenAI-Compatible AI Gateway
OpenRouter model slugsOpenRouter
Vercel AI SDKVercel AI SDK
LangChain.jsLangChain
Official Anthropic SDKAnthropic SDK
Claude CodeClaude Code
Tools and side effectsAgent Observability & Tool Caching

First-run smoke

This is the SDK equivalent of the gateway curl path. It sends the identity headers, captures the route and run id, then reads the route explanation.

import { Punk } from "@punktechnologies/sdk";

const punk = new Punk({
  app: "support-app",
  agent: "support-agent",
  subject: "user-123"
});

const result = await punk.chat({
  model: "gpt-4o-mini",
  temperature: 0,
  messages: [
    { role: "user", content: "Classify this ticket: I was charged twice. Return JSON." }
  ]
});

console.log({ route: result.route, runId: result.runId });
console.log(result.content);

const explanation = await punk.explain(result.runId);
console.dir(explanation, { depth: null });

Run it with PUNK_BASE_URL=https://app.punktechnologies.com and PUNK_API_KEY=pk_.... The first request normally routes live; repeated stable requests may cache or become optimization candidates after enough evidence. For deployment readiness, call /api/v1/readiness with an admin key; for first-run failures, see OpenAI-Compatible AI Gateway.


Constructor

new Punk(opts?: PunkOptions)
OptionTypeDefaultSent as
baseUrlstringPUNK_BASE_URL or "https://app.punktechnologies.com"not sent as a header (trailing slashes stripped)
apiKeystringPUNK_API_KEYAuthorization: Bearer <apiKey> on every request
appstringPUNK_APP or "default-app"X-Punk-App on gateway calls
agentstringPUNK_AGENT or noneX-Punk-Agent on gateway calls
subjectstringPUNK_SUBJECT or noneX-Punk-Subject on gateway and tool-cache calls

Explicit constructor options win over environment values. Construct one client per (app, agent, subject) identity. Hosted Punk requests require a tenant API key.

Provider config helpers

These helpers return plain objects for existing provider clients; the SDK does not depend on those packages.

punk.identityHeaders();
punk.openAIConfig();
punk.anthropicConfig();
punk.vercelOpenAICompatibleConfig();
punk.vercelAIConfig(); // alias
punk.langChainConfig();
new OpenAI(punk.openAIConfig());
new Anthropic(punk.anthropicConfig());

const punkProvider = createOpenAICompatible(
  punk.vercelOpenAICompatibleConfig({ name: "punk" })
);

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  ...punk.langChainConfig({
    onRun: ({ runId, route }) => console.log({ runId, route })
  })
});

All helpers accept { baseUrl, apiKey, app, agent, subject, headers } overrides. vercelOpenAICompatibleConfig() also accepts name and includeUsage. langChainConfig() additionally accepts onRun, which emits the exact bounded { runId, route, status } response evidence for buffered and streamed calls without a control-plane lookup. Pass fetch beside it when LangChain needs a custom transport.

Subpath helpers

These imports expose the same plain config objects while making the target stack explicit. They do not import provider packages.

import { createPunkOpenAIConfig } from "@punktechnologies/sdk/openai";
import { createPunkAnthropicConfig } from "@punktechnologies/sdk/anthropic";
import { createPunkVercelAIConfig } from "@punktechnologies/sdk/vercel-ai";
import { createPunkLangChainConfig } from "@punktechnologies/sdk/langchain";
import { createPunkOpenRouterConfig, openRouterModel } from "@punktechnologies/sdk/openrouter";
new OpenAI(createPunkOpenAIConfig({ app: "support" }));
new Anthropic(createPunkAnthropicConfig({ app: "support" }));

const punkProvider = createOpenAICompatible(
  createPunkVercelAIConfig({ app: "support", name: "punk" })
);

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  ...createPunkLangChainConfig({ app: "support" })
});

const openrouter = new OpenAI(createPunkOpenRouterConfig({ app: "support" }));
await openrouter.chat.completions.create({
  model: openRouterModel("google/gemini-2.5-flash"),
  messages: [{ role: "user", content: "hello" }],
});

OpenRouter routing is performed by the Punk gateway. The SDK helper normalizes model ids and returns OpenAI-compatible config for Punk.


Gateway Calls

models.list(): Promise<GatewayModel[]>

GET /v1/models. Returns Punk's validated model-routing catalog through the same configured SDK client and API key. Discovery is local to the gateway: it does not start a run or call a model provider.

models.retrieve(modelId): Promise<GatewayModel>

GET /v1/models/{model}. Retrieves one selector from the same bounded local catalog. Model IDs such as punk/chorus are URL-encoded by the SDK; the probe does not start a run or call a model provider.

chat(params: ChatParams): Promise<ChatResult>

POST /v1/chat/completions (OpenAI-compatible) with the X-Punk-* identity headers. Forces stream: false.

interface ChatParams {
  model: string;
  messages: ChatMessage[];
  signal?: AbortSignal;
  bufferedTimeoutMs?: number; // default 150000; never sent to the provider
  cacheBypass?: boolean;
  cacheNoStore?: boolean;
  temperature?: number;
  response_format?: unknown;
  // Chorus requests may also include budget, latency, quality, research,
  // receipt, policy, evaluation, and live-answer controls.
}

interface ChatMessage {
  role: string;
  content: string | null;
  name?: string;
  refusal?: string | null;
  tool_call_id?: string;
  tool_calls?: ChatToolCall[];
}

interface ChatResult {
  content: string; // choices[0].message.content, "" if absent
  runId: string;   // x-punk-run-id response header, "" if absent
  route: string;   // x-punk-route response header, "unknown" if absent
  usage?: PunkUsage;
  model?: string;
  provider?: string;
  finishReason?: string; // choices[0].finish_reason
  toolCalls: ChatToolCall[];
  raw: any;        // full OpenAI-shaped response body
}

Set cacheBypass: true to send X-Punk-Cache-Bypass: true. Punk skips exact-response, tool-plan, and semantic-response cache serving for that call and records the bypass in the run evidence. The SDK removes cacheBypass from the provider request body. Promoted artifacts and other non-cache routes remain eligible.

Set cacheNoStore: true to send Cache-Control: no-store. Punk skips response-cache serving and does not populate exact-response, tool-plan, or semantic-response caches from that call. This stronger control takes precedence over cacheBypass when both are set, remains visible in run evidence, and is removed from the provider request body.

Pass signal: controller.signal to buffered or streamed OpenAI and Anthropic calls to cancel gateway and in-flight provider work. The SDK forwards the signal to fetch, never the provider JSON body, and preserves the resulting AbortError.

Without a caller signal, buffered chat and anthropic.messages calls use a 150-second end-to-end deadline, including response-body consumption. Set bufferedTimeoutMs from 1 through 900000 on an individual buffered call for a tighter workload-specific bound. An explicit deadline composes with signal, and whichever cancels first wins.

Without a caller signal, streaming helpers use the client's requestTimeoutMs (30 seconds by default) only until successful response headers arrive, and keep that deadline active while reading a non-2xx error body. After successful headers, a healthy SSE stream can run for any duration and remains controlled by signal.

Errors: throws on any non-2xx (including policy blocks, which return the verdict in the body).

Aliases: punk.openai.chat(params) and punk.gateway.chat(params).

Tool-call results can be continued without casts. Append the assistant call and the matching tool result to the next request:

const first = await punk.chat({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Look up account acct_42" }],
  tools: [{
    type: "function",
    function: { name: "lookup_account", sideEffectLevel: 1 },
  }],
});

const answer = await punk.chat({
  model: "gpt-4o-mini",
  messages: [
    { role: "user", content: "Look up account acct_42" },
    { role: "assistant", content: null, tool_calls: first.toolCalls },
    {
      role: "tool",
      tool_call_id: first.toolCalls[0]!.id,
      content: JSON.stringify({ plan: "enterprise" }),
    },
  ],
});

streamChat(params: ChatParams): AsyncIterable<ChatStreamChunk>

POST /v1/chat/completions with stream: true. Yields parsed SSE chunks:

for await (const chunk of punk.streamChat({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Stream a reply." }]
})) {
  if (chunk.type === "delta") process.stdout.write(chunk.content);
  if (chunk.type === "done") executeToolCalls(chunk.toolCalls);
  if (chunk.type === "done" && chunk.finishReason === "length") handleTruncation();
}

toolCalls is a cumulative snapshot. Punk assembles fragmented function names and argument deltas, and the done chunk carries the complete calls.

Aliases: punk.chatStream(params), punk.openai.streamChat(params), and punk.gateway.streamChat(params).

anthropic.messages(params: AnthropicMessageParams): Promise<AnthropicMessageResult>

POST /v1/messages (Anthropic-compatible) with stream: false and anthropic-version: 2023-06-01.

const r = await punk.anthropic.messages({
  model: "claude-sonnet-4-6",
  max_tokens: 256,
  betas: ["your-beta-feature-token"], // optional; sent as anthropic-beta, never in JSON
  cacheBypass: true,
  messages: [{ role: "user", content: "What is a deterministic artifact?" }]
});

r.content;       // text blocks joined
r.contentBlocks; // original Anthropic content blocks
r.runId;
r.route;
r.usage;
r.stopReason;

betas is also header-only and accepts individual Anthropic beta feature tokens. cacheBypass and cacheNoStore have the same header-only semantics for buffered and streamed Anthropic Messages calls.

streamMessages(params: AnthropicMessageParams): AsyncIterable<AnthropicMessageStreamChunk>

POST /v1/messages with stream: true. Yields text deltas and a final done chunk. Anthropic tool_use input deltas are normalized into the same cumulative toolCalls shape; execute them from the terminal chunk after the input JSON is complete.

for await (const chunk of punk.streamMessages({
  model: "claude-sonnet-4-6",
  max_tokens: 256,
  messages: [{ role: "user", content: "Stream a haiku about caching." }]
})) {
  if (chunk.type === "delta") process.stdout.write(chunk.content);
  if (chunk.type === "done" && chunk.stopReason === "max_tokens") handleTruncation();
}

Aliases: punk.messagesStream(params) and punk.anthropic.streamMessages(params).

Chorus helper

Use PUNK_CHORUS_MODEL or punkChorusChat() when calling Chorus through the OpenAI-style chat wire:

import { PUNK_CHORUS_MODEL, punkChorusChat } from "@punktechnologies/sdk";

const r = await punk.chat(punkChorusChat({
  messages: [{ role: "user", content: "Build a source-backed answer with a receipt." }],
  budget_limit_usd: 0.25,
  latency_mode: "balanced",
  quality_mode: "maximum_quality",
  receipt_mode: "full",
  research_mode: "som",
  chorus: { requestId: "req_123" }
}));

punkChorusChat() is a small convenience wrapper that sets model: "punk/chorus" while preserving the rest of the chat request. See Chorus for the control fields.


Tool tracing

traceTool<TArgs, TResult>(def: ToolDefinition<TArgs, TResult>): TracedTool<TArgs, TResult>

Wraps a tool function so invocations are traced into a run and read-only results participate in the tool-result cache.

interface ToolDefinition<TArgs, TResult> {
  name: string;
  sideEffectLevel?: SideEffectLevel; // 0–4; default 3 (conservative)
  idempotencyKey?: (args: TArgs) => string | undefined; // write evidence
  ttlSeconds?: number;               // level <= 1 + ttl > 0 => cacheable
  schemaFp?: string;                 // isolate contract versions
  permissionScopeFp?: string;        // deprecated and ignored; scope is server-owned
  execute: (args: TArgs) => Promise<TResult> | TResult;
}

type TracedTool<TArgs, TResult> =
  (args: TArgs, ctx?: { runId?: string }) => Promise<TResult>;

Use withRun to avoid passing ctx.runId through every tool call:

const r = await punk.openai.chat({ model: "gpt-4o-mini", messages });

await punk.withRun(r, async () => {
  await lookupAccount({ accountId: "acct_42" });
});

Behavior of the returned function, in order:

  1. Cache check (only if sideEffectLevel <= 1 and ttlSeconds > 0): POST /api/v1/tool-cache/check with { toolName, schemaFp?, args }; identity is carried in authenticated Punk headers. On a hit, returns the cached result without executing; if a runId was given, traces tool.completed with cached: true. Network failure degrades to a miss.
  2. **Trace tool.called** with { name, args, sideEffectLevel }, only when ctx.runId or active withRun(...) context is available.
  3. **Trace side_effect.planned** with { toolName, level, payload, idempotencyKey? }, only for sideEffectLevel >= 2, before execution, so policy and evidence review can account for it.
  4. Execute def.execute(args).
  5. **Trace tool.completed** with { name, result }.
  6. Cache store (cacheable tools only): POST /api/v1/tool-cache/store with the result and TTL.

Guarantees: without ctx.runId or active run context, the tool executes untraced; trace and cache failures are swallowed (telemetry never breaks the tool call); errors thrown by execute propagate to the caller unchanged. Explicit { runId } has priority over withRun.

Tool-cache authority is server-owned. The SDK cache endpoints derive immutable execution authority from the authenticated API key, current exact grants, connector installation, credential revision, and tool contract. Cache partitions layer the current local policy state onto that execution authority, so policy changes cannot reuse earlier entries. Missing, ambiguous, revoked, or externally governed authority disables reuse. Persisted Buzz principals participate in managed workflow execution authority, but Buzz workflow execution does not currently perform tool-result cache lookup or storage. permissionScopeFp remains temporarily accepted only as a deprecated SDK input; it is ignored and never transmitted.

For writes, use idempotencyKey to resolve a stable, non-secret key from each invocation. Punk records it on planned, executed, suppressed, and completion evidence, and the gateway projects it into side-effect records. The resolver must return a trimmed string of at most 120 characters. It does not inject the key into the external request; execute remains responsible for sending the same key to the provider.

const upsertAccount = punk.traceTool({
  name: "crm.upsertAccount",
  sideEffectLevel: 2,
  idempotencyKey: ({ requestId }) => requestId,
  execute: async ({ requestId, account }) => crm.upsert(account, { idempotencyKey: requestId }),
});

withRun(run, fn) / currentRunId()

withRun accepts a run id string or any object with runId and makes it the active trace context for traceTool calls inside fn.

await punk.withRun(chatResult, async () => {
  console.log(punk.currentRunId());
  await tracedTool(args);
});

trace(runId: string, type: TraceEventType | string, payload: Record<string, unknown>): Promise<void>

POST /api/v1/trace with { runId, type, payload }. Appends a trace event to a run's ledger. Throws on non-2xx (unlike the internal tracing in traceTool, which is best-effort).


Feedback

feedback(runId: string, rating: 1 | -1, correction?: string): Promise<void>

POST /api/v1/runs/:id/feedback with a binary thumbs signal. The SDK maps 1 (thumbs up) to the API's strong-positive 5-star rating and preserves -1 as thumbs down, so both surfaces teach the same outcome. Corrections are the strongest learning signal. They count against pattern stability and artifact confidence. Throws on non-2xx.

feedbackMetrics(runId: string, feedback: FeedbackMetricsInput): Promise<void>

POST /api/v1/runs/:id/feedback/metrics with named application outcomes, an optimization direction, source provenance, and optional tags/evaluator identity. Metric values may be booleans, finite numbers, strings, { score, reason } evidence, or nested namespaces. The gateway remains authoritative for bounds and normalization and appends accepted evidence to the run's trace ledger; this method throws on non-2xx.

await punk.feedbackMetrics(runId, {
  metrics: {
    resolution: { score: true, reason: "ticket resolved" },
    latency_ms: 120,
  },
  optimize: "max",
  tags: { cohort: "support" },
  source: "customer-outcome",
  evaluator: { name: "support-score", version: "1" },
});

Memory quarantine

punk.memory.recordInfluence(runId, { source, trustLane, contentHash? })

POST /api/v1/runs/:runId/memory. Declare what memory/context influenced a run, tagged with its trust lane (untrusted | observed | verified | human_approved). Recording is always allowed: it's cheap telemetry, useful even when enforcement is off.

The SDK treats recording as successful only when the gateway acknowledges the exact requested run, source, and trust lane with a valid influence id. Malformed or mismatched success bodies are rejected instead of being exposed as quarantine evidence.

When the tenant enables memory_quarantine, a low-trust influence (untrusted/observed) on a run gates that run's high-impact (side-effect level ≥ threshold) tool actions to approval_required, so untrusted web content can't trigger a payment. A verified/human_approved influence on the same run covers it. See Governance § Memory Quarantine.

await punk.memory.recordInfluence(runId, { source: "web:example.com", trustLane: "untrusted" });

Web Fetch

fetchSom(url: string, opts?: { bypassCache?: boolean }): Promise<WebFetchResult>

POST /api/v1/web/fetch. Fetches a page and returns compact structured page context instead of raw HTML.

interface WebFetchResult {
  som: SomSnapshot;            // structured page snapshot, with meta byte counts
  source: string;              // adapter name or "cache"
  cached: boolean;             // served from the web snapshot cache
  htmlBytes: number;
  somBytes: number;
  tokensSavedEstimate: number; // raw-HTML tokens you didn't spend
  diff?: SomDiff;              // semantic diff vs. previous snapshot (on refetch)
  context: string;             // compact prompt-ready text rendering
}

bypassCache: true forces a refetch; when a prior snapshot exists, diff reports semantically weighted changes (pricing changed is high-significance; footer noise is low) and an aggregate driftScore in [0,1]. Throws on non-2xx.

Web sessions & actions: punk.web.*

The perception-to-action loop: open a stateful session, act on structured page element ids, observe the result. Actions are protocol-level (follow links, fill/submit forms) and governed server-side.

punk.web.openSession(url): Promise<WebSessionOpenResult>   // POST /api/v1/web/sessions
punk.web.act(sessionId, intent): Promise<WebActResult>     // POST /api/v1/web/sessions/:id/act
punk.web.closeSession(sessionId): Promise<{ ok: boolean }> // DELETE /api/v1/web/sessions/:id
punk.web.listSessions()                                    // GET /api/v1/web/sessions

interface WebActionIntent {
  action: "click" | "type" | "select" | "submit";
  target: string;   // element id e_... (or region id r_form... for submit)
  value?: string;   // for type/select
}

interface WebActResult {
  result: WebActionResult; // { ok, action, target, resolved?, navigated?, url, error?, posted? }
  som: SomSnapshot;        // fresh structured snapshot after the action
  diff?: SomDiff;          // semantic diff vs. the pre-action snapshot
  context: string;         // prompt-ready rendering of the fresh snapshot
}

Governance levels: type/select and form-local click actions (checkbox, radio, reset) are level 0 (session-local form state), navigation click is level 1 (read:web), and submit plus submit-button click are **level 3, a write:web,** gated by the same policy engine as chat tools. Successful form submissions include posted, the serialized field set that was sent (name -> value), so operators can inspect the write payload. Policy deny/approval_required on a web write returns 403 with the verdict; observe-mode keys can never perform web writes ("observe-mode keys cannot perform web writes", 403) though their reads run normally. Every action is audited and every navigation destination (session open, link hrefs, form actions) is SSRF-guarded. Idle sessions auto-close after 5 minutes; sessions are tenant-private (another tenant's key sees 404).


Read APIs

savings(): Promise<SavingsSummary>

GET /api/v1/savings. Tenant rollup: totalRuns across every observed status; completed liveRuns and optimizedRuns; blockedRuns, failedRuns, runningRuns, unknownRuns (completed rows with unknown routes), and unknownStatusRuns; totalCostUsd including failed provider attempts; totalSavedUsd; ghostSavedUsd (observe-mode "would have saved" accounting); totalSavedMs; cacheHitRate; artifactHitRate; and web-context token savings. Failed and in-flight runs are never credited as optimized or included in hit-rate denominators.

patterns(): Promise<Pattern[]>

GET /api/v1/patterns, unwraps { patterns } ([] if absent). Each Pattern carries its lifecycle state (observedcandidate → … → promoted, or negative/retired), fingerprints, runCount, cost/latency averages, stabilityScore, and optimizableScore.

artifacts(): Promise<Artifact[]>

GET /api/v1/artifacts, unwraps { artifacts } ([] if absent). Each Artifact carries state, type, confidence, and evidence counters for an optimized route.

listRuns(options?: RunListOptions): Promise<RunListPage>

GET /api/v1/runs with optional limit, offset, route, status, provider, model, and since filters. Returns a validated bounded page with run rows, effective pagination metadata, and route/status facets. Use the returned run IDs with runDetail, explain, feedback, receipts, or evidence packets.

artifactDetail(id: string): Promise<ArtifactDetail>

GET /api/v1/artifacts/:id.

interface ArtifactDetail {
  artifact: Artifact;
  evaluations: ArtifactEvaluation[]; // evidence rows
  pattern: Pattern | null;           // the source pattern
}

runDetail(id: string): Promise<RunDetail>

GET /api/v1/runs/:id.

interface RunDetail {
  run: Run;                        // includes routeExplanation
  events: TraceEvent[];            // the full append-only trace
  sideEffects: SideEffectRecord[]; // planned/executed/suppressed/blocked
}

run.routeExplanation is the audit story: route, reason, rejected alternatives, policy verdict, cache/artifact details, estimated savings, fallback.

For long or actively growing traces, page the append-only ledger directly instead of loading run detail repeatedly:

let page = await punk.runEvents(id, { limit: 100 });
while (page.window.hasMore) {
  page = await punk.runEvents(id, {
    limit: 100,
    before: page.window.nextBefore!,
  });
}

runEvents returns events in chronological order within each page. Its exclusive append-sequence cursor is stable when new evidence is appended concurrently, and every page is count- and byte-bounded.

For callers that want the complete bounded traversal without hand-written cursor bookkeeping, punk.runEventPages(id, { limit }) is an async iterator. It yields pages from newest to oldest and stops after the final page.

Convenience helpers:

await punk.explain(id);          // compact GET /runs/:id/route; routeExplanation or null
await punk.savingsForRun(id);    // per-run cost/savings/token counters
await punk.sideEffectsForRun(id);
await punk.waitForRun(id);       // polls until completed/failed/blocked

for await (const detail of punk.watchRun(id)) {
  console.log(detail.run.status);
}

receipt(id: string): Promise<PunkReceipt>

GET /api/v1/receipts/:id. Returns the Chorus receipt for a run when one exists.

evidencePacket(runId: string): Promise<EvidencePacket>

GET /api/v1/runs/:runId/evidence-packet. Returns a support/security evidence packet with route explanation, integrity result, replay material when available, side effects, audit rows, trace events, and Chorus material when present.

cacheStats(): Promise<CacheStats>

GET /api/v1/cache/stats{ stats: Array<{ cacheType, entries, hits }> } per tier (exact_response, tool_result, som, negative, …).

invalidateCache(options: CacheInvalidationOptions): Promise<CacheInvalidationResult>

POST /api/v1/cache/invalidate. Pass { all: true } to explicitly confirm a tenant-wide purge, cacheType for one tier, or add keyPrefix for an exact targeted prefix. The SDK validates the audited removal counts, requested scope echo, and post-invalidation cache inventory before returning success.


Learning lifecycle

learningTick(): Promise<LearningReport>

POST /api/v1/learning/tick. Forces one learning pass (it also runs on a timer inside the gateway). Returns at least:

interface LearningReport {
  artifactsSynthesized: number;
  promotionsEligible: string[]; // artifact ids that passed the gates
  autoPromoted: string[];       // promoted hands-free (PUNK_AUTO_PROMOTE)
  [key: string]: unknown;
}

promoteArtifact(id: string, reason?: string): Promise<Artifact>

POST /api/v1/artifacts/:id/promote, unwraps { artifact }. The gateway enforces promotion evidence; side-effect-bearing artifacts additionally require operator action. Throws on non-2xx, including "gate not satisfied" rejections.

rollbackArtifact(id: string, reason: string): Promise<Artifact>

POST /api/v1/artifacts/:id/rollback, unwraps { artifact }. Retires a serving artifact and returns its pattern to the watchlist. The single-line reason is required for lifecycle audit evidence.

quarantineArtifact(id: string, reason: string): Promise<Artifact>

POST /api/v1/artifacts/:id/quarantine, unwraps { artifact }. Immediately removes a suspect serving artifact from routing and returns its pattern to the watchlist. The single-line reason is required for lifecycle audit evidence.


MCP registry helpers

punk.mcp covers the small SDK surface for external MCP servers used by workflow tool_call nodes:

await punk.mcp.listServers();
await punk.mcp.createServer({
  name: "internal-tools",
  transport: "http",
  url: "https://mcp.example.com/mcp",
  headers: { Authorization: "cred:cred_123" }
});
await punk.mcp.testServer("mcp_123");

Registry mutations are admin-only. cred:<id> values resolve stored credentials at connect time.


Prompt ingest

ingestPrompt(source, prompt, opts?): Promise<{ runId: string }>

POST /api/v1/ingest/prompt. Side-loads an externally handled prompt as a completed observed run, useful for Claude Code hooks or other interfaces where Punk observes and audits work it did not execute directly.

await punk.ingestPrompt("claude-code", prompt, {
  sessionId: "local-session",
  metadata: { project: "support" }
});

Tool-result cache (low level)

traceTool calls these for you; they're public for manual integration.

toolCacheCheck(toolName, args, sideEffectLevel?, schemaFp?, deprecatedPermissionScopeFp?)

POST /api/v1/tool-cache/check sends the normalized tool, caller schema label, and arguments. App, agent, and subject travel only as authenticated Punk identity headers. The gateway resolves current server-owned authority and its cache partition. Transport/API failures and unavailable authority return { hit: false }; malformed local dimensions reject before transport.

toolCacheStore(toolName, args, result, ttlSeconds?, sideEffectLevel?, schemaFp?, deprecatedPermissionScopeFp?)

POST /api/v1/tool-cache/store sends the same caller-owned dimensions plus the result and TTL. The gateway caps TTL from the connector manifest, sanitizes declared PII and secrets, and may acknowledge a no-op store when exact authority is unavailable. Transport/API failures are swallowed; malformed local dimensions reject before transport. Caching remains an optimization, not a tool-execution failure mode.


Error behavior summary

SurfaceOn failure
chat, trace, feedback, fetchSom, web sessions, MCP helpers, prompt ingest, all read APIs, learningTick, artifact lifecycle methodsthrows Error("Punk API <METHOD> <path> failed: <status> <statusText>"), with the first 500 chars of the response body appended
Tracing inside traceToolswallowed; the tool call succeeds untraced
toolCacheCheckdegrades to { hit: false }
toolCacheStoreswallowed
def.execute inside a traced toolpropagates unchanged

There are no retries in the SDK; requests are hosted-gateway-first and Punk's router fails open server-side.

Properties

punk.baseUrl, punk.app, punk.agent, punk.subject are readable on the instance. The API key is private.