@felspar/sdk

中文文档
0.4.5发布于 2026/11/10Apache-2.0TypeScriptES Module近 30 天下载 2268

Client and action gate for Felspar, the shared decision system for enterprise agents.

原始使用文档
文档目录12 节
  1. Install
  2. Connect an Eve agent
  3. Hosted client
  4. One call: decide
  5. Action gate
  6. Eve
  7. Claude Agent SDK
  8. OpenAI Agents SDK
  9. Examples
  10. Documentation
  11. License
  12. Decision diagrams for people

@felspar/sdk

Felspar is a shared, versioned decision system for enterprise agents. A company's approved meaning, the evidence a decision needs and the decision itself travel together in a released Decision Pack. Agents ask Felspar for a decision, get a receipt they can verify, and act only on what Felspar decided.

This package connects an agent to Felspar: an Eve connection in two lines, a TypeScript client for the hosted Felspar API, and an action gate that keeps an agent's tools from acting without a verified Felspar decision. It runs on Node.js 24 or later, and its only dependency is zod. The SDK is a transport client: decisions and policy stay with the Felspar service and the released pack, and the SDK does not reimplement them.

The modules under draft/ may change between minor versions while the API settles, before 1.0.

Install

npm install @felspar/sdk

Connect an Eve agent

In agent/connections/felspar.ts:

import { defineMcpClientConnection } from "eve/connections";
import { felsparConnection } from "@felspar/sdk/eve";
export default defineMcpClientConnection(felsparConnection());

The agent gets Felspar's list_jobs, get_job_requirements, get_job_decision and check_output tools and nothing else. The key is the agent's own, created on the Felspar web page (Workspace → Company agents) and read from FELSPAR_AGENT_KEY when Eve connects; the endpoint is FELSPAR_MCP_URL, or production. Pass { url, agentKey, description } to override them. The tools carry the method: get_job_requirements says what to gather and how, and each result says what to do next, so the agent needs no Felspar skill or prompt of its own.

Over MCP, get_job_requirements and get_job_decision answer compactly by default. The brief's input lists the arguments and a short facts example from the job's own questions. The decision keeps what the agent acts on, plus executionObjectId, the full receipt, the context capsule and explanation.evidenceFreshUntil, which is what the action gate records and verifies. detail: "full" returns the REST brief or result unchanged. The agent sends no execution ID: calling get_job_decision again with the same arguments within the same hour returns the decision already made, and blocked means that first call is still running. See MCP tools.

Hosted client

import { createHostedFelsparClient } from "@felspar/sdk/draft/hosted-client";

const felspar = createHostedFelsparClient({
  origin: "https://api.felspar.ai",
  agentKey: process.env.FELSPAR_AGENT_KEY,
});

// The jobs this agent key may run, by ID.
const { jobs } = await felspar.listJobs();

// What the job needs (questions, evidence rules, outcomes and their outputs),
// on the release Felspar runs for this key now.
const job = await felspar.describeCurrentJob("route-support-ticket");
const releaseId = job.release.releaseId;

// Facts in, decision out. The release pin comes from the job description.
const response = await felspar.resolveFacts(releaseId, {
  jobId: "route-support-ticket",
  executionId: crypto.randomUUID(),
  entityId: "48213",
  facts: {
    request_type: {
      text: "I was charged twice for my March invoice.",
      source: { class: "ticket-thread", id: "48213", observedAt: new Date().toISOString() },
    },
  },
});

if (response.status === "ready") {
  console.log(response.result?.outcome, response.result?.receipt);
}

One call: decide

Since 0.4.4; the rule, next step, hand-off words, receipt and open facts since 0.4.5.

