npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sakneen/agent-sdk

v0.5.1

Published

Embed a Sakneen Agent widget in any web application.

Readme

Sakneen Agent SDK

Package: @sakneen/agent-sdk. Legacy InBrief-named exports remain available during migration. Version 0.5.0 is prepared locally; publication is a separate release step.

Mount the Sakneen Agent widget without managing script tags, or consume the server-authoritative Responses protocol through an application-owned transport.

The host application supplies its existing runtime access token, current client, and signed-in user claims. Sakneen uses the client and user context for widget policy and conversation isolation. The customer API independently validates the token and enforces organization, permission, and data scope on every action request.

import { mountSakneenAgent } from "@sakneen/agent-sdk";

const widget = await mountSakneenAgent({
  baseUrl: "https://ai-agent.sakneen.com",
  version: "v1",
  agentId: "agent-id",
  authorization: {
    getAccessToken: async () => {
      const response = await fetch("/api/auth/access-token", { credentials: "same-origin" });
      if (!response.ok) throw new Error("Authentication required");
      return (await response.json()).accessToken;
    },
    getClientIdentity: () => ({
      headerName: "x-inbrief-client-id",
      headerValue: authenticatedSession.inbriefClientId,
    }),
    getUserIdentity: () => ({
      id: authenticatedUser.sub,
      email: authenticatedUser.email,
    }),
  },
  getPageContext: () => ({
    locale: document.documentElement.lang || "en",
    route: window.location.pathname,
    pageType: "search-results",
    activeFilters: currentFilters,
    selectedResourceId: selectedResultId,
    visibleResultIds: visibleResults.map((result) => result.id),
  }),
});

widget.destroy();

Widget runtime 1.8 and later includes a user-facing New conversation action. Integrators can also call widget.resetConversation?.() to clear the browser transcript and start a new server-side conversation programmatically.

getPageContext is optional. Its generic values are validated, deduplicated, and bounded to 20 filters, eight values per filter, 20 visible result IDs, and 8,192 serialized characters. The context helps resolve phrases such as “this result” or “keep these filters.” It is explicitly untrusted and is never used to select a workspace, client, organization, user, credential, or permission. Keep authorization exclusively in the authorization provider.

Opt-in host search synchronization

SDK 0.4.0 and widget runtime 1.13.0 add a separate, optional search-state channel. Text and cards work without it. Nothing is applied by reading model prose or by following a server-provided URL/selector.

First, an administrator reviews a new immutable action revision with this generic contract mapping (example names, not a built-in customer schema):

{
  "hostSearch": {
    "target": "recordSearch",
    "controls": [
      { "input": "regions", "control": "regions" },
      { "input": "maximum", "control": "maximum" }
    ]
  }
}

Only declared query constraints and sorting inputs on a side-effect-free record query may be mapped. The mapping participates in the review fingerprint. It does not grant permission or enable an action. Existing published revisions are unchanged until an explicitly authorized configuration rollout.

The host then adds an exact allowlist and an atomic handler to its mount options:

hostSearch: {
  target: "recordSearch",
  controls: ["regions", "maximum"],
  getRevision: () => currentSearchView ? searchRevision : undefined,
  // Optional: atomically capture all reviewed controls for inbound reconciliation.
  getSnapshot: () => currentSearchView ? {
    revision: searchRevision,
    controls: { regions: searchStore.regions ?? null, maximum: searchStore.maximum ?? null },
  } : undefined,
  apply: (controls, { expectedRevision }) => {
    if (!currentSearchView || searchRevision !== expectedRevision) return false;
    // Use the application's existing store/router; validate host-specific types.
    // Null clears a control. Replace every mapped control, not just non-null ones.
    searchStore.replaceFilters(controls);
    searchRevision = crypto.randomUUID();
    // Start normal pagination reset/result refresh separately from this commit.
    refreshSearchResults();
    return true;
  },
}

apply must commit synchronously, return true only when accepted, and perform its own revision comparison immediately before mutation. Increment the revision on every route, filter, or signed-in-session change, including change-then- change-back. Return undefined when this widget is not on the configured surface. The SDK additionally compares client/user identity in mounted widgets, consumes each request nonce once, expires captures after two minutes, and bounds host state reads to two seconds. Unknown/missing controls, arbitrary navigation, malformed values, stale pages, and replayed commands cannot reach the handler.

