@promptev/client 0.2.0 → 1.1.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
@@ -1,8 +1,10 @@
1
- # Promptev JavaScript SDK
1
+ # Promptev JavaScript client
2
2
 
3
- The official JavaScript/TypeScript SDK for [Promptev.ai](https://promptev.ai) — trigger and stream your AI **agents** programmatically using your project API key.
3
+ The official JavaScript/TypeScript client for [Promptev](https://promptev.ai) — stream your AI **agents** and upload documents to their knowledge base, from anywhere that has `fetch`.
4
4
 
5
- > In Promptev, an **agent** is your deployed AI assistant: instructions + model + tools (Jira, Slack, GitHub, …) + context packs, all configured in the dashboard. This SDK runs those agents. Tools execute server-side, memory is built in.
5
+ > In Promptev, an **agent** is your deployed AI assistant: instructions + model + tools (Jira, Slack, GitHub, your own APIs) + the documents it can retrieve, all configured in the app. This client runs those agents. Tools execute server-side, retrieval is governed server-side, and conversation memory is built in.
6
+
7
+ **Upgrading from 0.2.x? Read [Migrating from 0.2.x](#migrating-from-02x) first — 1.0 is a rewrite against a new API, and none of the 0.2 calls exist any more.**
6
8
 
7
9
  ## Installation
8
10
 
@@ -16,209 +18,300 @@ pnpm add @promptev/client
16
18
 
17
19
  Requires Node.js 18+ or any modern browser. Zero dependencies.
18
20
 
19
- ## Platform Support
21
+ ## Quick Start
20
22
 
21
- Works everywhere — backend, frontend, and edge runtimes. Built entirely on [Web Platform APIs](https://wintercg.org/) (`fetch`, `ReadableStream`, `AbortController`, `TextDecoder`) with no Node.js-specific modules.
23
+ Mint an API key in Promptev under **Settings → API keys** of the project you want to reach. It looks like `fk_…`, it is shown exactly once, and it is bound to that one project.
22
24
 
23
- | Environment | Supported | Notes |
24
- |---|---|---|
25
- | **Node.js 18+** | ✅ | ESM and CommonJS |
26
- | **Bun** | ✅ | Native Web API support |
27
- | **Deno** | ✅ | Native Web API support |
28
- | **React / Next.js** | ✅ | Works with any bundler |
29
- | **Angular** | ✅ | Full TypeScript types included |
30
- | **Vue / Nuxt** | ✅ | Standard ESM import |
31
- | **Svelte / SvelteKit** | ✅ | Standard ESM import |
32
- | **Vanilla JS** | ✅ | No framework required |
33
- | **Cloudflare Workers** | ✅ | Edge runtime compatible |
34
- | **Vercel Edge Functions** | ✅ | Edge runtime compatible |
35
-
36
- ```js
37
- // ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
25
+ ```ts
38
26
  import { PromptevClient } from "@promptev/client";
39
27
 
40
- // CommonJS (legacy Node.js projects)
41
- const { PromptevClient } = require("@promptev/client");
28
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY! });
29
+
30
+ let conversationId: string | undefined;
31
+
32
+ for await (const event of client.chat("support-agent", "What is the refund policy?")) {
33
+ switch (event.type) {
34
+ case "conversation":
35
+ conversationId = event.id; // keep it to continue this thread
36
+ break;
37
+ case "token":
38
+ process.stdout.write(event.content); // the answer, one delta at a time
39
+ break;
40
+ case "approval_required":
41
+ console.log(`\n⏸ ${event.tool_name} is waiting for a human — ${event.reason}`);
42
+ break;
43
+ case "error":
44
+ throw new Error(event.message);
45
+ }
46
+ }
42
47
  ```
43
48
 
44
- ## Quick Start
45
-
46
- The fastest way to use an agent is `triggerAgent` — one blocking call that runs the agent and returns its final answer. Pass your agent's id (from the **Deploy** tab) and a message:
47
-
48
- ```ts
49
- import { PromptevClient } from "@promptev/client";
49
+ That is the whole conversational surface: one call that starts or continues a thread and streams everything the turn produces.
50
50
 
51
- const client = new PromptevClient({ projectKey: "pv_sk_your_key_here" });
51
+ ## Authentication
52
52
 
53
- const { response, sessionId } = await client.triggerAgent(
54
- "your-agent-id",
55
- "What is the refund policy?",
56
- { variables: { company: "Acme Corp" } }
57
- );
58
- console.log(response);
53
+ The key travels in a header, never in a URL:
59
54
 
60
- // Keep conversation memory by passing the same sessionId back:
61
- const followUp = await client.triggerAgent(
62
- "your-agent-id",
63
- "And for digital goods?",
64
- { sessionId }
65
- );
66
- console.log(followUp.response);
55
+ ```
56
+ Authorization: Bearer fk_live_xxxxxxxx
67
57
  ```
68
58
 
69
- That's it. The agent's tools, context packs, and model all run server-side — you just get the answer.
59
+ Three things follow from that, and they are the reason 1.0 exists:
70
60
 
71
- ## `triggerAgent` — run an agent in one call
61
+ - **The key never leaks into logs.** A key in a URL path ends up in access logs, proxy logs and `Referer` headers. A Bearer token does not.
62
+ - **The key carries its own retrieval identity.** What the agent may read when *you* call it is part of the credential (`retrieves_as`), not a parameter you send. A caller cannot widen its own reach.
63
+ - **The key is bound to one project.** An agent in a sibling project answers exactly the same `404` an unknown slug does — a key holder learns nothing about projects it was not given.
72
64
 
73
- `triggerAgent(agentId, message, opts?)` creates **or** resumes a session **and** runs the agent, returning the final response as JSON.
65
+ Keys are minted by workspace admins, expire (at most a year), and can be revoked at any time. Keep them server-side.
66
+
67
+ ## `chat` — stream a turn
74
68
 
75
69
  ```ts
76
- const result = await client.triggerAgent("your-agent-id", "Summarize our Q4 sales report");
77
- console.log(result.response); // the agent's answer
78
- console.log(result.sessionId); // pass this back to continue the conversation
70
+ client.chat(agentSlug, message, options?): AsyncGenerator<AgentEvent>
79
71
  ```
80
72
 
81
73
  | Parameter | Type | Description |
82
74
  |---|---|---|
83
- | `agentId` | `string` | Your agent's id (from the Deploy tab) |
84
- | `message` | `string` | The user message to send to the agent |
85
- | `opts.sessionId` | `string?` | Pass the `sessionId` from a previous call to keep memory. Omit for a fresh, stateless run. |
86
- | `opts.variables` | `object?` | Fills the agent's `{{...}}` prompt variables |
87
- | `opts.visitor` | `string?` | Attribute the run to an end-user id (stored on the chat session for analytics/audit). Defaults to a generated `webhook_<id>`. Set it on the first call of a conversation; a resumed session keeps its original visitor. |
75
+ | `agentSlug` | `string` | The agent's slug, from its page in Promptev. It must be **published**, and it must be in the key's project — otherwise `NotFoundError`. |
76
+ | `message` | `string` | What to say. Required, non-empty after trimming, at most 20,000 characters. |
77
+ | `options.conversationId` | `string?` | Continue a thread. Take it from the first `conversation` event. |
78
+ | `options.onBehalfOf` | `object?` | Act for one of your end users — see [On behalf of your users](#on-behalf-of-your-users). |
79
+ | `options.model` | `{ provider, model }?` | Run this one call on another model. See [Changing the model or a tool's values for one call](#changing-the-model-or-a-tools-values-for-one-call). |
80
+ | `options.toolInputs` | `Record<toolId, ToolInput>?` | Override what the agent's tools are set up with, for this one call. Same section. |
81
+ | `options.signal` | `AbortSignal?` | Cancel the turn. |
82
+
83
+ Returns an async generator of [events](#events). It ends after `done`, or after `error`.
88
84
 
89
- Returns `{ response: string; sessionId: string }`.
85
+ ### Multi-turn
90
86
 
91
87
  ```ts
92
- // Attribute the run to a specific end-user:
93
- const { response } = await client.triggerAgent(
94
- "your-agent-id",
95
- "How do I reset my password?",
96
- { visitor: "user_12345" }
97
- );
88
+ let conversationId: string | undefined;
89
+
90
+ async function ask(question: string) {
91
+ let answer = "";
92
+ for await (const ev of client.chat("support-agent", question, { conversationId })) {
93
+ if (ev.type === "conversation") conversationId = ev.id;
94
+ if (ev.type === "token") answer += ev.content;
95
+ if (ev.type === "error") throw new Error(ev.message);
96
+ }
97
+ return answer;
98
+ }
99
+
100
+ console.log(await ask("Summarize our Q4 sales report"));
101
+ console.log(await ask("Compare that with Q3")); // remembers the first turn
98
102
  ```
99
103
 
100
- ### Stateful (with memory)
104
+ Passing `conversationId` is worth doing for a second reason: a tool set to **Ask once** asks for approval on the first call in a conversation and not again for the rest of it, so a continued thread interrupts you far less often.
101
105
 
102
- Pass the returned `sessionId` back on each follow-up to keep context:
106
+ A thread belongs to the agent, to this API, **and** to the `onBehalfOf.subject` it was opened under. Keep the subject the same across a conversation's turns, and never reuse an id from the app, the portal or an embed — any mismatch answers `404`, not a fresh thread.
103
107
 
104
- ```ts
105
- let { response, sessionId } = await client.triggerAgent("your-agent-id", "Summarize Q4 sales");
106
- console.log(response);
108
+ ## Changing the model or a tool's values for one call
107
109
 
108
- ({ response } = await client.triggerAgent("your-agent-id", "Compare that with Q3", { sessionId }));
109
- console.log(response);
110
+ Both options are optional. Leave them out and the agent runs exactly as it was published. Nothing a call sends is stored: the next call runs the published agent again.
111
+
112
+ ### `model`
113
+
114
+ ```ts
115
+ for await (const ev of client.chat("support-agent", question, {
116
+ model: { provider: "openai", model: "gpt-5-mini" },
117
+ })) { /* … */ }
110
118
  ```
111
119
 
112
- ### Stateless (no memory)
120
+ `provider` and `model` are the ids shown in your workspace's model list. The workspace's own provider key is used; a call never carries a key. The model runs on its default settings, because the agent's saved settings (temperature and the like) were tuned for its own model.
121
+
122
+ The call answers `400` (a `ValidationError`, with `field` set) before the model is called when the workspace has no such provider, the provider has no key, the model is not in its list or has been retired, or the model cannot call tools and the agent has tools.
123
+
124
+ ### `toolInputs`
113
125
 
114
- Omit `sessionId` for an independent, one-shot run each time:
126
+ Override what a tool is set up with, keyed by the tool's **id** (shown on the tool's page in promptev, with a copy button). Each value goes under the place it belongs to:
115
127
 
116
128
  ```ts
117
- const { response } = await client.triggerAgent(
118
- "your-agent-id",
119
- "What is the refund policy?",
120
- { variables: { company: "Acme Corp" } }
121
- );
129
+ for await (const ev of client.chat("support-agent", "How many open orders does ACME have?", {
130
+ toolInputs: {
131
+ "0b6f2c1e-…": { // an HTTP tool
132
+ headers: { Authorization: `Bearer ${endUserToken}` },
133
+ query: { region: "us" },
134
+ body: { account_id: "acme" },
135
+ path: { customer: "acme" }, // a {{customer}} in the tool's URL
136
+ },
137
+ "7d41a9c3-…": { // an MCP server
138
+ headers: { Authorization: `Bearer ${endUserToken}` },
139
+ arguments: { project_id: "p_123" },
140
+ },
141
+ },
142
+ })) { /* … */ }
122
143
  ```
123
144
 
124
- ### With prompt variables
145
+ | Tool | Place | What it is |
146
+ |---|---|---|
147
+ | HTTP | `headers` | A header the tool was saved with. Names match in any letter case. |
148
+ | HTTP | `query` | A query parameter: one the tool was saved with, or one the model fills. A `GET` or `DELETE` tool has no body, so all of its parameters are `query`. |
149
+ | HTTP | `body` | A body value (`POST`, `PUT`, `PATCH`): a key of the saved body, or a parameter the model fills. |
150
+ | HTTP | `path` | A `{{name}}` in the tool's URL. |
151
+ | MCP server | `headers` | A header the server was saved with (`Authorization` when it was saved with a token). |
152
+ | MCP server | `arguments` | An argument of the server's functions. The value reaches every function that takes an argument of that name. |
153
+
154
+ The rules:
155
+
156
+ - **Override only.** A name the tool does not have in that place answers `400`, and the message lists what the tool does have there. Nothing extra is ever passed through.
157
+ - **A value the model would have filled is fixed for the call.** The model neither sees that parameter nor can change it, so the answer's wording follows the question while the tool gets your value.
158
+ - **What is visible.** A value the model would have filled shows wherever tool arguments show: the `tool_call` event and approval requests. A header, and any value the tool was saved with, never appears in the stream. **Put secrets in `headers`.**
159
+ - **Limits.** 20 tools per call, 20 names per place, 16 KB per tool.
160
+ - Overriding a value the tool was **saved with** (a header, a fixed query parameter, a key of the saved body) needs a server that supports it. One that does not answers `400` saying so; it never runs the call without your value.
161
+
162
+ ## Events
125
163
 
126
- If your agent's instructions contain `{{...}}` placeholders, fill them via `variables`:
164
+ Every frame the server can send, typed. Narrow on `event.type`.
165
+
166
+ A frame from a **newer** server is passed through rather than thrown on, so an event added by a Promptev deploy can never break your integration — it simply matches none of your cases. If you want to log them, `import type { UnknownEvent }` and cast in your `default` branch.
167
+
168
+ | Event | Fields | What it means |
169
+ |---|---|---|
170
+ | `conversation` | `id` | Always first. The thread id — pass it back as `conversationId`. |
171
+ | `thinking` | `content` | One delta of the model's reasoning. Only from models that stream it. |
172
+ | `token` | `content` | One delta of the answer. Concatenate them all to get the reply. |
173
+ | `step` | `name`, `status`, `detail?` | A stage of the turn: reading attachments, retrieving, waiting on an emailed approval. `status` is `started` / `completed` / `failed`. |
174
+ | `sources` | `items[]` (`name`, `document_id`) | The documents the answer is grounded in. |
175
+ | `tool_call` | `id`, `name`, `args?` | The agent invoked a tool. |
176
+ | `tool_result` | `id`, `name`, `ok`, `summary?` | That tool answered. `id` matches the `tool_call`. |
177
+ | `approval_required` | `approval_id`, `tool_name`, `args?`, `reason?`, `channel?`, `expires_at?` | **The turn is paused, waiting for a human** — see below. |
178
+ | `artifact` | `id`, `name`, `mime`, `size` | A file the agent produced this turn. Fetch it from the conversation in Promptev. |
179
+ | `suggestions` | `questions[]` | 2–3 follow-ups the same person would plausibly ask next. |
180
+ | `usage` | `units`, `tokens?`, `tool_calls?` | What the turn cost, just before `done`. |
181
+ | `done` | — | The stream finished cleanly. |
182
+ | `error` | `message` | The turn failed mid-stream. |
183
+
184
+ ### `approval_required` — handle this one
185
+
186
+ A tool the agent wants to run can be gated on human approval. When that happens the turn **pauses and the stream stays open**: a person approves it in the Promptev app, by email, or in Slack, and the turn then continues on that same connection. You do not re-send anything. If nobody decides in time, the approval expires and the turn reports it as a failed `tool_result`.
187
+
188
+ An integration that ignores this event looks *hung* to its user for as long as the approval window lasts. Show it:
127
189
 
128
190
  ```ts
129
- const { response } = await client.triggerAgent(
130
- "your-agent-id",
131
- "How do I reset my password?",
132
- { variables: { company: "Acme Corp", tone: "professional" } }
133
- );
191
+ for await (const ev of client.chat("ops-agent", "Refund invoice 4182")) {
192
+ if (ev.type === "approval_required") {
193
+ console.log(`Waiting on a human: ${ev.tool_name}`);
194
+ console.log(` why: ${ev.reason}`);
195
+ console.log(` args: ${JSON.stringify(ev.args)}`);
196
+ console.log(` decide: in Promptev (${ev.channel}), by ${ev.expires_at ?? "the policy deadline"}`);
197
+ }
198
+ if (ev.type === "tool_result" && ev.approval) {
199
+ console.log(`Approval ended: ${ev.approval}`); // rejected / timeout / expired
200
+ }
201
+ if (ev.type === "token") process.stdout.write(ev.content);
202
+ }
134
203
  ```
135
204
 
136
- ## Streaming an agent
205
+ `channel` is where a human can decide it — `"inline"` (the Promptev app) or `"slack"`. Approvals routed to an **approver by email** send no card at all; they show up as a `step` named `waiting for approval — <tool>`, which closes when the decision lands.
137
206
 
138
- When you want real-time output — agent reasoning, tool-execution status, and the final answer as they happen — use the streaming flow: `startAgent` to open a session, then `streamAgent` to receive [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
207
+ ## On behalf of your users
139
208
 
140
- ### Start a session
209
+ If you are putting Promptev behind your own UI, tell it which of your end users a call is for:
141
210
 
142
211
  ```ts
143
- const session = await client.startAgent("your-agent-id", { visitor: "John" });
144
-
145
- console.log(session.sessionToken); // use this for all subsequent stream calls
146
- console.log(session.agentId); // the agent id, echoed back
147
- console.log(session.name); // agent display name
148
- console.log(session.memoryEnabled); // whether the agent retains conversation context
212
+ for await (const ev of client.chat("support-agent", question, {
213
+ onBehalfOf: { subject: "ana@acme.com", groups: ["finance", "emea"] },
214
+ })) { /* … */ }
149
215
  ```
150
216
 
151
- ### Stream a response
217
+ - `subject` is recorded as the conversation's actor, for the audit trail.
218
+ - `groups` are **intersected** with the key's own grant. They can only ever narrow what the caller may retrieve, never widen it — so a compromised caller of yours still cannot read past what the key was given. (Because the assertion is unverified, the key's own per-user grants drop for that call; only an omitted `onBehalfOf` gets the key's full reach.)
152
219
 
153
- Each event has a `type` and `output`:
220
+ ## `uploadDocument` — add to the knowledge base
154
221
 
155
- | Event Type | Description |
156
- |---|---|
157
- | `thoughts` | Agent's internal reasoning |
158
- | `processing` | Tool execution status (e.g., "Searching Jira...") |
159
- | `approval_required` | Agent needs permission to run a tool |
160
- | `done` | Final response text |
161
- | `error` | Something went wrong |
222
+ ```ts
223
+ client.uploadDocument(files, options?): Promise<{ results: UploadResult[] }>
224
+ ```
225
+
226
+ Files land in the project's **Uploads** source — the same corpus the app writes to, so what you upload is what that project's agents retrieve. The key names the project; there is nothing to address.
162
227
 
163
228
  ```ts
164
- for await (const event of client.streamAgent(session.agentId, {
165
- sessionToken: session.sessionToken,
166
- query: "What are the open P1 bugs in our backlog?",
167
- })) {
168
- if (event.type === "thoughts") console.log(`Thinking: ${event.output}`);
169
- else if (event.type === "processing") console.log(`Running: ${event.output}`);
170
- else if (event.type === "done") console.log(`\n${event.output}`);
171
- else if (event.type === "error") console.log(`Error: ${event.output}`);
229
+ import { readFile } from "node:fs/promises";
230
+
231
+ const { results } = await client.uploadDocument([
232
+ { name: "handbook.pdf", content: await readFile("handbook.pdf") },
233
+ { name: "policies.md", content: "# Refunds\n30 days." },
234
+ ]);
235
+
236
+ for (const r of results) {
237
+ console.log(`${r.filename}: ${r.status}${r.error ? ` — ${r.error}` : ""} (${r.chunks} chunks)`);
172
238
  }
173
239
  ```
174
240
 
175
- ### Multi-turn conversation
241
+ In a browser, pass `File` objects straight from an `<input type="file">`.
242
+
243
+ | Parameter | Type | Description |
244
+ |---|---|---|
245
+ | `files` | `File \| Blob \| { name, content, type? }`, or an array of them | `content` may be a `string`, `ArrayBuffer`, typed array, or `Blob`. |
246
+ | `options.mode` | `"hybrid" \| "graph"` | How it is indexed. `"graph"` needs a graph database and an extraction model configured for the workspace. |
247
+ | `options.signal` | `AbortSignal?` | Cancel the upload. |
248
+
249
+ Limits (enforced here and again on the server): **20 files per call**, **25 MB per file**. A larger set belongs in a connector source (Google Drive, SharePoint, Dropbox, …), configured in the app. Re-uploading identical bytes updates the existing document instead of duplicating it.
250
+
251
+ A per-file failure is *reported*, not thrown: every file gets a row in `results` with its own `status` and `error`. Only a whole-request failure (a bad key, no credits, a rejected file type) raises.
252
+
253
+ > Ingestion is slow and the request does not return until every file is indexed. Raise `timeout` well above its 30-second default: `new PromptevClient({ apiKey, timeout: 300_000 })`.
176
254
 
177
- The session token maintains conversation context across messages:
255
+ ## `runOrchestration` — run a team of agents
256
+
257
+ An orchestration is a **team of agents** working one job. `chat` streams one agent's answer inline; an orchestration runs in a worker for as long as it takes, so `runOrchestration` hands back a run id straight away and `getRun` says what became of it.
178
258
 
179
259
  ```ts
180
- const session = await client.startAgent("your-agent-id", { visitor: "Sarah" });
181
-
182
- // First message
183
- for await (const event of client.streamAgent(session.agentId, {
184
- sessionToken: session.sessionToken,
185
- query: "Summarize our Q4 sales report",
186
- })) {
187
- if (event.type === "done") console.log(event.output);
188
- }
260
+ const started = await client.runOrchestration("month-end-close", {
261
+ period: "2026-03",
262
+ });
189
263
 
190
- // Follow-up — agent remembers the previous context
191
- for await (const event of client.streamAgent(session.agentId, {
192
- sessionToken: session.sessionToken,
193
- query: "Compare that with Q3",
194
- })) {
195
- if (event.type === "done") console.log(event.output);
264
+ let run = await client.getRun(started.run_id);
265
+ while (run.status === "running" || run.status === "revising") {
266
+ await new Promise((r) => setTimeout(r, 2000));
267
+ run = await client.getRun(started.run_id);
196
268
  }
269
+
270
+ console.log(run.status); // "delivered"
271
+ console.log(run.output);
197
272
  ```
198
273
 
199
- ### Collect the final response only
274
+ The orchestration must be **published**, **switched on**, and in your key's project — one that is not published, or lives in a sibling project, throws the same `NotFoundError` an invented name does. One that exists and is published but is switched off says so plainly, so you can tell "turn it on" from "wrong name".
275
+
276
+ The keys in the second argument are the orchestration's **own declared inputs** — the ones its Inputs step lists. A required one you leave out is refused before anything runs, with the field named:
200
277
 
201
278
  ```ts
202
- async function askAgent(client, session, query) {
203
- for await (const event of client.streamAgent(session.agentId, {
204
- sessionToken: session.sessionToken,
205
- query,
206
- })) {
207
- if (event.type === "done") return event.output;
208
- if (event.type === "error") throw new Error(event.output);
209
- }
279
+ try {
280
+ await client.runOrchestration("month-end-close", {});
281
+ } catch (e) {
282
+ if (e instanceof ValidationError) console.log(e.field); // "period"
210
283
  }
284
+ ```
285
+
286
+ ### Sending files
287
+
288
+ An input declared `File(s)` takes a file, keyed by that input:
211
289
 
212
- const answer = await askAgent(client, session, "What's our monthly churn rate?");
290
+ ```ts
291
+ await client.runOrchestration(
292
+ "contract-review",
293
+ { matter: "Acme renewal" },
294
+ { files: { contract: { name: "lease.pdf", content: bytes } } },
295
+ );
213
296
  ```
214
297
 
215
- > **When to use which:**
216
- > - `triggerAgent()` — the default. One call, blocking, returns the final answer. Best for APIs, jobs, and server-to-server use.
217
- > - `startAgent()` + `streamAgent()` — when you want to render reasoning and tool progress live (e.g., a chat UI).
298
+ Up to **five files per call**, 25 MB each. Each part is named after the input it fills, which is what lets one call fill two different file inputs. Files take the same shapes `uploadDocument` does — a `File`, a `Blob`, or `{ name, content }`.
218
299
 
219
- ## Error Handling
300
+ ### Retrying safely
220
301
 
221
- The SDK raises typed errors for each failure scenario:
302
+ Starting a run twice runs the team twice. Pass an `idempotencyKey` and a retry returns the run you already started:
303
+
304
+ ```ts
305
+ await client.runOrchestration(
306
+ "month-end-close",
307
+ { period: "2026-03" },
308
+ { idempotencyKey: "close-2026-03" },
309
+ );
310
+ ```
311
+
312
+ Only your caller knows whether two calls are one event, which is why this is yours to send rather than something the client invents. Without one the start is **not** retried on a transient failure, because a retried start could fan the team out twice.
313
+
314
+ ## Error Handling
222
315
 
223
316
  ```ts
224
317
  import {
@@ -227,230 +320,297 @@ import {
227
320
  AuthenticationError,
228
321
  NotFoundError,
229
322
  RateLimitError,
323
+ InsufficientCreditsError,
230
324
  ServerError,
231
325
  NetworkError,
232
326
  } from "@promptev/client";
233
327
 
234
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
235
-
236
328
  try {
237
- const { response } = await client.triggerAgent("your-agent-id", "Hello");
238
- console.log(response);
329
+ for await (const ev of client.chat("support-agent", "Hello")) {
330
+ if (ev.type === "token") process.stdout.write(ev.content);
331
+ }
239
332
  } catch (err) {
240
333
  if (err instanceof ValidationError) {
241
- // 400 — missing variables, bad input
242
- console.log(`Invalid request: ${err.message}`);
243
- } else if (err instanceof NotFoundError) {
244
- // 404 — agent or project not found
245
- console.log(`Not found: ${err.message}`);
334
+ console.log(`Invalid request: ${err.message} (field: ${err.field})`);
246
335
  } else if (err instanceof AuthenticationError) {
247
- // 401/403 — invalid API key or agent not active
248
- console.log(`Auth error: ${err.message}`);
336
+ console.log(`Bad or revoked API key: ${err.message}`);
337
+ } else if (err instanceof NotFoundError) {
338
+ console.log(`No such published agent in this key's project: ${err.message}`);
339
+ } else if (err instanceof InsufficientCreditsError) {
340
+ console.log(`Out of credits — top up in Billing: ${err.message}`);
249
341
  } else if (err instanceof RateLimitError) {
250
- // 429 — API usage quota exceeded
251
- console.log(`Rate limited: ${err.message}`);
342
+ console.log(`Rate limited; retry in ${err.retryAfter ?? "a moment"}s`);
252
343
  } else if (err instanceof ServerError) {
253
- // 5xx — server error (after retries exhausted)
254
- console.log(`Server error: ${err.message}`);
344
+ console.log(`Server error (after retries): ${err.message}`);
255
345
  } else if (err instanceof NetworkError) {
256
- // Connection failed, timeout, DNS error
257
- console.log(`Network error: ${err.message}`);
346
+ console.log(`Could not reach Promptev: ${err.message}`);
258
347
  }
259
348
  }
260
349
  ```
261
350
 
262
- All errors extend `PromptevError` and include:
263
- - `err.statusCode` — HTTP status code (if applicable)
264
- - `err.responseText` — Raw response body (for debugging)
351
+ All errors extend `PromptevError` and carry:
352
+
353
+ - `err.statusCode` — the HTTP status, when there was one
354
+ - `err.field` — which input the server objected to, when it named one
355
+ - `err.responseText` — the raw body, for debugging
265
356
 
266
- | Error | HTTP Status | When |
357
+ | Error | HTTP | When |
267
358
  |---|---|---|
268
- | `ValidationError` | 400 | Missing required variables, bad input |
269
- | `AuthenticationError` | 401, 403 | Invalid API key, agent not active |
270
- | `NotFoundError` | 404 | Project or agent not found |
271
- | `RateLimitError` | 429 | API usage quota exceeded |
272
- | `ServerError` | 5xx | Server error (after retries exhausted) |
273
- | `NetworkError` | — | Connection failed, timeout, DNS error |
274
- | `PromptevError` | any | Base class for all above errors |
359
+ | `ValidationError` | 400, 422 | Empty or over-long message, bad `mode`, a rejected file type — and the same checks made client-side before a request goes out |
360
+ | `AuthenticationError` | 401, 403 | Missing, malformed, revoked or expired key |
361
+ | `InsufficientCreditsError` | 402 | The workspace is out of credits. Waiting does not fix it; a top-up does |
362
+ | `NotFoundError` | 404 | Unknown agent slug, an unpublished agent, or one outside the key's project — all three answer identically on purpose |
363
+ | `RateLimitError` | 429 | Too many requests. `err.retryAfter` is the server's own answer in seconds |
364
+ | `ServerError` | 5xx | The server failed, after retries |
365
+ | `NetworkError` | — | DNS, connection, abort, or the client timeout |
366
+ | `PromptevError` | any | Base class for all of the above |
367
+
368
+ **Retries.** `uploadDocument` retries 502/503/504 with exponential backoff (`maxRetries`, default 2), which is safe because a document's id is a hash of its bytes — the same upload twice is the same document. `chat` is deliberately **not** retried: re-sending a turn could run the agent, and its tools, a second time.
369
+
370
+ **A failed turn may also arrive as an `error` event** rather than an exception, when the stream had already started. Handle both.
371
+
372
+ ## What a call costs
373
+
374
+ Promptev is pay as you go and this API is metered. The rates below are the
375
+ managed ones; a workspace running on its own database and embedding provider
376
+ (BYO) is charged a flat **1** per operation instead, because those bills are
377
+ already yours.
378
+
379
+ | what happens | credits |
380
+ |---|---|
381
+ | any request to this API | **1** (`api_call`) |
382
+ | a chat turn | **1** (`chat_turn`) |
383
+ | each tool the agent calls | **1** |
384
+ | retrieval | **1** per knowledge base searched |
385
+ | retrieval in graph mode | **5** per knowledge base searched |
386
+ | a document you upload | **1** per page, slide or MB (minimum 1) |
387
+
388
+ So an ordinary `chat` costs **2** — one for the request, one for the turn — and
389
+ more if the agent calls tools or searches knowledge. The same conversation held
390
+ in the Promptev app costs one, because there is no request to the programmatic
391
+ door.
392
+
393
+ Two things worth knowing before they surprise you:
394
+
395
+ - **A refused request is still billed.** An unknown agent slug, a rate-limited
396
+ call and a rejected upload each consumed the same capacity a successful one
397
+ would, so each costs its `api_call` credit. Only the outcome differs.
398
+ - **A request this client rejects locally is never billed**, because it never
399
+ leaves your process. That is why the empty-message and upload-limit checks
400
+ exist here rather than being left to the server.
401
+
402
+ Model tokens are not billed by Promptev at all. Model calls go to your provider
403
+ on your own keys, and that bill is yours.
275
404
 
276
405
  ## Configuration
277
406
 
278
407
  ```ts
279
408
  const client = new PromptevClient({
280
- projectKey: "pv_sk_...", // Required — your project API key
281
- baseUrl: "https://api.promptev.ai", // Default — override for self-hosted
282
- timeout: 30000, // Default — request timeout in ms
283
- maxRetries: 2, // Default — retries for 502/503/504
284
- headers: { "X-Custom": "value" }, // Optional — extra HTTP headers
409
+ apiKey: "fk_...", // Required
410
+ baseUrl: "https://api.promptev.ai", // Default — override for self-hosted or during the cutover
411
+ timeout: 30000, // Default — ms to wait for the response
412
+ maxRetries: 2, // Default — retries for 502/503/504
413
+ headers: { "X-Custom": "value" }, // Optional — extra HTTP headers
285
414
  });
286
415
  ```
287
416
 
288
417
  | Parameter | Default | Description |
289
418
  |---|---|---|
290
- | `projectKey` | *required* | Your Promptev project API key (`pv_sk_...`) |
291
- | `baseUrl` | `https://api.promptev.ai` | API base URL |
292
- | `timeout` | `30000` | Request timeout in milliseconds |
293
- | `maxRetries` | `2` | Automatic retries for transient server errors (502, 503, 504) |
419
+ | `apiKey` | *required* | Your Promptev API key (`fk_…`) |
420
+ | `baseUrl` | `https://api.promptev.ai` | API base URL. Self-hosted? Point it at your own host. |
421
+ | `timeout` | `30000` | Milliseconds to wait for the **response**. A `chat()` stream is not bounded by it — a turn that runs tools, or waits on an approval, may run for minutes. |
422
+ | `maxRetries` | `2` | Automatic retries for transient server errors (502, 503, 504) on non-streaming calls |
294
423
  | `headers` | `{}` | Additional HTTP headers |
295
424
 
296
- ## API Reference
425
+ ## Platform Support
297
426
 
298
- ### `PromptevClient`
427
+ Works everywhere — backend, frontend, and edge runtimes. Built entirely on [Web Platform APIs](https://wintercg.org/) (`fetch`, `ReadableStream`, `AbortController`, `TextDecoder`, `FormData`) with no Node.js-specific modules.
299
428
 
300
- | Method | Description | Returns |
429
+ | Environment | Supported | Notes |
301
430
  |---|---|---|
302
- | `triggerAgent(agentId, message, opts?)` | Run an agent in one blocking call | `Promise<TriggerResult>` |
303
- | `startAgent(agentId, options?)` | Start a streaming agent session | `Promise<AgentSession>` |
304
- | `streamAgent(agentId, options)` | Stream an agent response (SSE) | `AsyncGenerator<AgentEvent>` |
431
+ | **Node.js 18+** | ✅ | ESM and CommonJS |
432
+ | **Bun** | ✅ | Native Web API support |
433
+ | **Deno** | ✅ | Native Web API support |
434
+ | **React / Next.js** | ✅ | Server-side — see the warning below |
435
+ | **Angular** | ✅ | Full TypeScript types included |
436
+ | **Vue / Nuxt** | ✅ | Standard ESM import |
437
+ | **Svelte / SvelteKit** | ✅ | Standard ESM import |
438
+ | **Cloudflare Workers** | ✅ | Edge runtime compatible |
439
+ | **Vercel Edge Functions** | ✅ | Edge runtime compatible |
305
440
 
306
- ### `TriggerResult`
441
+ ```js
442
+ // ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
443
+ import { PromptevClient } from "@promptev/client";
307
444
 
308
- | Field | Type | Description |
309
- |---|---|---|
310
- | `response` | `string` | The agent's final answer |
311
- | `sessionId` | `string` | Pass back as `opts.sessionId` to continue the conversation |
445
+ // CommonJS (legacy Node.js projects)
446
+ const { PromptevClient } = require("@promptev/client");
447
+ ```
312
448
 
313
- ### `AgentSession`
449
+ > **Do not ship your API key to a browser.** The key carries a retrieval identity: whoever holds it can read everything it was granted. Call Promptev from your own server, or put an agent in a page with Promptev's embed widget, which issues a short-lived per-visitor token instead.
314
450
 
315
- | Field | Type | Description |
316
- |---|---|---|
317
- | `sessionToken` | `string` | Token for subsequent stream calls |
318
- | `agentId` | `string` | Agent identifier |
319
- | `name` | `string` | Agent display name |
320
- | `memoryEnabled` | `boolean` | Whether the agent retains conversation context |
321
- | `messages` | `array` | Previous messages (populated when resuming a session) |
451
+ ## Framework Examples
322
452
 
323
- ### `AgentEvent`
453
+ ### Next.js route handler — stream to your own UI
324
454
 
325
- | Field | Type | Description |
326
- |---|---|---|
327
- | `type` | `string` | Event type: `thoughts`, `processing`, `done`, `error`, `approval_required` |
328
- | `output` | `string` | Event content text |
329
- | `raw` | `object` | Full parsed SSE event data |
455
+ ```ts
456
+ // app/api/ask/route.ts
457
+ import { PromptevClient } from "@promptev/client";
330
458
 
331
- ## Framework Examples
459
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY! });
460
+
461
+ export async function POST(req: Request) {
462
+ const { question, conversationId, user } = await req.json();
463
+
464
+ const stream = new ReadableStream({
465
+ async start(controller) {
466
+ const encoder = new TextEncoder();
467
+ try {
468
+ for await (const ev of client.chat("support-agent", question, {
469
+ conversationId,
470
+ onBehalfOf: { subject: user.email, groups: user.groups },
471
+ })) {
472
+ // Forward the events your UI cares about, as your own SSE stream.
473
+ if (ev.type === "token" || ev.type === "approval_required" || ev.type === "conversation") {
474
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`));
475
+ }
476
+ }
477
+ } catch (err) {
478
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: (err as Error).message })}\n\n`));
479
+ } finally {
480
+ controller.close();
481
+ }
482
+ },
483
+ });
332
484
 
333
- ### React
485
+ return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
486
+ }
487
+ ```
334
488
 
335
- ```tsx
336
- import { useState } from "react";
337
- import { PromptevClient } from "@promptev/client";
489
+ ### Express — collect the answer server-side
338
490
 
339
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
491
+ ```js
492
+ import express from "express";
493
+ import { PromptevClient } from "@promptev/client";
340
494
 
341
- export function AskAgent() {
342
- const [answer, setAnswer] = useState("");
343
- const [loading, setLoading] = useState(false);
495
+ const app = express();
496
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY });
344
497
 
345
- async function handleAsk() {
346
- setLoading(true);
347
- try {
348
- const { response } = await client.triggerAgent("your-agent-id", "What is your refund policy?");
349
- setAnswer(response);
350
- } finally {
351
- setLoading(false);
352
- }
498
+ app.post("/api/ask", express.json(), async (req, res) => {
499
+ let answer = "";
500
+ let conversationId = req.body.conversationId;
501
+ let pendingApproval = null;
502
+
503
+ for await (const ev of client.chat("support-agent", req.body.question, { conversationId })) {
504
+ if (ev.type === "conversation") conversationId = ev.id;
505
+ if (ev.type === "token") answer += ev.content;
506
+ if (ev.type === "approval_required") pendingApproval = ev; // tell the user!
507
+ if (ev.type === "error") return res.status(502).json({ error: ev.message });
353
508
  }
354
509
 
355
- return (
356
- <div>
357
- <button onClick={handleAsk} disabled={loading}>Ask Agent</button>
358
- {answer && <p>{answer}</p>}
359
- </div>
360
- );
361
- }
510
+ res.json({ answer, conversationId, pendingApproval });
511
+ });
512
+
513
+ app.listen(3000);
362
514
  ```
363
515
 
364
- ### React — Streaming Agent Chat
516
+ ### Upload from a browser form (through your backend)
365
517
 
366
- ```tsx
367
- import { useState, useRef } from "react";
368
- import { PromptevClient } from "@promptev/client";
518
+ ```ts
519
+ // Your server route, holding the key:
520
+ const files = form.getAll("files") as File[];
521
+ const { results } = await client.uploadDocument(files, { mode: "hybrid" });
522
+ ```
369
523
 
370
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
524
+ ## Migrating from 0.2.x
371
525
 
372
- export function AgentChat() {
373
- const [messages, setMessages] = useState<string[]>([]);
374
- const [input, setInput] = useState("");
375
- const sessionRef = useRef(null);
526
+ **1.0 talks to a different backend.** 0.2.x targeted `POST /api/sdk/v1/agent/{project_key}/{agent_id}/…` on the previous product; Promptev's current API is `/api/v1`, which does not serve those paths at all. Every 0.2 call would 404. Upgrading is a rewrite of your call sites, not a version bump — 0.2.x still works if you have not moved yet, so migrate deliberately.
376
527
 
377
- async function startChat() {
378
- sessionRef.current = await client.startAgent("your-agent-id", { visitor: "user" });
379
- }
528
+ ### 1. Authentication: a Bearer key, not a key in the URL
380
529
 
381
- async function sendMessage() {
382
- if (!sessionRef.current) await startChat();
530
+ ```diff
531
+ - const client = new PromptevClient({ projectKey: "pv_sk_..." });
532
+ + const client = new PromptevClient({ apiKey: "fk_..." });
533
+ ```
383
534
 
384
- setMessages((prev) => [...prev, `You: ${input}`]);
385
- const query = input;
386
- setInput("");
535
+ Mint the new key in **Settings → API keys** of the project you want to reach. This is the change everything else follows from: the key is now a header, it is bound to one project, and it carries the retrieval identity the agent answers under. Your old `pv_sk_…` project key does not work against the new API.
387
536
 
388
- for await (const event of client.streamAgent(sessionRef.current.agentId, {
389
- sessionToken: sessionRef.current.sessionToken,
390
- query,
391
- })) {
392
- if (event.type === "done") {
393
- setMessages((prev) => [...prev, `Agent: ${event.output}`]);
394
- }
395
- }
396
- }
537
+ ### 2. Base URL
397
538
 
398
- return (
399
- <div>
400
- {messages.map((msg, i) => <p key={i}>{msg}</p>)}
401
- <input value={input} onChange={(e) => setInput(e.target.value)} />
402
- <button onClick={sendMessage}>Send</button>
403
- </div>
404
- );
405
- }
539
+ The default is `https://api.promptev.ai`. That host is being moved onto the new backend — until it is, pass `baseUrl: "https://console.promptev.ai"` explicitly. If you had hard-coded a `baseUrl`, remove it once the move is done, unless you are self-hosted.
540
+
541
+ ### 3. `startAgent` + `streamAgent` → `chat`
542
+
543
+ There is no session token any more. The key is the credential, and a `conversationId` is the thread.
544
+
545
+ ```diff
546
+ - const session = await client.startAgent(AGENT_ID, { visitor: "user_123" });
547
+ - for await (const ev of client.streamAgent(session.agentId, {
548
+ - sessionToken: session.sessionToken,
549
+ - query: "What is the refund policy?",
550
+ - })) {
551
+ - if (ev.type === "done") console.log(ev.output);
552
+ - }
553
+ + let conversationId: string | undefined;
554
+ + let answer = "";
555
+ + for await (const ev of client.chat("support-agent", "What is the refund policy?")) {
556
+ + if (ev.type === "conversation") conversationId = ev.id;
557
+ + if (ev.type === "token") answer += ev.content; // the answer arrives in deltas
558
+ + }
559
+ + console.log(answer);
406
560
  ```
407
561
 
408
- ### Node.js / Express API
562
+ Other differences to expect:
409
563
 
410
- ```js
411
- import express from "express";
412
- import { PromptevClient } from "@promptev/client";
564
+ - **Agents are addressed by slug**, not by a UUID from the Deploy tab.
565
+ - **Events changed shape.** There is no `event.output`; the answer arrives as `token` deltas that you concatenate, and `done` carries no text. See the [events table](#events). `thoughts` is now `thinking`; `processing` is now `step` / `tool_call` / `tool_result`.
566
+ - **`visitor` is now `onBehalfOf`**, and it does more than label a session — see [On behalf of your users](#on-behalf-of-your-users).
567
+ - **`variables` is gone.** Prompt variables were a property of the old product's templates; configure the agent's instructions in Promptev instead, or put the context in the message.
568
+ - **`approval_required` is real now.** The old SDK had no concept of a paused turn. Handle it.
413
569
 
414
- const app = express();
415
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
570
+ ### 4. `triggerAgent` → the project MCP server
416
571
 
417
- app.post("/api/ask", express.json(), async (req, res) => {
418
- const { response, sessionId } = await client.triggerAgent(
419
- "your-agent-id",
420
- req.body.question,
421
- { sessionId: req.body.sessionId } // pass back to keep memory across turns
422
- );
423
- res.json({ answer: response, sessionId });
424
- });
572
+ `triggerAgent` — one blocking call that ran the agent and returned JSON — has **no equivalent in this client**. The 1.0 chat endpoint only streams, and faking a blocking call by draining the stream would silently swallow `approval_required`, leaving your integration hung with no way to know why.
425
573
 
426
- app.listen(3000);
427
- ```
574
+ Use Promptev's **project MCP server** instead, which is exactly the shape `trigger` was:
428
575
 
429
- ### Vanilla HTML
576
+ ```
577
+ POST https://api.promptev.ai/api/v1/mcp
578
+ Authorization: Bearer fk_... ← the same key
579
+ ```
430
580
 
431
- ```html
432
- <script type="module">
433
- import { PromptevClient } from "https://cdn.jsdelivr.net/npm/@promptev/client/dist/esm/index.js";
581
+ It exposes `list_agents` and `call_agent`; `call_agent(agent, message)` runs an agent and returns its answer in one call. A gated tool comes back as `status: "pending_approval"` with an id — decide it in Promptev and call again. Any MCP client works, including n8n's MCP Client node, Zapier, and Claude.
434
582
 
435
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
583
+ If you would rather stay in this client, collect the answer yourself — and handle the pause:
436
584
 
437
- document.getElementById("ask-btn").addEventListener("click", async () => {
438
- const { response } = await client.triggerAgent("your-agent-id", "What is the refund policy?");
439
- document.getElementById("output").textContent = response;
440
- });
441
- </script>
585
+ ```ts
586
+ async function ask(slug: string, message: string, conversationId?: string) {
587
+ let answer = "";
588
+ const waiting: string[] = [];
589
+ for await (const ev of client.chat(slug, message, { conversationId })) {
590
+ if (ev.type === "conversation") conversationId = ev.id;
591
+ if (ev.type === "token") answer += ev.content;
592
+ if (ev.type === "approval_required") waiting.push(ev.tool_name);
593
+ if (ev.type === "error") throw new Error(ev.message);
594
+ }
595
+ return { answer, conversationId, waitedOn: waiting };
596
+ }
442
597
  ```
443
598
 
599
+ ### 5. What did not change
600
+
601
+ The typed error hierarchy, the retry/backoff configuration (`timeout`, `maxRetries`, `headers`), and the SSE parsing all behave as they did. `InsufficientCreditsError` (402) and `err.field` are new; nothing was removed.
602
+
444
603
  ## Building from source
445
604
 
446
605
  ```bash
447
606
  npm install
448
607
  npm run build # builds both ESM (dist/esm) and CommonJS (dist/cjs)
608
+ npm test # runs the offline test suite against the built output
449
609
  ```
450
610
 
451
611
  ## License
452
612
 
453
- This SDK is commercial software by [Promptev Inc](https://promptev.ai).
613
+ This client is commercial software by [Promptev Inc](https://promptev.ai).
454
614
 
455
615
  - Free tier use allowed
456
616
  - Production use requires an active subscription