When the same kinds of evidence arrive every time (a coding agent's tool call and its change record, a support ticket and its account record), label each piece by kind and let Felspar file it under the job's questions:

const decision = await felspar.decide({
  job: "gate-deploys",
  case: "toolu_01ABC", // the case's ID; leave it out where the job derives its IDs
  evidence: [
    { kind: "tool-call", toolCall: { name: "Bash", input: { command: "kubectl apply -f deploy/prod.yaml" } } },
    { kind: "change-record", id: "CHG-1042", text: "CHG-1042: approved by Ana Ruiz; production" },
  ],
  timeoutMs: 10_000, // the default; keep it inside your caller's own timeout
});
// { action: "allow" | "flag" | "block", reason, rule, nextStep, missing, escalation,
//   resultId, receiptUrl, open, toProceed, outcome, receipt }
if (decision.action === "block") {
  console.error(decision.reason, decision.nextStep, decision.toProceed);
}

decide sends one request, with no execution ID or release: Felspar uses the agent's release (Latest or pinned), and the same call in the same hour returns the same decision. A tool call goes as toolCall exactly as the agent's runtime reports it; Felspar reads it. action is what to do. It is allow or flag only when Felspar decided an outcome that means so, and block for everything else: a blocking outcome, missing evidence, a refusal, no answer within timeoutMs, or no connection. So code that acts only on action fails closed. reason is the policy's quoted rule or, in plain words, why there is no decision. nextStep is what the policy says to do now, escalation.text who takes it from here, and resultId and receiptUrl the decision's receipt. When Felspar couldn't decide, open lists each fact it couldn't settle with a plain-words label, and toProceed says what evidence would settle it. Fields an older Felspar doesn't send come back null or empty. decide never throws, and retries once within the deadline when Felspar is busy or the same call is still being decided. Pass shadow: true when you won't act on the decision; Felspar records it. Each kind must be one the job's description lists under a question's accepted sources.

describeJob(releaseId, jobId) describes a release you already know, such as one you pinned.

Facts can come straight from a system of record, with no agent in between: send each question's evidence as the source's own words (text, such as a ticket's body or an account record's "Plan: Business (monthly)") with where it came from (source.class, source.id, source.observedAt). Felspar classifies those words for answer-choice and yes/no questions; a bare label such as { value: "business" } is refused with label_needs_mapping, because the released question decides what the words mean. Number and date questions take a typed { value } (a date as YYYY-MM-DD). Only the job's own question keys are accepted (unknown_question otherwise).

Words a person gave the agent (a line from a call, a message) are evidence too: send them as given with source: { class, manual: true, locator }, naming in locator the source they came from, and no id or observedAt; Felspar stamps the time it received them, and the result and its receipt list them under manualFacts. A job whose description carries manualFacts: { allowed: false } refuses them (manual_facts_refused): quote the source record itself.

verifyReceipt(executionObjectId, { receipt, expectedReceiptDigest, deliveredCapsule }) asks Felspar to replay a decision and compare its receipt, and checkOutput(job, { executionId, text, output? }) checks text written for one of your decisions (readOutputCheck(job, checkId) reads a check back).

How a case is identified comes from the job's description (identity): send entityId for the record's own ID (provided), identity field values (fields), or nothing (content, and the first decision of an issued job). The result's case gives the case's id and mode; send case.id as entityId for every later decision on the same case. Results are typed (FelsparJobResult): case, subject with its related records, escalation (id, trigger, why) and missing for a gap.

A refused request throws FelsparRequestError with the HTTP status and the service's refusal (its error code, where there is one a fix and a recovery of fix_input, retry or escalate, and any escalation it names, such as for attempt_limit_reached).

Action gate

The gate sits in front of a tool that acts, such as writing an email draft or replying to a ticket. It allows a call only when the call's fields are allowed, the call is about a case Felspar decided on in this session, Felspar re-verifies that decision's receipt, and the decided outcome permits the tool. Any error, and anything unknown or ambiguous, denies.

import {
  createActionGate,
  felsparJobDescriber,
  felsparOutputCheckVerifier,
  felsparVerifier,
} from "@felspar/sdk/draft/action-gate";

const felspar = { origin: "https://api.felspar.ai", agentKey };
const gate = createActionGate({
  job: "route-support-ticket",
  decisionTool: "felspar__get_job_decision", // exact tool names in your runtime
  outputCheckTool: "felspar__check_output",
  tools: {
    helpdesk__reply: {
      writes: "billing-team-reply",
      entity: { field: "ticketId" },
      fields: ["ticketId", "body"],
      outputCheck: { fields: ["body"] },
    },
  },
  verify: felsparVerifier(felspar),
  describe: felsparJobDescriber(felspar),
  verifyOutputCheck: felsparOutputCheckVerifier(felspar),
});

What a tool may do comes from the released pack, not from the gate's config:

  • writes names the output the tool writes, by its id in the pack. The call is allowed only when the released job description lists that output under the outcome Felspar's replay reports. (A rule may give outcomes, a list of outcome ids, instead; exactly one of the two.)
  • Which case a call is about comes from the released job's subject and identity. A decision is recorded by the case.id Felspar's result names (so an agent that sends no entity_id, for an issued, fields or content job, still gets one), and Felspar's replay must name the same case ID and mode. By default:
    • for a job whose subject is a provided, case-insensitive ID such as a web domain (or a release with no subject), every recipient's email domain must belong to the one company Felspar decided on; a decided ID that isn't a company domain allows no recipient, so give such a rule an entity field;
    • for any other job, the call is about the one case decided in the session for this job; with several, the call is denied until the rule names a field.
  • entity: { field } overrides that: the input field must hold the case's ID, the record's own ID (such as a ticket ID) or the case.id Felspar issued or derived, trimmed and matched by the subject's match rule (exact unless the subject is case-insensitive; with no subject, HC-1201 and hc-1201 are two cases). Recipients are then not tied to the case, so a tool that can address anyone should also limit them (testRecipients, or the tool's own settings).
  • outputCheck requires that Felspar passed exactly the text the call sends for that decision (check_output); with writes, for that output.

