novoagents 0.1.0-alpha.2
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/CHANGELOG.md +17 -0
- package/README.md +69 -0
- package/dist/index.d.ts +444 -0
- package/dist/index.js +752 -0
- package/package.json +40 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0-alpha.2
|
|
4
|
+
|
|
5
|
+
- Hardened the npm package surface for public beta distribution: the tarball
|
|
6
|
+
now ships compiled runtime and declaration files only, with TypeScript source
|
|
7
|
+
and source maps excluded.
|
|
8
|
+
- Added a browser-runtime guard so the SDK fails fast before a customer can
|
|
9
|
+
accidentally expose `NOVO_AGENTS_API_KEY` client-side.
|
|
10
|
+
|
|
11
|
+
## 0.1.0-alpha.1
|
|
12
|
+
|
|
13
|
+
- Added the first publishable Novo Agents SDK surface.
|
|
14
|
+
- Added `NovoAgents` with `agents` and `threads` namespaces.
|
|
15
|
+
- Added server-side streaming via `RunStream`, including async iteration, `toResponse()`, `toReadableStream()`, `reconnect()`, `cancel()`, `finalResult`, and `usage`.
|
|
16
|
+
- Added `NovoAPIError` for Stripe-shaped API errors.
|
|
17
|
+
- Defaulted the API base to `https://api.novoagents.ai`.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Novo Agents TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Server-only TypeScript client for the hosted Novo Agents API.
|
|
4
|
+
|
|
5
|
+
Customers install `novoagents`, keep `NOVO_AGENTS_API_KEY` on their server, and stream agent output through their own HTTP route. The SDK is a thin HTTP client; it does not include the private engine, Vercel Workflow, Vercel Sandbox, or provider SDKs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install novoagents@alpha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { NovoAgents } from 'novoagents';
|
|
17
|
+
|
|
18
|
+
const novo = new NovoAgents({
|
|
19
|
+
apiKey: process.env.NOVO_AGENTS_API_KEY,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const agent = await novo.agents.create({
|
|
23
|
+
identity: 'You are an expert coding agent for our platform.',
|
|
24
|
+
intelligence: 3,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const thread = await novo.threads.create({ agentId: agent.id });
|
|
28
|
+
|
|
29
|
+
const stream = await novo.threads.stream(thread.id, {
|
|
30
|
+
input: 'Fix the failing checkout tests',
|
|
31
|
+
budgetCents: 250,
|
|
32
|
+
maxSteps: 25,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for await (const event of stream) {
|
|
36
|
+
// event is NovoAgentEvent: AI SDK UIMessageChunk plus data-novo-* chunks.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = await stream.finalResult;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Browser Streaming
|
|
43
|
+
|
|
44
|
+
Do not expose Novo API keys to browsers. `novoagents` is server-only and throws
|
|
45
|
+
if constructed in a browser-like runtime; proxy the stream through your server:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { NovoAgents } from 'novoagents';
|
|
49
|
+
|
|
50
|
+
const novo = new NovoAgents();
|
|
51
|
+
|
|
52
|
+
export async function POST(req: Request) {
|
|
53
|
+
const { threadId, input } = await req.json();
|
|
54
|
+
const stream = await novo.threads.stream(threadId, { input });
|
|
55
|
+
return stream.toResponse();
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Shape
|
|
60
|
+
|
|
61
|
+
- `novo.agents.*` manages reusable agent configuration: identity, intelligence, and metadata.
|
|
62
|
+
- `novo.threads.*` manages conversation/task context and is the happy path for sending input.
|
|
63
|
+
- `novo.threads.stream(threadId, ...)` starts a run and returns a `RunStream`.
|
|
64
|
+
- `RunStream` is async iterable and also exposes `toResponse()`, `toReadableStream()`, `reconnect()`, `cancel()`, `finalResult`, and `usage`.
|
|
65
|
+
- Runs are operational receipts. The SDK exposes `threads.getRun(runId)` for status reads, but customers do not create runs directly.
|
|
66
|
+
|
|
67
|
+
Default API base: `https://api.novoagents.ai`.
|
|
68
|
+
|
|
69
|
+
Default auth env var: `NOVO_AGENTS_API_KEY`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
export type NovoAgentsOptions = {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
fetch?: typeof fetch;
|
|
6
|
+
};
|
|
7
|
+
export type Metadata = Record<string, string>;
|
|
8
|
+
export type MetadataPatch = Record<string, string | null>;
|
|
9
|
+
export type Page<T> = {
|
|
10
|
+
object: 'list';
|
|
11
|
+
data: T[];
|
|
12
|
+
hasMore: boolean;
|
|
13
|
+
nextCursor?: string;
|
|
14
|
+
};
|
|
15
|
+
export type StandardUIMessageChunk = {
|
|
16
|
+
type: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
export type NovoRunMetaChunk = {
|
|
20
|
+
type: 'data-novo-run-meta';
|
|
21
|
+
data: {
|
|
22
|
+
runId: string;
|
|
23
|
+
threadId: string;
|
|
24
|
+
agentId: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export type NovoUsageChunk = {
|
|
29
|
+
type: 'data-novo-usage';
|
|
30
|
+
data: {
|
|
31
|
+
runId: string;
|
|
32
|
+
threadId: string;
|
|
33
|
+
agentId: string;
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
cacheReadTokens: number;
|
|
37
|
+
cacheWriteTokens: number;
|
|
38
|
+
runtimeMs: number;
|
|
39
|
+
toolCallCount: number;
|
|
40
|
+
finishReason: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type NovoTerminalChunk = {
|
|
44
|
+
type: 'data-novo-terminal';
|
|
45
|
+
data: {
|
|
46
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
47
|
+
error?: RunError;
|
|
48
|
+
finishReason?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export type NovoDataChunk = NovoRunMetaChunk | NovoUsageChunk | NovoTerminalChunk;
|
|
52
|
+
export type NovoAgentEvent = StandardUIMessageChunk | NovoDataChunk;
|
|
53
|
+
export type AgentCapabilities = {
|
|
54
|
+
filesystem?: {
|
|
55
|
+
environmentId: string;
|
|
56
|
+
};
|
|
57
|
+
shell?: {
|
|
58
|
+
environmentId: string;
|
|
59
|
+
};
|
|
60
|
+
web?: boolean;
|
|
61
|
+
planning?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Enable the managed `generate_image` tool. Binary output routes through
|
|
64
|
+
* the universal `toolResultArtifacts` handler — the customer's handler
|
|
65
|
+
* does per-tool routing via the `toolName` field in the dispatch body.
|
|
66
|
+
* Requires `toolResultArtifacts` to be configured.
|
|
67
|
+
*/
|
|
68
|
+
imageGeneration?: boolean;
|
|
69
|
+
toolResultArtifacts?: {
|
|
70
|
+
resultHandlerId: string;
|
|
71
|
+
};
|
|
72
|
+
rawMcp?: {
|
|
73
|
+
toolServerIds: string[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Enable the managed `search_voices` tool — a metadata lookup against
|
|
77
|
+
* the public voice library. Inline response; requires the cloud
|
|
78
|
+
* worker to have an ElevenLabs API key configured.
|
|
79
|
+
*/
|
|
80
|
+
voiceSearch?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Enable the managed `generate_video` tool — Fal.ai Seedance 2.0 Fast
|
|
83
|
+
* Reference-to-Video. Output (an MP4 URL) routes through the universal
|
|
84
|
+
* `toolResultArtifacts` handler as a `content.kind: 'url'` artifact;
|
|
85
|
+
* the customer's handler is expected to copy the asset to its own
|
|
86
|
+
* storage before Fal's short-lived URL expires. Requires
|
|
87
|
+
* `toolResultArtifacts` to be configured.
|
|
88
|
+
*/
|
|
89
|
+
videoGeneration?: boolean;
|
|
90
|
+
skills?: AgentSkill[];
|
|
91
|
+
};
|
|
92
|
+
export type AgentSkill = {
|
|
93
|
+
name: string;
|
|
94
|
+
description: string;
|
|
95
|
+
body: string;
|
|
96
|
+
frontmatter?: Record<string, unknown>;
|
|
97
|
+
metadata?: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
export type AgentEventsInput = {
|
|
100
|
+
eventSinkId: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: 'remote';
|
|
103
|
+
url: string;
|
|
104
|
+
secret: string;
|
|
105
|
+
};
|
|
106
|
+
export type AgentPolicies = {
|
|
107
|
+
maxStepsDefault?: number;
|
|
108
|
+
maxDurationMsDefault?: number;
|
|
109
|
+
mediaFidelity?: 'standard' | 'spatial';
|
|
110
|
+
};
|
|
111
|
+
export type Agent = {
|
|
112
|
+
id: string;
|
|
113
|
+
object: 'agent';
|
|
114
|
+
status: 'active' | 'deleting';
|
|
115
|
+
identity: string;
|
|
116
|
+
intelligence: 1 | 2 | 3 | 4 | 5;
|
|
117
|
+
capabilities: AgentCapabilities;
|
|
118
|
+
events: {
|
|
119
|
+
eventSinkId: string;
|
|
120
|
+
} | null;
|
|
121
|
+
policies: AgentPolicies;
|
|
122
|
+
metadata: Metadata;
|
|
123
|
+
configVersion: number;
|
|
124
|
+
createdAt: string;
|
|
125
|
+
updatedAt: string;
|
|
126
|
+
};
|
|
127
|
+
export type Thread = {
|
|
128
|
+
id: string;
|
|
129
|
+
object: 'thread';
|
|
130
|
+
agentId: string;
|
|
131
|
+
status: 'idle' | 'running' | 'deleting';
|
|
132
|
+
metadata: Metadata;
|
|
133
|
+
createdAt: string;
|
|
134
|
+
updatedAt: string;
|
|
135
|
+
};
|
|
136
|
+
export type UsageTotal = {
|
|
137
|
+
inputTokens: number;
|
|
138
|
+
outputTokens: number;
|
|
139
|
+
cacheReadTokens: number;
|
|
140
|
+
cacheWriteTokens: number;
|
|
141
|
+
runtimeMs: number;
|
|
142
|
+
toolCallCount: number;
|
|
143
|
+
};
|
|
144
|
+
export type RunError = {
|
|
145
|
+
type: string;
|
|
146
|
+
code: string;
|
|
147
|
+
message: string;
|
|
148
|
+
};
|
|
149
|
+
export type Run = {
|
|
150
|
+
id: string;
|
|
151
|
+
object: 'run';
|
|
152
|
+
threadId: string;
|
|
153
|
+
agentId: string;
|
|
154
|
+
agentConfigSnapshot: {
|
|
155
|
+
identity: string;
|
|
156
|
+
intelligence: 1 | 2 | 3 | 4 | 5;
|
|
157
|
+
configVersion: number;
|
|
158
|
+
};
|
|
159
|
+
status: 'queued' | 'preparing' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
160
|
+
finishReason?: string;
|
|
161
|
+
usage?: UsageTotal;
|
|
162
|
+
error?: RunError;
|
|
163
|
+
metadata: Metadata;
|
|
164
|
+
createdAt: string;
|
|
165
|
+
startedAt?: string;
|
|
166
|
+
completedAt?: string;
|
|
167
|
+
};
|
|
168
|
+
export type Environment = {
|
|
169
|
+
id: string;
|
|
170
|
+
object: 'environment';
|
|
171
|
+
name: string;
|
|
172
|
+
type: 'http_workspace';
|
|
173
|
+
url: string;
|
|
174
|
+
metadata: Metadata;
|
|
175
|
+
configVersion: number;
|
|
176
|
+
createdAt: string;
|
|
177
|
+
updatedAt: string;
|
|
178
|
+
};
|
|
179
|
+
export type ResultHandler = {
|
|
180
|
+
id: string;
|
|
181
|
+
object: 'result_handler';
|
|
182
|
+
name: string;
|
|
183
|
+
/**
|
|
184
|
+
* Always `'artifact'` after the universal-handler collapse. One customer
|
|
185
|
+
* endpoint receives the binary output of every managed tool that emits
|
|
186
|
+
* one; per-tool routing happens inside the customer's handler via the
|
|
187
|
+
* `toolName` field in the dispatch body.
|
|
188
|
+
*/
|
|
189
|
+
kind: 'artifact';
|
|
190
|
+
url: string;
|
|
191
|
+
metadata: Metadata;
|
|
192
|
+
configVersion: number;
|
|
193
|
+
createdAt: string;
|
|
194
|
+
updatedAt: string;
|
|
195
|
+
};
|
|
196
|
+
export type EventSink = {
|
|
197
|
+
id: string;
|
|
198
|
+
object: 'event_sink';
|
|
199
|
+
name: string;
|
|
200
|
+
url: string;
|
|
201
|
+
enabled: boolean;
|
|
202
|
+
deliveryMode: 'required' | 'best_effort';
|
|
203
|
+
metadata: Metadata;
|
|
204
|
+
configVersion: number;
|
|
205
|
+
createdAt: string;
|
|
206
|
+
updatedAt: string;
|
|
207
|
+
};
|
|
208
|
+
export type ToolServer = {
|
|
209
|
+
id: string;
|
|
210
|
+
object: 'tool_server';
|
|
211
|
+
name: string;
|
|
212
|
+
transport: 'http' | 'sse';
|
|
213
|
+
url: string;
|
|
214
|
+
allowedTools?: string[];
|
|
215
|
+
resultCapBytes?: number;
|
|
216
|
+
protocolVersion: string;
|
|
217
|
+
trusted: boolean;
|
|
218
|
+
metadata: Metadata;
|
|
219
|
+
configVersion: number;
|
|
220
|
+
createdAt: string;
|
|
221
|
+
updatedAt: string;
|
|
222
|
+
};
|
|
223
|
+
export type DeleteAgentResponse = {
|
|
224
|
+
id: string;
|
|
225
|
+
object: 'agent';
|
|
226
|
+
status: 'deleting';
|
|
227
|
+
};
|
|
228
|
+
export type DeleteThreadResponse = {
|
|
229
|
+
id: string;
|
|
230
|
+
object: 'thread';
|
|
231
|
+
status: 'deleting';
|
|
232
|
+
};
|
|
233
|
+
export type CancelThreadResponse = {
|
|
234
|
+
object: 'thread.cancel';
|
|
235
|
+
threadId: string;
|
|
236
|
+
runId: string | null;
|
|
237
|
+
status: 'cancelling' | 'idle';
|
|
238
|
+
};
|
|
239
|
+
export type RunFinalResult = {
|
|
240
|
+
runId: string;
|
|
241
|
+
threadId: string;
|
|
242
|
+
agentId: string;
|
|
243
|
+
text: string;
|
|
244
|
+
finishReason: string;
|
|
245
|
+
usage: UsageTotal;
|
|
246
|
+
};
|
|
247
|
+
export type RunStream = AsyncIterable<NovoAgentEvent> & {
|
|
248
|
+
runId: Promise<string>;
|
|
249
|
+
threadId: string;
|
|
250
|
+
agentId: Promise<string>;
|
|
251
|
+
finalResult: Promise<RunFinalResult>;
|
|
252
|
+
usage: Promise<UsageTotal>;
|
|
253
|
+
close(): void;
|
|
254
|
+
cancel(): Promise<void>;
|
|
255
|
+
reconnect(startIndex?: number): Promise<RunStream>;
|
|
256
|
+
toResponse(init?: ResponseInit): Response;
|
|
257
|
+
toReadableStream(): ReadableStream<Uint8Array>;
|
|
258
|
+
};
|
|
259
|
+
export type AgentsNamespace = {
|
|
260
|
+
list(input?: {
|
|
261
|
+
limit?: number;
|
|
262
|
+
cursor?: string;
|
|
263
|
+
}): Promise<Page<Agent>>;
|
|
264
|
+
create(input: {
|
|
265
|
+
identity: string;
|
|
266
|
+
intelligence?: 1 | 2 | 3 | 4 | 5;
|
|
267
|
+
capabilities?: AgentCapabilities;
|
|
268
|
+
events?: AgentEventsInput;
|
|
269
|
+
policies?: AgentPolicies;
|
|
270
|
+
metadata?: Metadata;
|
|
271
|
+
}): Promise<Agent>;
|
|
272
|
+
get(agentId: string): Promise<Agent>;
|
|
273
|
+
update(agentId: string, input: {
|
|
274
|
+
identity?: string;
|
|
275
|
+
intelligence?: 1 | 2 | 3 | 4 | 5;
|
|
276
|
+
capabilities?: AgentCapabilities;
|
|
277
|
+
events?: AgentEventsInput | null;
|
|
278
|
+
policies?: {
|
|
279
|
+
maxStepsDefault?: number | null;
|
|
280
|
+
maxDurationMsDefault?: number | null;
|
|
281
|
+
mediaFidelity?: 'standard' | 'spatial' | null;
|
|
282
|
+
};
|
|
283
|
+
metadata?: MetadataPatch;
|
|
284
|
+
}): Promise<Agent>;
|
|
285
|
+
delete(agentId: string): Promise<DeleteAgentResponse>;
|
|
286
|
+
};
|
|
287
|
+
export type EnvironmentsNamespace = {
|
|
288
|
+
register(input: {
|
|
289
|
+
type: 'http_workspace';
|
|
290
|
+
name: string;
|
|
291
|
+
url: string;
|
|
292
|
+
secret: string;
|
|
293
|
+
metadata?: Metadata;
|
|
294
|
+
}): Promise<Environment>;
|
|
295
|
+
list(): Promise<Page<Environment>>;
|
|
296
|
+
get(environmentId: string): Promise<Environment>;
|
|
297
|
+
delete(environmentId: string): Promise<{
|
|
298
|
+
id: string;
|
|
299
|
+
object: 'environment';
|
|
300
|
+
deleted: true;
|
|
301
|
+
}>;
|
|
302
|
+
rotateSecret(environmentId: string, input: {
|
|
303
|
+
secret: string;
|
|
304
|
+
}): Promise<Environment>;
|
|
305
|
+
};
|
|
306
|
+
export type ResultHandlersNamespace = {
|
|
307
|
+
register(input: {
|
|
308
|
+
kind: 'artifact';
|
|
309
|
+
name: string;
|
|
310
|
+
url: string;
|
|
311
|
+
secret: string;
|
|
312
|
+
metadata?: Metadata;
|
|
313
|
+
}): Promise<ResultHandler>;
|
|
314
|
+
list(): Promise<Page<ResultHandler>>;
|
|
315
|
+
get(resultHandlerId: string): Promise<ResultHandler>;
|
|
316
|
+
delete(resultHandlerId: string): Promise<{
|
|
317
|
+
id: string;
|
|
318
|
+
object: 'result_handler';
|
|
319
|
+
deleted: true;
|
|
320
|
+
}>;
|
|
321
|
+
rotateSecret(resultHandlerId: string, input: {
|
|
322
|
+
secret: string;
|
|
323
|
+
}): Promise<ResultHandler>;
|
|
324
|
+
};
|
|
325
|
+
export type EventSinksNamespace = {
|
|
326
|
+
register(input: {
|
|
327
|
+
name: string;
|
|
328
|
+
url: string;
|
|
329
|
+
secret: string;
|
|
330
|
+
deliveryMode?: 'required' | 'best_effort';
|
|
331
|
+
metadata?: Metadata;
|
|
332
|
+
}): Promise<EventSink>;
|
|
333
|
+
list(): Promise<Page<EventSink>>;
|
|
334
|
+
get(eventSinkId: string): Promise<EventSink>;
|
|
335
|
+
delete(eventSinkId: string): Promise<{
|
|
336
|
+
id: string;
|
|
337
|
+
object: 'event_sink';
|
|
338
|
+
deleted: true;
|
|
339
|
+
}>;
|
|
340
|
+
rotateSecret(eventSinkId: string, input: {
|
|
341
|
+
secret: string;
|
|
342
|
+
}): Promise<EventSink>;
|
|
343
|
+
test(eventSinkId: string): Promise<{
|
|
344
|
+
delivered: true;
|
|
345
|
+
latencyMs: number;
|
|
346
|
+
}>;
|
|
347
|
+
};
|
|
348
|
+
export type ToolServersNamespace = {
|
|
349
|
+
register(input: {
|
|
350
|
+
name: string;
|
|
351
|
+
transport: 'http' | 'sse';
|
|
352
|
+
url: string;
|
|
353
|
+
secret: string;
|
|
354
|
+
allowedTools?: string[];
|
|
355
|
+
resultCapBytes?: number;
|
|
356
|
+
protocolVersion?: '2025-11-25';
|
|
357
|
+
trusted?: boolean;
|
|
358
|
+
metadata?: Metadata;
|
|
359
|
+
}): Promise<ToolServer>;
|
|
360
|
+
list(): Promise<Page<ToolServer>>;
|
|
361
|
+
get(toolServerId: string): Promise<ToolServer>;
|
|
362
|
+
delete(toolServerId: string): Promise<{
|
|
363
|
+
id: string;
|
|
364
|
+
object: 'tool_server';
|
|
365
|
+
deleted: true;
|
|
366
|
+
}>;
|
|
367
|
+
rotateSecret(toolServerId: string, input: {
|
|
368
|
+
secret: string;
|
|
369
|
+
}): Promise<ToolServer>;
|
|
370
|
+
};
|
|
371
|
+
export type ThreadsNamespace = {
|
|
372
|
+
list(input: {
|
|
373
|
+
agentId: string;
|
|
374
|
+
limit?: number;
|
|
375
|
+
cursor?: string;
|
|
376
|
+
}): Promise<Page<Thread>>;
|
|
377
|
+
create(input: {
|
|
378
|
+
agentId: string;
|
|
379
|
+
metadata?: Metadata;
|
|
380
|
+
}): Promise<Thread>;
|
|
381
|
+
get(threadId: string): Promise<Thread>;
|
|
382
|
+
update(threadId: string, input: {
|
|
383
|
+
metadata?: MetadataPatch;
|
|
384
|
+
}): Promise<Thread>;
|
|
385
|
+
delete(threadId: string): Promise<DeleteThreadResponse>;
|
|
386
|
+
stream(threadId: string, input: {
|
|
387
|
+
input: string;
|
|
388
|
+
budgetCents?: number;
|
|
389
|
+
maxSteps?: number;
|
|
390
|
+
maxDurationMs?: number;
|
|
391
|
+
metadata?: Metadata;
|
|
392
|
+
idempotencyKey?: string;
|
|
393
|
+
}): Promise<RunStream>;
|
|
394
|
+
send(threadId: string, input: {
|
|
395
|
+
input: string;
|
|
396
|
+
budgetCents?: number;
|
|
397
|
+
maxSteps?: number;
|
|
398
|
+
maxDurationMs?: number;
|
|
399
|
+
metadata?: Metadata;
|
|
400
|
+
idempotencyKey?: string;
|
|
401
|
+
}): Promise<Run>;
|
|
402
|
+
cancel(threadId: string): Promise<CancelThreadResponse>;
|
|
403
|
+
getRun(runId: string): Promise<Run>;
|
|
404
|
+
};
|
|
405
|
+
export declare class NovoAPIError extends Error {
|
|
406
|
+
readonly type: string;
|
|
407
|
+
readonly code: string;
|
|
408
|
+
readonly requestId: string;
|
|
409
|
+
readonly status: number;
|
|
410
|
+
constructor(input: {
|
|
411
|
+
type: string;
|
|
412
|
+
code: string;
|
|
413
|
+
message: string;
|
|
414
|
+
requestId: string;
|
|
415
|
+
status: number;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
export declare class NovoAgents {
|
|
419
|
+
readonly apiKey: string;
|
|
420
|
+
readonly baseUrl: string;
|
|
421
|
+
readonly version: string;
|
|
422
|
+
readonly fetch: typeof fetch;
|
|
423
|
+
readonly agents: AgentsNamespace;
|
|
424
|
+
readonly threads: ThreadsNamespace;
|
|
425
|
+
readonly environments: EnvironmentsNamespace;
|
|
426
|
+
readonly resultHandlers: ResultHandlersNamespace;
|
|
427
|
+
readonly eventSinks: EventSinksNamespace;
|
|
428
|
+
readonly toolServers: ToolServersNamespace;
|
|
429
|
+
private readonly client;
|
|
430
|
+
constructor(options?: NovoAgentsOptions);
|
|
431
|
+
}
|
|
432
|
+
export declare function verifyNovoSignature(input: {
|
|
433
|
+
rawBody: string;
|
|
434
|
+
signatureHeader: string;
|
|
435
|
+
secret: string;
|
|
436
|
+
toleranceSeconds?: number;
|
|
437
|
+
}): {
|
|
438
|
+
valid: true;
|
|
439
|
+
timestamp: number;
|
|
440
|
+
} | {
|
|
441
|
+
valid: false;
|
|
442
|
+
reason: string;
|
|
443
|
+
};
|
|
444
|
+
export declare const verifyEventSinkSignature: typeof verifyNovoSignature;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.novoagents.ai';
|
|
3
|
+
const DEFAULT_VERSION = '2026-05-17';
|
|
4
|
+
const TERMINAL_RUN_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
5
|
+
const EMPTY_USAGE = {
|
|
6
|
+
inputTokens: 0,
|
|
7
|
+
outputTokens: 0,
|
|
8
|
+
cacheReadTokens: 0,
|
|
9
|
+
cacheWriteTokens: 0,
|
|
10
|
+
runtimeMs: 0,
|
|
11
|
+
toolCallCount: 0,
|
|
12
|
+
};
|
|
13
|
+
export class NovoAPIError extends Error {
|
|
14
|
+
type;
|
|
15
|
+
code;
|
|
16
|
+
requestId;
|
|
17
|
+
status;
|
|
18
|
+
constructor(input) {
|
|
19
|
+
super(input.message);
|
|
20
|
+
this.name = 'NovoAPIError';
|
|
21
|
+
this.type = input.type;
|
|
22
|
+
this.code = input.code;
|
|
23
|
+
this.requestId = input.requestId;
|
|
24
|
+
this.status = input.status;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class NovoAgents {
|
|
28
|
+
apiKey;
|
|
29
|
+
baseUrl;
|
|
30
|
+
version;
|
|
31
|
+
fetch;
|
|
32
|
+
agents;
|
|
33
|
+
threads;
|
|
34
|
+
environments;
|
|
35
|
+
resultHandlers;
|
|
36
|
+
eventSinks;
|
|
37
|
+
toolServers;
|
|
38
|
+
client;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
throw new Error('novoagents is server-only; proxy streams through your server route.');
|
|
42
|
+
}
|
|
43
|
+
const apiKey = options.apiKey ?? process.env.NOVO_AGENTS_API_KEY;
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
throw new Error('NovoAgents requires apiKey or NOVO_AGENTS_API_KEY.');
|
|
46
|
+
}
|
|
47
|
+
this.apiKey = apiKey;
|
|
48
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
49
|
+
this.version = options.version ?? DEFAULT_VERSION;
|
|
50
|
+
this.fetch = options.fetch ?? fetch;
|
|
51
|
+
this.client = new NovoHttpClient({
|
|
52
|
+
apiKey: this.apiKey,
|
|
53
|
+
baseUrl: this.baseUrl,
|
|
54
|
+
version: this.version,
|
|
55
|
+
fetch: this.fetch,
|
|
56
|
+
});
|
|
57
|
+
this.agents = createAgentsNamespace(this.client);
|
|
58
|
+
this.threads = createThreadsNamespace(this.client);
|
|
59
|
+
this.environments = createEnvironmentsNamespace(this.client);
|
|
60
|
+
this.resultHandlers = createResultHandlersNamespace(this.client);
|
|
61
|
+
this.eventSinks = createEventSinksNamespace(this.client);
|
|
62
|
+
this.toolServers = createToolServersNamespace(this.client);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
class NovoHttpClient {
|
|
66
|
+
baseUrl;
|
|
67
|
+
apiKey;
|
|
68
|
+
version;
|
|
69
|
+
fetchImpl;
|
|
70
|
+
constructor(input) {
|
|
71
|
+
this.apiKey = input.apiKey;
|
|
72
|
+
this.baseUrl = input.baseUrl;
|
|
73
|
+
this.version = input.version;
|
|
74
|
+
this.fetchImpl = input.fetch;
|
|
75
|
+
}
|
|
76
|
+
async json(options) {
|
|
77
|
+
const response = await this.raw({
|
|
78
|
+
...options,
|
|
79
|
+
headers: {
|
|
80
|
+
Accept: 'application/json',
|
|
81
|
+
...options.headers,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
await assertOk(response);
|
|
85
|
+
return (await response.json());
|
|
86
|
+
}
|
|
87
|
+
async stream(options) {
|
|
88
|
+
const response = await this.raw({
|
|
89
|
+
...options,
|
|
90
|
+
headers: {
|
|
91
|
+
Accept: 'text/event-stream',
|
|
92
|
+
...options.headers,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
await assertOk(response);
|
|
96
|
+
if (!response.body) {
|
|
97
|
+
throw new NovoAPIError({
|
|
98
|
+
type: 'api_error',
|
|
99
|
+
code: 'internal_error',
|
|
100
|
+
message: 'Stream response did not include a body.',
|
|
101
|
+
requestId: response.headers.get('X-Request-Id') ?? '',
|
|
102
|
+
status: response.status,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
async raw(options) {
|
|
108
|
+
const headers = {
|
|
109
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
110
|
+
'Novo-Version': this.version,
|
|
111
|
+
};
|
|
112
|
+
for (const [key, value] of Object.entries(options.headers ?? {})) {
|
|
113
|
+
if (value !== undefined)
|
|
114
|
+
headers[key] = value;
|
|
115
|
+
}
|
|
116
|
+
let body;
|
|
117
|
+
if (options.body !== undefined) {
|
|
118
|
+
headers['Content-Type'] = 'application/json';
|
|
119
|
+
body = JSON.stringify(options.body);
|
|
120
|
+
}
|
|
121
|
+
return this.fetchImpl(this.url(options.path, options.query), {
|
|
122
|
+
method: options.method ?? 'GET',
|
|
123
|
+
headers,
|
|
124
|
+
body,
|
|
125
|
+
signal: options.signal,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
url(path, query) {
|
|
129
|
+
const url = new URL(`${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
130
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
131
|
+
if (value !== undefined)
|
|
132
|
+
url.searchParams.set(key, String(value));
|
|
133
|
+
}
|
|
134
|
+
return url.toString();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function createAgentsNamespace(client) {
|
|
138
|
+
return {
|
|
139
|
+
list(input = {}) {
|
|
140
|
+
return client.json({
|
|
141
|
+
path: '/v1/agents',
|
|
142
|
+
query: input,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
create(input) {
|
|
146
|
+
return client.json({
|
|
147
|
+
method: 'POST',
|
|
148
|
+
path: '/v1/agents',
|
|
149
|
+
body: input,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
get(agentId) {
|
|
153
|
+
return client.json({ path: `/v1/agents/${encodeURIComponent(agentId)}` });
|
|
154
|
+
},
|
|
155
|
+
update(agentId, input) {
|
|
156
|
+
return client.json({
|
|
157
|
+
method: 'PATCH',
|
|
158
|
+
path: `/v1/agents/${encodeURIComponent(agentId)}`,
|
|
159
|
+
body: input,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
delete(agentId) {
|
|
163
|
+
return client.json({
|
|
164
|
+
method: 'DELETE',
|
|
165
|
+
path: `/v1/agents/${encodeURIComponent(agentId)}`,
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function createThreadsNamespace(client) {
|
|
171
|
+
return {
|
|
172
|
+
list(input) {
|
|
173
|
+
return client.json({
|
|
174
|
+
path: '/v1/threads',
|
|
175
|
+
query: input,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
create(input) {
|
|
179
|
+
return client.json({
|
|
180
|
+
method: 'POST',
|
|
181
|
+
path: '/v1/threads',
|
|
182
|
+
body: input,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
get(threadId) {
|
|
186
|
+
return client.json({ path: `/v1/threads/${encodeURIComponent(threadId)}` });
|
|
187
|
+
},
|
|
188
|
+
update(threadId, input) {
|
|
189
|
+
return client.json({
|
|
190
|
+
method: 'PATCH',
|
|
191
|
+
path: `/v1/threads/${encodeURIComponent(threadId)}`,
|
|
192
|
+
body: input,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
delete(threadId) {
|
|
196
|
+
return client.json({
|
|
197
|
+
method: 'DELETE',
|
|
198
|
+
path: `/v1/threads/${encodeURIComponent(threadId)}`,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
async stream(threadId, input) {
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const response = await client.stream({
|
|
204
|
+
method: 'POST',
|
|
205
|
+
path: `/v1/threads/${encodeURIComponent(threadId)}/messages`,
|
|
206
|
+
body: stripIdempotencyKey(input),
|
|
207
|
+
signal: controller.signal,
|
|
208
|
+
headers: {
|
|
209
|
+
...(input.idempotencyKey ? { 'Idempotency-Key': input.idempotencyKey } : {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return new NovoRunStream({
|
|
213
|
+
client,
|
|
214
|
+
threadId,
|
|
215
|
+
body: response.body,
|
|
216
|
+
abortController: controller,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
async send(threadId, input) {
|
|
220
|
+
const receipt = await client.json({
|
|
221
|
+
method: 'POST',
|
|
222
|
+
path: `/v1/threads/${encodeURIComponent(threadId)}/messages`,
|
|
223
|
+
body: stripIdempotencyKey(input),
|
|
224
|
+
headers: {
|
|
225
|
+
...(input.idempotencyKey ? { 'Idempotency-Key': input.idempotencyKey } : {}),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
return client.json({ path: `/v1/runs/${encodeURIComponent(receipt.id)}` });
|
|
229
|
+
},
|
|
230
|
+
cancel(threadId) {
|
|
231
|
+
return client.json({
|
|
232
|
+
method: 'POST',
|
|
233
|
+
path: `/v1/threads/${encodeURIComponent(threadId)}/cancel`,
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
getRun(runId) {
|
|
237
|
+
return client.json({ path: `/v1/runs/${encodeURIComponent(runId)}` });
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function createEnvironmentsNamespace(client) {
|
|
242
|
+
return {
|
|
243
|
+
register(input) {
|
|
244
|
+
return client.json({ method: 'POST', path: '/v1/environments', body: input });
|
|
245
|
+
},
|
|
246
|
+
list() {
|
|
247
|
+
return client.json({ path: '/v1/environments' });
|
|
248
|
+
},
|
|
249
|
+
get(environmentId) {
|
|
250
|
+
return client.json({ path: `/v1/environments/${encodeURIComponent(environmentId)}` });
|
|
251
|
+
},
|
|
252
|
+
delete(environmentId) {
|
|
253
|
+
return client.json({
|
|
254
|
+
method: 'DELETE',
|
|
255
|
+
path: `/v1/environments/${encodeURIComponent(environmentId)}`,
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
rotateSecret(environmentId, input) {
|
|
259
|
+
return client.json({
|
|
260
|
+
method: 'POST',
|
|
261
|
+
path: `/v1/environments/${encodeURIComponent(environmentId)}/rotate-secret`,
|
|
262
|
+
body: input,
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function createResultHandlersNamespace(client) {
|
|
268
|
+
return {
|
|
269
|
+
register(input) {
|
|
270
|
+
return client.json({ method: 'POST', path: '/v1/result-handlers', body: input });
|
|
271
|
+
},
|
|
272
|
+
list() {
|
|
273
|
+
return client.json({ path: '/v1/result-handlers' });
|
|
274
|
+
},
|
|
275
|
+
get(resultHandlerId) {
|
|
276
|
+
return client.json({ path: `/v1/result-handlers/${encodeURIComponent(resultHandlerId)}` });
|
|
277
|
+
},
|
|
278
|
+
delete(resultHandlerId) {
|
|
279
|
+
return client.json({
|
|
280
|
+
method: 'DELETE',
|
|
281
|
+
path: `/v1/result-handlers/${encodeURIComponent(resultHandlerId)}`,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
rotateSecret(resultHandlerId, input) {
|
|
285
|
+
return client.json({
|
|
286
|
+
method: 'POST',
|
|
287
|
+
path: `/v1/result-handlers/${encodeURIComponent(resultHandlerId)}/rotate-secret`,
|
|
288
|
+
body: input,
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function createEventSinksNamespace(client) {
|
|
294
|
+
return {
|
|
295
|
+
register(input) {
|
|
296
|
+
return client.json({ method: 'POST', path: '/v1/event-sinks', body: input });
|
|
297
|
+
},
|
|
298
|
+
list() {
|
|
299
|
+
return client.json({ path: '/v1/event-sinks' });
|
|
300
|
+
},
|
|
301
|
+
get(eventSinkId) {
|
|
302
|
+
return client.json({ path: `/v1/event-sinks/${encodeURIComponent(eventSinkId)}` });
|
|
303
|
+
},
|
|
304
|
+
delete(eventSinkId) {
|
|
305
|
+
return client.json({
|
|
306
|
+
method: 'DELETE',
|
|
307
|
+
path: `/v1/event-sinks/${encodeURIComponent(eventSinkId)}`,
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
rotateSecret(eventSinkId, input) {
|
|
311
|
+
return client.json({
|
|
312
|
+
method: 'POST',
|
|
313
|
+
path: `/v1/event-sinks/${encodeURIComponent(eventSinkId)}/rotate-secret`,
|
|
314
|
+
body: input,
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
test(eventSinkId) {
|
|
318
|
+
return client.json({
|
|
319
|
+
method: 'POST',
|
|
320
|
+
path: `/v1/event-sinks/${encodeURIComponent(eventSinkId)}/test`,
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function createToolServersNamespace(client) {
|
|
326
|
+
return {
|
|
327
|
+
register(input) {
|
|
328
|
+
return client.json({ method: 'POST', path: '/v1/tool-servers', body: input });
|
|
329
|
+
},
|
|
330
|
+
list() {
|
|
331
|
+
return client.json({ path: '/v1/tool-servers' });
|
|
332
|
+
},
|
|
333
|
+
get(toolServerId) {
|
|
334
|
+
return client.json({ path: `/v1/tool-servers/${encodeURIComponent(toolServerId)}` });
|
|
335
|
+
},
|
|
336
|
+
delete(toolServerId) {
|
|
337
|
+
return client.json({
|
|
338
|
+
method: 'DELETE',
|
|
339
|
+
path: `/v1/tool-servers/${encodeURIComponent(toolServerId)}`,
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
rotateSecret(toolServerId, input) {
|
|
343
|
+
return client.json({
|
|
344
|
+
method: 'POST',
|
|
345
|
+
path: `/v1/tool-servers/${encodeURIComponent(toolServerId)}/rotate-secret`,
|
|
346
|
+
body: input,
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
class NovoRunStream {
|
|
352
|
+
threadId;
|
|
353
|
+
runId;
|
|
354
|
+
agentId;
|
|
355
|
+
client;
|
|
356
|
+
abortController;
|
|
357
|
+
body;
|
|
358
|
+
consumed = false;
|
|
359
|
+
decoder = new TextDecoder();
|
|
360
|
+
sse = new SSEDecoder();
|
|
361
|
+
textParts = [];
|
|
362
|
+
observedFinishReason;
|
|
363
|
+
observedUsage;
|
|
364
|
+
finalResultPromise;
|
|
365
|
+
activeReader;
|
|
366
|
+
activeIterator;
|
|
367
|
+
complete;
|
|
368
|
+
fail;
|
|
369
|
+
completion;
|
|
370
|
+
runIdDeferred = deferred();
|
|
371
|
+
agentIdDeferred = deferred();
|
|
372
|
+
constructor(input) {
|
|
373
|
+
this.client = input.client;
|
|
374
|
+
this.threadId = input.threadId;
|
|
375
|
+
this.body = input.body;
|
|
376
|
+
this.abortController = input.abortController;
|
|
377
|
+
this.runId = this.runIdDeferred.promise;
|
|
378
|
+
this.agentId = this.agentIdDeferred.promise;
|
|
379
|
+
this.completion = new Promise((resolve, reject) => {
|
|
380
|
+
this.complete = resolve;
|
|
381
|
+
this.fail = reject;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
get finalResult() {
|
|
385
|
+
this.finalResultPromise ??= this.buildFinalResult();
|
|
386
|
+
return this.finalResultPromise;
|
|
387
|
+
}
|
|
388
|
+
get usage() {
|
|
389
|
+
return this.finalResult.then((result) => result.usage);
|
|
390
|
+
}
|
|
391
|
+
[Symbol.asyncIterator]() {
|
|
392
|
+
const body = this.takeBody();
|
|
393
|
+
const reader = body.getReader();
|
|
394
|
+
this.activeReader = reader;
|
|
395
|
+
const iterator = this.iterateBody(reader);
|
|
396
|
+
this.activeIterator = iterator;
|
|
397
|
+
return iterator;
|
|
398
|
+
}
|
|
399
|
+
async *iterateBody(reader) {
|
|
400
|
+
try {
|
|
401
|
+
while (true) {
|
|
402
|
+
const next = await reader.read();
|
|
403
|
+
if (next.done)
|
|
404
|
+
break;
|
|
405
|
+
const chunks = this.decodeChunk(next.value);
|
|
406
|
+
for (const chunk of chunks) {
|
|
407
|
+
this.observe(chunk);
|
|
408
|
+
yield chunk;
|
|
409
|
+
}
|
|
410
|
+
if (this.sse.done)
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
const chunks = this.flushDecoder();
|
|
414
|
+
for (const chunk of chunks) {
|
|
415
|
+
this.observe(chunk);
|
|
416
|
+
yield chunk;
|
|
417
|
+
}
|
|
418
|
+
this.complete();
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
this.fail(error);
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (this.activeReader === reader) {
|
|
426
|
+
this.activeReader = undefined;
|
|
427
|
+
}
|
|
428
|
+
if (this.activeIterator) {
|
|
429
|
+
this.activeIterator = undefined;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await reader.cancel();
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// The stream may already be closed by the server.
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
reader.releaseLock();
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// `close()` may have force-released the lock for a paused iterator.
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
close() {
|
|
446
|
+
this.abortController.abort();
|
|
447
|
+
void this.activeIterator?.return?.(undefined).catch(() => undefined);
|
|
448
|
+
if (!this.activeIterator && this.activeReader) {
|
|
449
|
+
const reader = this.activeReader;
|
|
450
|
+
void reader.cancel().catch(() => undefined);
|
|
451
|
+
try {
|
|
452
|
+
reader.releaseLock();
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// The reader may already be released by a concurrently finishing consumer.
|
|
456
|
+
}
|
|
457
|
+
this.activeReader = undefined;
|
|
458
|
+
}
|
|
459
|
+
this.complete();
|
|
460
|
+
}
|
|
461
|
+
async cancel() {
|
|
462
|
+
await this.client.json({
|
|
463
|
+
method: 'POST',
|
|
464
|
+
path: `/v1/threads/${encodeURIComponent(this.threadId)}/cancel`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async reconnect(startIndex = 0) {
|
|
468
|
+
const runId = await this.runId;
|
|
469
|
+
const controller = new AbortController();
|
|
470
|
+
const response = await this.client.stream({
|
|
471
|
+
path: `/v1/threads/${encodeURIComponent(this.threadId)}/events`,
|
|
472
|
+
query: { runId, startIndex },
|
|
473
|
+
signal: controller.signal,
|
|
474
|
+
});
|
|
475
|
+
return new NovoRunStream({
|
|
476
|
+
client: this.client,
|
|
477
|
+
threadId: this.threadId,
|
|
478
|
+
body: response.body,
|
|
479
|
+
abortController: controller,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
toReadableStream() {
|
|
483
|
+
const body = this.takeBody();
|
|
484
|
+
const reader = body.getReader();
|
|
485
|
+
this.activeReader = reader;
|
|
486
|
+
return new ReadableStream({
|
|
487
|
+
start: async (controller) => {
|
|
488
|
+
try {
|
|
489
|
+
while (true) {
|
|
490
|
+
const next = await reader.read();
|
|
491
|
+
if (next.done)
|
|
492
|
+
break;
|
|
493
|
+
for (const chunk of this.decodeChunk(next.value)) {
|
|
494
|
+
this.observe(chunk);
|
|
495
|
+
}
|
|
496
|
+
controller.enqueue(next.value);
|
|
497
|
+
if (this.sse.done)
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
for (const chunk of this.flushDecoder()) {
|
|
501
|
+
this.observe(chunk);
|
|
502
|
+
}
|
|
503
|
+
controller.close();
|
|
504
|
+
this.complete();
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
controller.error(error);
|
|
508
|
+
this.fail(error);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
if (this.activeReader === reader) {
|
|
512
|
+
this.activeReader = undefined;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
reader.releaseLock();
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// `close()` may have force-released the lock for a paused proxy reader.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
cancel: async (reason) => {
|
|
523
|
+
await reader.cancel(reason);
|
|
524
|
+
this.close();
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
toResponse(init = {}) {
|
|
529
|
+
const headers = new Headers(init.headers);
|
|
530
|
+
headers.set('Content-Type', headers.get('Content-Type') ?? 'text/event-stream; charset=utf-8');
|
|
531
|
+
headers.set('Cache-Control', headers.get('Cache-Control') ?? 'no-cache, no-transform');
|
|
532
|
+
headers.set('Connection', headers.get('Connection') ?? 'keep-alive');
|
|
533
|
+
headers.set('x-vercel-ai-ui-message-stream', headers.get('x-vercel-ai-ui-message-stream') ?? 'v1');
|
|
534
|
+
return new Response(this.toReadableStream(), {
|
|
535
|
+
...init,
|
|
536
|
+
headers,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
takeBody() {
|
|
540
|
+
if (this.consumed || !this.body) {
|
|
541
|
+
throw new Error('RunStream is single-consumer; create a reconnect stream for another reader.');
|
|
542
|
+
}
|
|
543
|
+
this.consumed = true;
|
|
544
|
+
const body = this.body;
|
|
545
|
+
this.body = null;
|
|
546
|
+
return body;
|
|
547
|
+
}
|
|
548
|
+
decodeChunk(bytes) {
|
|
549
|
+
const text = this.decoder.decode(bytes, { stream: true });
|
|
550
|
+
return this.sse.push(text);
|
|
551
|
+
}
|
|
552
|
+
flushDecoder() {
|
|
553
|
+
return this.sse.push(this.decoder.decode());
|
|
554
|
+
}
|
|
555
|
+
observe(event) {
|
|
556
|
+
if (isNovoRunMetaChunk(event)) {
|
|
557
|
+
this.runIdDeferred.resolve(event.data.runId);
|
|
558
|
+
this.agentIdDeferred.resolve(event.data.agentId);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (isNovoUsageChunk(event)) {
|
|
562
|
+
this.observedUsage = {
|
|
563
|
+
inputTokens: event.data.inputTokens,
|
|
564
|
+
outputTokens: event.data.outputTokens,
|
|
565
|
+
cacheReadTokens: event.data.cacheReadTokens,
|
|
566
|
+
cacheWriteTokens: event.data.cacheWriteTokens,
|
|
567
|
+
runtimeMs: event.data.runtimeMs,
|
|
568
|
+
toolCallCount: event.data.toolCallCount,
|
|
569
|
+
};
|
|
570
|
+
this.observedFinishReason = event.data.finishReason;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (event.type === 'text-delta' && typeof event.delta === 'string') {
|
|
574
|
+
this.textParts.push(event.delta);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (event.type === 'finish' && typeof event.finishReason === 'string') {
|
|
578
|
+
this.observedFinishReason = event.finishReason;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async buildFinalResult() {
|
|
582
|
+
await this.completion;
|
|
583
|
+
const runId = await this.runId;
|
|
584
|
+
const agentId = await this.agentId;
|
|
585
|
+
const run = await this.pollTerminalRun(runId);
|
|
586
|
+
const usage = run.usage ?? this.observedUsage ?? EMPTY_USAGE;
|
|
587
|
+
return {
|
|
588
|
+
runId,
|
|
589
|
+
threadId: this.threadId,
|
|
590
|
+
agentId,
|
|
591
|
+
text: this.textParts.join(''),
|
|
592
|
+
finishReason: run.finishReason ?? this.observedFinishReason ?? run.status,
|
|
593
|
+
usage,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async pollTerminalRun(runId) {
|
|
597
|
+
let latest;
|
|
598
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
599
|
+
latest = await this.client.json({ path: `/v1/runs/${encodeURIComponent(runId)}` });
|
|
600
|
+
if (TERMINAL_RUN_STATUSES.has(latest.status) && latest.usage) {
|
|
601
|
+
return latest;
|
|
602
|
+
}
|
|
603
|
+
await sleep(250);
|
|
604
|
+
}
|
|
605
|
+
if (latest)
|
|
606
|
+
return latest;
|
|
607
|
+
return this.client.json({ path: `/v1/runs/${encodeURIComponent(runId)}` });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
class SSEDecoder {
|
|
611
|
+
done = false;
|
|
612
|
+
buffer = '';
|
|
613
|
+
push(input) {
|
|
614
|
+
if (this.done)
|
|
615
|
+
return [];
|
|
616
|
+
this.buffer += input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
617
|
+
const events = [];
|
|
618
|
+
while (true) {
|
|
619
|
+
const separator = this.buffer.indexOf('\n\n');
|
|
620
|
+
if (separator === -1)
|
|
621
|
+
break;
|
|
622
|
+
const frame = this.buffer.slice(0, separator);
|
|
623
|
+
this.buffer = this.buffer.slice(separator + 2);
|
|
624
|
+
const event = this.parseFrame(frame);
|
|
625
|
+
if (event === '[DONE]') {
|
|
626
|
+
this.done = true;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
if (event)
|
|
630
|
+
events.push(event);
|
|
631
|
+
}
|
|
632
|
+
return events;
|
|
633
|
+
}
|
|
634
|
+
parseFrame(frame) {
|
|
635
|
+
const dataLines = [];
|
|
636
|
+
for (const line of frame.split('\n')) {
|
|
637
|
+
if (!line || line.startsWith(':'))
|
|
638
|
+
continue;
|
|
639
|
+
if (!line.startsWith('data:'))
|
|
640
|
+
continue;
|
|
641
|
+
dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
642
|
+
}
|
|
643
|
+
if (dataLines.length === 0)
|
|
644
|
+
return undefined;
|
|
645
|
+
const data = dataLines.join('\n');
|
|
646
|
+
if (data === '[DONE]')
|
|
647
|
+
return '[DONE]';
|
|
648
|
+
return JSON.parse(data);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function assertOk(response) {
|
|
652
|
+
if (response.ok)
|
|
653
|
+
return;
|
|
654
|
+
const requestId = response.headers.get('X-Request-Id') ?? '';
|
|
655
|
+
let parsed;
|
|
656
|
+
try {
|
|
657
|
+
parsed = (await response.json());
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
parsed = undefined;
|
|
661
|
+
}
|
|
662
|
+
const error = parsed?.error;
|
|
663
|
+
throw new NovoAPIError({
|
|
664
|
+
type: error?.type ?? 'api_error',
|
|
665
|
+
code: error?.code ?? 'internal_error',
|
|
666
|
+
message: error?.message ?? `Novo API request failed with status ${response.status}.`,
|
|
667
|
+
requestId: error?.request_id ?? requestId,
|
|
668
|
+
status: response.status,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function stripIdempotencyKey(input) {
|
|
672
|
+
const { idempotencyKey: _idempotencyKey, ...body } = input;
|
|
673
|
+
return body;
|
|
674
|
+
}
|
|
675
|
+
function isNovoRunMetaChunk(event) {
|
|
676
|
+
return (event.type === 'data-novo-run-meta' &&
|
|
677
|
+
hasObjectData(event) &&
|
|
678
|
+
typeof event.data.runId === 'string' &&
|
|
679
|
+
typeof event.data.threadId === 'string' &&
|
|
680
|
+
typeof event.data.agentId === 'string' &&
|
|
681
|
+
typeof event.data.createdAt === 'string');
|
|
682
|
+
}
|
|
683
|
+
function isNovoUsageChunk(event) {
|
|
684
|
+
return (event.type === 'data-novo-usage' &&
|
|
685
|
+
hasObjectData(event) &&
|
|
686
|
+
typeof event.data.runId === 'string' &&
|
|
687
|
+
typeof event.data.threadId === 'string' &&
|
|
688
|
+
typeof event.data.agentId === 'string' &&
|
|
689
|
+
typeof event.data.inputTokens === 'number' &&
|
|
690
|
+
typeof event.data.outputTokens === 'number' &&
|
|
691
|
+
typeof event.data.cacheReadTokens === 'number' &&
|
|
692
|
+
typeof event.data.cacheWriteTokens === 'number' &&
|
|
693
|
+
typeof event.data.runtimeMs === 'number' &&
|
|
694
|
+
typeof event.data.toolCallCount === 'number' &&
|
|
695
|
+
typeof event.data.finishReason === 'string');
|
|
696
|
+
}
|
|
697
|
+
function hasObjectData(event) {
|
|
698
|
+
return ('data' in event &&
|
|
699
|
+
typeof event.data === 'object' &&
|
|
700
|
+
event.data !== null &&
|
|
701
|
+
!Array.isArray(event.data));
|
|
702
|
+
}
|
|
703
|
+
function normalizeBaseUrl(baseUrl) {
|
|
704
|
+
return baseUrl.replace(/\/+$/, '');
|
|
705
|
+
}
|
|
706
|
+
export function verifyNovoSignature(input) {
|
|
707
|
+
const parsed = parseNovoSignatureHeader(input.signatureHeader);
|
|
708
|
+
if (!parsed)
|
|
709
|
+
return { valid: false, reason: 'signature_header_invalid' };
|
|
710
|
+
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
711
|
+
const now = Math.floor(Date.now() / 1000);
|
|
712
|
+
if (Math.abs(now - parsed.timestamp) > toleranceSeconds) {
|
|
713
|
+
return { valid: false, reason: 'timestamp_outside_tolerance' };
|
|
714
|
+
}
|
|
715
|
+
const expected = createHmac('sha256', input.secret)
|
|
716
|
+
.update(`${parsed.timestamp}.${input.rawBody}`)
|
|
717
|
+
.digest('hex');
|
|
718
|
+
const actual = Buffer.from(parsed.signature, 'hex');
|
|
719
|
+
const expectedBuffer = Buffer.from(expected, 'hex');
|
|
720
|
+
if (actual.length !== expectedBuffer.length) {
|
|
721
|
+
return { valid: false, reason: 'signature_mismatch' };
|
|
722
|
+
}
|
|
723
|
+
if (!timingSafeEqual(actual, expectedBuffer)) {
|
|
724
|
+
return { valid: false, reason: 'signature_mismatch' };
|
|
725
|
+
}
|
|
726
|
+
return { valid: true, timestamp: parsed.timestamp };
|
|
727
|
+
}
|
|
728
|
+
export const verifyEventSinkSignature = verifyNovoSignature;
|
|
729
|
+
function parseNovoSignatureHeader(value) {
|
|
730
|
+
const parts = Object.fromEntries(value.split(',').map((part) => {
|
|
731
|
+
const [key, ...rest] = part.trim().split('=');
|
|
732
|
+
return [key, rest.join('=')];
|
|
733
|
+
}));
|
|
734
|
+
const timestamp = Number(parts.t);
|
|
735
|
+
const signature = parts.v1;
|
|
736
|
+
if (!Number.isInteger(timestamp) || !signature || !/^[a-f0-9]+$/i.test(signature)) {
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
return { timestamp, signature };
|
|
740
|
+
}
|
|
741
|
+
function deferred() {
|
|
742
|
+
let resolve;
|
|
743
|
+
let reject;
|
|
744
|
+
const promise = new Promise((res, rej) => {
|
|
745
|
+
resolve = res;
|
|
746
|
+
reject = rej;
|
|
747
|
+
});
|
|
748
|
+
return { promise, resolve, reject };
|
|
749
|
+
}
|
|
750
|
+
function sleep(ms) {
|
|
751
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
752
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "novoagents",
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
|
+
"description": "Thin TypeScript SDK for the Novo Agents hosted API.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md"
|
|
14
|
+
],
|
|
15
|
+
"browser": {
|
|
16
|
+
"./dist/index.js": false
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"clean": "rm -rf dist",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
32
|
+
"prepack": "pnpm run clean && pnpm run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "catalog:",
|
|
36
|
+
"typescript": "catalog:",
|
|
37
|
+
"vite": "catalog:",
|
|
38
|
+
"vitest": "catalog:"
|
|
39
|
+
}
|
|
40
|
+
}
|