Commands contain a complete snapshot of the mapped controls, up to 20 controls, 50 scalar values per array, 512 characters per string, and 8,192 serialized characters. They are emitted only from validated successful lookups (including zero matches), never inferred from prose. Failure to apply leaves the answer and cards intact and displays a separate non-application notice. The acknowledgement confirms acceptance of the filters, not completion of the host's data refresh. No host command is emitted after failed final prose; confirmed lookups can instead appear as failure-recovery evidence (below). Ambiguous network retries retain the original captured context; a page reload cannot replay a command into a newly created SDK binding.

For an application-owned Responses transport, pass hostSearchContext when preparing a response and forward it as host_search_context. Completed responses may include a validated host_search command. createInBriefHostSearchBinding can be used by custom hosts; provide a stable current-principal reader as its second argument, or ensure the revision changes on all identity changes. These fields are never authorization or model instructions.

getSnapshot opts into inbound reconciliation using the same reviewed mapping. It must return the exact allowlisted control set and revision from one atomic store read. Without it, the integration remains outbound-only. The first snapshot seeds mapped inputs. Subsequently, changed controls update the input baseline; unchanged controls preserve newer sticky conversation intent, including attempted refinements whose lookup failed. Explicit tool deltas for the new message take precedence. Non-sticky mapped inputs use the current snapshot each time. Null or an empty array clears a mapped field; required upstream inputs must still validate. Every executed value still passes schema and enabled-option checks. Inconsistent revisions or missing/unknown controls fail rather than silently drop a filter. The server stores bounded private comparison hashes, not a second raw host snapshot.

Changing a revision without changing a value is not a filter reset. An explicit host “start over” operation should call the mounted widget's resetConversation() alongside resetting its filters; the next snapshot starts a new search. Do not reuse old revision IDs. Ambiguous transport retries retain their original frozen snapshot; use a new logical turn for new page changes. A failed snapshot read stops that submission with a visible refresh message rather than running without its filters; outbound apply failures still preserve verified text/cards.

Use bounded getPageContext for unstructured reference grounding; it is not this deterministic control channel. Real integration requires a reviewed mapping and a host implementation; installing the SDK alone does not update an external application's controls.

Responses client

createInBriefAgentClient manages one-input turns, idempotency, response events, and conversation IDs without storing credentials or transcripts. The transport is application-owned so a browser can call a signed same-origin facade while a trusted server can call the public Responses endpoint. Never put a workspace API key in browser code.

The following is trusted server-side pseudocode. Do not place this transport or serverOnlyApiKey in a browser bundle; browser clients should call a signed, same-origin application facade instead.

import {
  createInBriefAgentClient,
  toInBriefAgentPageContextRequest,
} from "@sakneen/agent-sdk";

const client = createInBriefAgentClient({
  agentId,
  transport: async (request) => fetch(
    `${baseUrl}/api/v1/agents/${request.agentId}/responses`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-InBrief-API-Key": serverOnlyApiKey,
        "Idempotency-Key": request.idempotencyKey,
      },
      body: JSON.stringify({
        input: request.input,
        client_id: clientId,
        end_user: endUser,
        ...(request.conversationId
          ? { conversation_id: request.conversationId }
          : {}),
        ...(request.pageContext
          ? { page_context: toInBriefAgentPageContextRequest(request.pageContext) }
          : {}),
        ...(request.hostSearchContext
          ? { host_search_context: request.hostSearchContext }
          : {}),
        stream: request.stream,
      }),
      signal: request.signal,
    },
  ),
});

const conversation = client.createConversation({
  conversationId: savedConversationId,
  onConversationId: saveConversationId,
});
const turn = conversation.prepareResponse("Compare these results.", {
  pageContext: {
    locale: "en-US",
    route: "/search",
    pageType: "search-results",
    activeFilters: { status: ["available"], bedrooms: 3 },
    visibleResultIds: ["result-1", "result-2"],
  },
});
const stream = turn.stream();

for await (const event of stream) {
  if (event.type === "response.output_text.delta") render(event.delta);
}
await stream.completed;

Typed errors and retries

HTTP failures, response.failed transport events, and invalid response streams reject with InBriefAgentResponseError. Branch on code, use status and retryAfter for bounded retry policy, and retain requestId, responseId, conversationId, and traceId for support correlation. Do not parse the human-readable message.

RESPONSE_FINALIZATION_PENDING, RESPONSE_IN_PROGRESS, CONVERSATION_BUSY, and IDEMPOTENCY_STATE_UNAVAILABLE do not authorize a fresh logical request. Retry the same prepared turn with bounded backoff. The widget retains its exact key, original conversation ID and context through these errors and reloads. After recovery, it replaces the transient notice with the completed answer. An expired execution lease may take up to two minutes to become reclaimable.

