@felspar/sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,14 +5,16 @@ company's approved meaning, the evidence a decision needs and the decision
5
5
  itself travel together in a released Decision Pack. Agents ask Felspar for a
6
6
  decision, get a receipt they can verify, and act only on what Felspar decided.
7
7
 
8
- This package is the TypeScript client for the hosted Felspar API and an action
9
- gate that keeps an agent's tools from acting without a verified Felspar
10
- decision. It runs on Node.js 24 or later, and its only dependency is `zod`.
11
- The SDK is a transport client: decisions and policy stay with the Felspar
12
- service, and the SDK does not reimplement them.
8
+ This package connects an agent to Felspar: an [Eve](https://github.com/vercel/eve)
9
+ connection in two lines, a TypeScript client for the hosted Felspar API, an
10
+ action gate that keeps an agent's tools from acting without a verified Felspar
11
+ decision, and the agent skill for running any Felspar job. It runs on Node.js
12
+ 24 or later, and its only dependency is `zod`. The SDK is a transport client:
13
+ decisions and policy stay with the Felspar service and the released pack, and
14
+ the SDK does not reimplement them.
13
15
 
14
- The modules live under `draft/` while the API settles: they may change between
15
- minor versions before 1.0.
16
+ The modules under `draft/` may change between minor versions while the API
17
+ settles, before 1.0.
16
18
 
17
19
  ## Install
18
20
 
@@ -20,6 +22,23 @@ minor versions before 1.0.
20
22
  npm install @felspar/sdk
21
23
  ```
22
24
 
25
+ ## Connect an Eve agent
26
+
27
+ In `agent/connections/felspar.ts`:
28
+
29
+ ```ts
30
+ import { defineMcpClientConnection } from "eve/connections";
31
+ import { felsparConnection } from "@felspar/sdk/eve";
32
+ export default defineMcpClientConnection(felsparConnection());
33
+ ```
34
+
35
+ The agent gets Felspar's `get_job_requirements`, `get_job_decision` and
36
+ `check_output` tools and nothing else. The key is the agent's own, created on
37
+ the Felspar web page (Workspace → Agents) and read from `FELSPAR_AGENT_KEY`
38
+ when Eve connects; the endpoint is `FELSPAR_MCP_URL`, or production. Pass
39
+ `{ url, agentKey, description }` to override them. Add the skill (below) so the
40
+ agent knows the method.
41
+
23
42
  ## Hosted client
24
43
 
25
44
  ```ts
@@ -30,7 +49,7 @@ const felspar = createHostedFelsparClient({
30
49
  agentKey: process.env.FELSPAR_AGENT_KEY,
31
50
  });
32
51
 
33
- // What the job needs: questions, evidence rules and outcomes.
52
+ // What the job needs: questions, evidence rules, outcomes and their outputs.
34
53
  const job = await felspar.describeJob(releaseId, "classify-prospect");
35
54
 
36
55
  // Facts in, decision out. The release pin comes from the job description.
@@ -56,38 +75,75 @@ service's `refusal` (its `error` code and, where there is one, a `fix`).
56
75
 
57
76
  ## Action gate
58
77
 
59
- The gate sits in front of a tool that acts, such as writing an email draft. It
60
- allows a call only when the call's fields are allowed, every recipient belongs
61
- to a company Felspar decided on in this session, Felspar re-verifies that
62
- decision's receipt, and the decided outcome allows the tool. Any error denies.
78
+ The gate sits in front of a tool that acts, such as writing an email draft or
79
+ replying to a ticket. It allows a call only when the call's fields are allowed,
80
+ the call is about a case Felspar decided on in this session, Felspar
81
+ re-verifies that decision's receipt, and the decided outcome permits the tool.
82
+ Any error, and anything unknown or ambiguous, denies.
63
83
 
64
84
  ```ts
65
- import { createActionGate, felsparVerifier } from "@felspar/sdk/draft/action-gate";
85
+ import {
86
+ createActionGate,
87
+ felsparJobDescriber,
88
+ felsparOutputCheckVerifier,
89
+ felsparVerifier,
90
+ } from "@felspar/sdk/draft/action-gate";
66
91
 
92
+ const felspar = { origin: "https://api.felspar.ai", agentKey };
67
93
  const gate = createActionGate({
68
- job: "classify-prospect",
69
- decisionTool: "felspar__get_job_decision", // exact tool name in your runtime
94
+ job: "route-support-ticket",
95
+ decisionTool: "felspar__get_job_decision", // exact tool names in your runtime
96
+ outputCheckTool: "felspar__check_output",
70
97
  tools: {
71
- agentmail__create_draft: {
72
- outcomes: ["platform-builder"],
73
- fields: ["inboxId", "to", "cc", "bcc", "replyTo", "subject", "text", "html", "labels"],
98
+ helpdesk__reply: {
99
+ writes: "billing-team-reply",
100
+ entity: { field: "ticketId" },
101
+ fields: ["ticketId", "body"],
102
+ outputCheck: { fields: ["body"] },
74
103
  },
75
104
  },
76
- verify: felsparVerifier({ origin: "https://api.felspar.ai", agentKey }),
105
+ verify: felsparVerifier(felspar),
106
+ describe: felsparJobDescriber(felspar),
107
+ verifyOutputCheck: felsparOutputCheckVerifier(felspar),
77
108
  });
78
109
  ```
79
110
 
80
- Then connect it to your agent framework. Tool names must match exactly as the
81
- runtime reports them.
111
+ What a tool may do comes from the released pack, not from the gate's config:
112
+
113
+ - `writes` names the output the tool writes, by its id in the pack. The call is
114
+ allowed only when the released job description lists that output under the
115
+ outcome Felspar's replay reports. (A rule may give `outcomes`, a list of
116
+ outcome ids, instead; exactly one of the two.)
117
+ - `entity` says which case a call is about. By default, every recipient's
118
+ email domain must belong to the one company Felspar decided on. With
119
+ `{ field }`, that input field must hold the decision's `entity_id` (such as a
120
+ ticket ID), trimmed and otherwise exact, case included, as Felspar keys
121
+ cases (`HC-1201` and `hc-1201` are two cases). Only email domains match
122
+ without case.
123
+ Recipients are then not tied to the case, so a tool that can address anyone
124
+ should also limit them (`testRecipients`, or the tool's own settings).
125
+ - `outputCheck` requires that Felspar passed exactly the text the call sends
126
+ for that decision (`check_output`); with `writes`, for that output.
127
+
128
+ A denial is `{ allow: false, reason, escalate? }`. `escalate` is the company's
129
+ next step, quoted from the released description: the outcome's next step when
130
+ the outcome doesn't permit the tool, or what to do when nothing is decided. For
131
+ a missing or failed output check it is the fixed instruction to fix what
132
+ `check_output` flagged and check again. It is absent when the description
133
+ doesn't say. Every adapter below shows it to the
134
+ model with the reason.
135
+
136
+ Then connect the gate to your agent framework. Tool names must match exactly
137
+ as the runtime reports them.
82
138
 
83
139
  ### Eve
84
140
 
85
141
  ```ts
86
- import { felsparDecisionEvents, felsparGate } from "@felspar/sdk/draft/action-gate/eve";
142
+ import { felsparDecisionEvents, felsparGate } from "@felspar/sdk/eve";
87
143
 
88
144
  // On the connection whose tools act:
89
145
  approval: felsparGate(gate),
90
- // And in a hook that records Felspar's decisions:
146
+ // And in a hook that records Felspar's decisions and output checks:
91
147
  export default defineHook({ events: felsparDecisionEvents(gate) });
92
148
  ```
93
149
 
@@ -118,12 +174,28 @@ toolInputGuardrails: [felsparToolInputGuardrail(gate, { sessionOf })],
118
174
  toolOutputGuardrails: [felsparDecisionOutputGuardrail(gate, { sessionOf })],
119
175
  ```
120
176
 
121
- Outside an agent framework, `checkAction({ rules, toolName, input, decisions, verify })`
122
- returns `{ allow, reason }`.
177
+ Outside an agent framework,
178
+ `checkAction({ rules, toolName, input, decisions, verify, describe })` returns
179
+ the same verdict.
123
180
 
124
181
  A Felspar receipt records what Felspar decided from the evidence it was given.
125
- It is not a factual, legal or security certification, and the gate checks
126
- fields, recipients and the decision, not what the tool writes.
182
+ It is not a factual, legal or security certification, and a passed output
183
+ check means the text follows the released rules, not that it is true or
184
+ approved to send.
185
+
186
+ ## Agent skill
187
+
188
+ The package ships the skill that teaches an agent the method for any Felspar
189
+ job: get the requirements, gather exact evidence, get the decision, write and
190
+ check what the decision asks for, and escalate with the company's next step.
191
+
192
+ ```bash
193
+ npx @felspar/sdk skill # writes agent/skills/felspar/SKILL.md
194
+ npx @felspar/sdk skill .claude --force
195
+ ```
196
+
197
+ The directory defaults to `agent`, where an Eve agent keeps its skills; an
198
+ existing copy is kept unless you pass `--force`.
127
199
 
128
200
  ## Documentation
129
201
 
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ // The package's command line: `npx @felspar/sdk skill [dir] [--force]`.
3
+ import { main } from "./skill.mjs";
4
+
5
+ try {
6
+ console.log(`Wrote ${main(process.argv.slice(2))}`);
7
+ } catch (error) {
8
+ console.error(error.message);
9
+ process.exitCode = 1;
10
+ }
package/bin/skill.mjs ADDED
@@ -0,0 +1,33 @@
1
+ // `npx @felspar/sdk skill [dir] [--force]` copies Felspar's agent skill, the
2
+ // method for running any Felspar job, to <dir>/skills/felspar/SKILL.md. The
3
+ // directory defaults to agent, where an Eve agent keeps its skills.
4
+ import { constants, copyFileSync, mkdirSync } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ export const skillSource = path.join(import.meta.dirname, "../skills/felspar/SKILL.md");
8
+ const usage = "usage: npx @felspar/sdk skill [dir] [--force]";
9
+
10
+ // Copies the skill and returns where it went. Without force an existing file
11
+ // is kept: COPYFILE_EXCL fails on any existing path, a dangling link included.
12
+ export function installSkill(directory = "agent", { force = false } = {}) {
13
+ const target = path.join(directory, "skills", "felspar", "SKILL.md");
14
+ const mode = force ? 0 : constants.COPYFILE_EXCL;
15
+ mkdirSync(path.dirname(target), { recursive: true });
16
+ try {
17
+ copyFileSync(skillSource, target, mode);
18
+ } catch (error) {
19
+ throw error.code === "EEXIST"
20
+ ? new Error(`${target} exists; pass --force to replace it`, { cause: error })
21
+ : error;
22
+ }
23
+ return target;
24
+ }
25
+
26
+ export function main(argv) {
27
+ const force = argv.includes("--force");
28
+ const [command, directory, ...rest] = argv.filter((argument) => argument !== "--force");
29
+ if (command !== "skill" || rest.length > 0 || directory?.startsWith("-")) {
30
+ throw new Error(usage);
31
+ }
32
+ return installSkill(directory, { force });
33
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ denialText,
3
+ entityIdOf,
4
+ jobOf
5
+ } from "./chunk-IY6A2423.js";
6
+
7
+ // src/draft-action-gate-eve.ts
8
+ function felsparGate(gate) {
9
+ return async (call) => {
10
+ if (!gate.isGated(call.toolName)) {
11
+ return "not-applicable";
12
+ }
13
+ const verdict = await gate.check(call.session.id, call.toolName, call.toolInput);
14
+ return verdict.allow ? { type: "approved", reason: verdict.reason } : { type: "denied", reason: denialText(verdict) };
15
+ };
16
+ }
17
+ function felsparDecisionEvents(gate) {
18
+ const checks = /* @__PURE__ */ new Map();
19
+ function requested(event) {
20
+ const { actions } = event.data;
21
+ if (actions === void 0) {
22
+ return;
23
+ }
24
+ for (const action of actions) {
25
+ const entity = entityIdOf(action.input);
26
+ if (entity !== void 0 && gate.isDecisionTool(action.toolName ?? "")) {
27
+ gate.decisions.noteRequest(action.callId, entity);
28
+ }
29
+ const job = jobOf(action.input);
30
+ if (job !== void 0 && gate.isOutputCheckTool(action.toolName ?? "")) {
31
+ checks.set(action.callId, job);
32
+ }
33
+ }
34
+ }
35
+ function result(event, context) {
36
+ const action = event.data.result;
37
+ const job = action?.callId === void 0 ? void 0 : checks.get(action.callId);
38
+ if (job !== void 0 && action?.callId !== void 0) {
39
+ checks.delete(action.callId);
40
+ if (action.isError !== true) {
41
+ gate.decisions.recordOutputCheck(context.session.id, job, action.output);
42
+ }
43
+ }
44
+ if (action?.callId !== void 0 && gate.isDecisionTool(action.toolName ?? "") && action.isError !== true) {
45
+ gate.decisions.noteResult(context.session.id, action.callId, action.output);
46
+ }
47
+ }
48
+ return { "actions.requested": requested, "action.result": result };
49
+ }
50
+
51
+ export {
52
+ felsparGate,
53
+ felsparDecisionEvents
54
+ };