A denial is { allow: false, reason, escalate?, escalation? }. escalate is the company's next step, quoted from the released description: the outcome's next step when the outcome doesn't permit the tool, or what to do when nothing is decided. For a missing or failed output check it is the fixed instruction to fix what check_output flagged and check again. escalation is the hand-off the policy names: { id, trigger, why }, with the id from Felspar's replay of the outcome (or the job's unresolved path) and why quoted from the release. Both are absent when the release doesn't say. Every adapter below shows them to the model with the reason.

Then connect the gate to your agent framework. Tool names must match exactly as the runtime reports them.

Eve

import { felsparDecisionEvents, felsparGate } from "@felspar/sdk/eve";

// On the connection whose tools act:
approval: felsparGate(gate),
// And in a hook that records Felspar's decisions and output checks:
export default defineHook({ events: felsparDecisionEvents(gate) });

Claude Agent SDK

import { felsparHooks } from "@felspar/sdk/draft/action-gate/claude-agent-sdk";

// Tool names are mcp__<server>__<tool>.
for await (const message of query({ prompt, options: { hooks: felsparHooks(gate) } })) {
  // ...
}

OpenAI Agents SDK

import {
  felsparDecisionOutputGuardrail,
  felsparToolInputGuardrail,
} from "@felspar/sdk/draft/action-gate/openai-agents";

// sessionOf must return an ID unique to the conversation.
const sessionOf = (runContext) => (runContext.context as { conversationId: string }).conversationId;
// On the MCP server whose tools act:
toolInputGuardrails: [felsparToolInputGuardrail(gate, { sessionOf })],
// On Felspar's MCP server:
toolOutputGuardrails: [felsparDecisionOutputGuardrail(gate, { sessionOf })],

Outside an agent framework, checkAction({ rules, toolName, input, decisions, verify, describe }) returns the same verdict.

For production actions, set environment: "production" in the gate rules; use "staging" for a staging gate. This option is unreleased after 0.4.3. A configured gate denies other or missing environments, including staging decisions whose release was later promoted. Omitting it preserves the existing behavior without environment restrictions. Keep the setting host-owned; custom verifiers must return the environment Felspar confirms. A releases pin alone does not establish the decision's environment.

A Felspar receipt records what Felspar decided from the evidence it was given. It is not a factual, legal or security certification, and a passed output check means the text follows the released rules, not that it is true or approved to send.

Examples

felspar-examples has complete projects: an Eve agent, the same agent with email drafts behind the action gate, and a script that gets a decision with no agent.

Documentation

Full documentation: docs.felspar.ai.

License

Apache-2.0

Decision diagrams for people

The hosted client's getReleaseGraph(releaseId), getProposalGraph(proposalId) and getDecisionGraph(executionObjectId) read canonical Mermaid diagrams with release/material pins and a dashboard link. These inspection methods require a person's session and current source access; agent keys are refused. A decision graph reproduces the saved receipt on its original release. Check status before using mermaid: unavailable history is not an inferred path. Display the returned source unchanged.

Use getReleaseDiagram, getProposalDiagram or getDecisionDiagram for the compact overview. Each accepts { detail: "overview" | "full" }; the decision helper also accepts { view: "run" | "release" }. Check availability, display mermaid and the complete outcome next steps in overview, and offer the original graph under canonical. The original graph helpers are unchanged.

版本与发布记录

@felspar/sdk 共发布 12 个版本,首次发布于 2026/10/22;过去 12 个月发布了 12 个版本。

版本发布日期距今
0.4.5latest2026/11/104 天前
0.4.42026/11/095 天前
0.4.32026/11/068 天前
0.4.22026/11/0212 天前
0.4.12026/10/3015 天前
0.4.02026/10/2817 天前

关于 @felspar/sdk 的常见问题

@felspar/sdk 是什么?

Client and action gate for Felspar, the shared decision system for enterprise agents.

如何安装 @felspar/sdk?

在项目目录运行 npm install @felspar/sdk,或使用 pnpm add / yarn add / bun add。

@felspar/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@felspar/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@felspar/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.15.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@felspar/sdk 可以商用吗?

@felspar/sdk 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@felspar/sdk 的下载量有多少?

根据 npm 官方下载统计,@felspar/sdk 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,268 次,日均约 76 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@felspar/sdk 还在维护吗?

最新版本 0.4.5 发布于 2026/11/10(4 天前);至今共发布 12 个版本,其中过去 12 个月发布了 12 个;首次发布于 2026/10/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。