The server records provider admission before calling the model, including when budgets are disabled. After a process stop, it resumes a pre-provider turn, or materializes an already committed answer without another model/action call. If provider work began but no completed answer was committed, recovery preserves the failed key and durable lookup evidence; it does not replay uncertain work. Committed answers retain their original message ID, timestamp, references and metered usage. This is not a guarantee that every interrupted response succeeds.

The local SDK also exposes error.lookupOutcomes, and response.failed events can include response.lookup_outcomes. These contain up to four durably saved lookups with counts and optional reviewed cards (up to five each). They never contain raw action input, private lookup IDs, host commands, or completed prose. The response remains failed. Show an incomplete-answer notice alongside the cards; current_filters_confirmed: false means historical results, not matches for the latest filters. Only call a zero count “no matches” when total_count_known is true. An unknown total must not imply exhaustive results or a globally best/cheapest record.

The widget keeps recovered cards in its scoped session display as lookup evidence, not an assistant message, and restores them after reload. Retrying the same frozen idempotency key after a finalized failure returns HTTP 409 IDEMPOTENT_RESPONSE_FAILED with saved, re-scoped lookup evidence, without another provider/action execution. A new follow-up uses a new key. Process-stop recovery revalidates conversation scope and fences expired writers; recovery metadata is not a guarantee of uninterrupted delivery.

Result-card follow-ups use the action's reviewed referenceField, not a card's title or display link. The server retains up to four recent distinct result sets, five positions each, independently of prose pruning within the existing 4,096-character structured-history cap. This includes repeated searches using the same action. These are historical references, not proof of current matches or permission; further details require a fresh authorized action.

Exact string IDs support the action transport's existing 512 UTF-8 byte limit. Invalid or oversized IDs are unavailable, never truncated. IDs longer than 96 JSON-serialized characters use private handles and the conditional internal inbriefReadResultReferences tool. It reads only the authorized conversation's retained data, accepts up to five handles per call and four calls per turn, and shares the existing 8,192-character/estimated-token result budget with discovery and action output. It is not a public SDK endpoint or authorization token. Short-ID turns need no extra tool. Neither this name nor discoverAvailableActions may be used as a configured action name. No host adapter change is required.

import { InBriefAgentResponseError } from "@sakneen/agent-sdk";

try {
  await turn.stream().completed;
} catch (error) {
  if (error instanceof InBriefAgentResponseError) {
    reportFailure({
      code: error.code,
      status: error.status,
      retryAfter: error.retryAfter,
      requestId: error.requestId,
      traceId: error.traceId,
    });
  }
  throw error;
}

For current-turn image input, pass ordered inline parts. The SDK freezes the prepared parts for safe retries; the server validates image bytes and persists metadata only, not the base64 payload.

const turn = conversation.prepareResponse([
  { type: "input_text", text: "Describe this floor plan." },
  { type: "input_image", media_type: "image/png", data: base64Png, filename: "plan.png" },
]);

Prepare a turn once and reuse that prepared object for a transport retry. Its input, idempotency key, and original conversation ID are frozen, including an absent ID on the first turn. A newly prepared turn uses the conversation ID captured from the previous response.

baseUrl and version are the only Sakneen runtime location settings. For example, move a customer from staging to production by changing baseUrl; adopt a future API-compatible widget by changing version from v1 to v2.

Runtime identity contract

getAccessToken, getClientIdentity, and getUserIdentity are called before the widget checks availability and before every chat turn. The SDK sends the returned values only for that request. The ID and email are host claims used by Sakneen for widget policy and conversation isolation; they are not customer-API authorization. The customer API must independently validate the delegated token and selected client for every action request.

For a multi-tenant host, getClientIdentity sends the current tenant using exactly one client selector: organization-domain, organization-auth-id, or x-inbrief-client-id. For a host without tenant subdomains, select the intended Sakneen client directly:

getClientIdentity: () => ({
  headerName: "x-inbrief-client-id",
  headerValue: "client-id",
})

Both paths use the same access check. The selected client must exist and have the published agent assigned.

User access policy

Sakneen administrators own widget access. Each client is either off, on for all signed-in users, or on for a specific email list. getUserIdentity supplies the host's claimed runtime user ID and email; the SDK normalizes the email before sending it. Sakneen applies grants to that normalized email, scoped to the exact workspace and client. Raw access tokens and raw external user IDs remain request-scoped; conversations may retain a one-way user hash and the normalized email for isolation and workspace-visible operations.

The SDK applies this contract before mounting and again for every chat turn. Unassigned clients and unauthorized emails receive no launcher.