@k2b/nessi 0.10.0-rc.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/LICENSE +21 -0
- package/README.md +251 -0
- package/aggregates.d.ts +7 -0
- package/aggregates.js +115 -0
- package/ai/complete-from-stream.d.ts +2 -0
- package/ai/complete-from-stream.js +36 -0
- package/ai/index.d.ts +10 -0
- package/ai/index.js +9 -0
- package/ai/providers/anthropic.d.ts +13 -0
- package/ai/providers/anthropic.js +266 -0
- package/ai/providers/gemini.d.ts +12 -0
- package/ai/providers/gemini.js +192 -0
- package/ai/providers/mistral.d.ts +12 -0
- package/ai/providers/mistral.js +287 -0
- package/ai/providers/ollama.d.ts +10 -0
- package/ai/providers/ollama.js +241 -0
- package/ai/providers/openai-compatible.d.ts +2 -0
- package/ai/providers/openai-compatible.js +349 -0
- package/ai/providers/openai.d.ts +12 -0
- package/ai/providers/openai.js +22 -0
- package/ai/providers/openrouter.d.ts +13 -0
- package/ai/providers/openrouter.js +28 -0
- package/ai/providers/vllm.d.ts +11 -0
- package/ai/providers/vllm.js +22 -0
- package/ai/shared/errors.d.ts +15 -0
- package/ai/shared/errors.js +56 -0
- package/ai/shared/json.d.ts +3 -0
- package/ai/shared/json.js +15 -0
- package/ai/shared/messages.d.ts +15 -0
- package/ai/shared/messages.js +58 -0
- package/ai/shared/ndjson.d.ts +4 -0
- package/ai/shared/ndjson.js +60 -0
- package/ai/shared/sse.d.ts +15 -0
- package/ai/shared/sse.js +79 -0
- package/ai/shared/stream-helpers.d.ts +13 -0
- package/ai/shared/stream-helpers.js +105 -0
- package/ai/shared/tool-call-ids.d.ts +5 -0
- package/ai/shared/tool-call-ids.js +38 -0
- package/ai/shared/tool-stream-normalizer.d.ts +6 -0
- package/ai/shared/tool-stream-normalizer.js +271 -0
- package/ai/shared/tools.d.ts +29 -0
- package/ai/shared/tools.js +25 -0
- package/ai/shared/usage.d.ts +3 -0
- package/ai/shared/usage.js +5 -0
- package/ai/types.d.ts +252 -0
- package/ai/types.js +0 -0
- package/compact.d.ts +5 -0
- package/compact.js +108 -0
- package/index.d.ts +11 -0
- package/index.js +12 -0
- package/nessi.d.ts +2 -0
- package/nessi.js +1250 -0
- package/package.json +80 -0
- package/providers/ollama.d.ts +2 -0
- package/providers/ollama.js +1 -0
- package/providers/openai.d.ts +2 -0
- package/providers/openai.js +1 -0
- package/providers/openrouter.d.ts +2 -0
- package/providers/openrouter.js +1 -0
- package/stores.d.ts +11 -0
- package/stores.js +42 -0
- package/structured.d.ts +9 -0
- package/structured.js +413 -0
- package/tools.d.ts +25 -0
- package/tools.js +36 -0
- package/types.d.ts +290 -0
- package/types.js +3 -0
- package/utils.d.ts +15 -0
- package/utils.js +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ValentinKolb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# @k2b/nessi
|
|
2
|
+
|
|
3
|
+
Minimal agent loop and provider adapters for TypeScript.
|
|
4
|
+
|
|
5
|
+
Use the package root for the managed `nessi()` loop with tools, storage, and loop metadata. Use `@k2b/nessi/ai` when an app only needs provider calls through one normalized message and stream API.
|
|
6
|
+
|
|
7
|
+
`@k2b/nessi` replaces the deprecated `@valentinkolb/nessi` package. Existing
|
|
8
|
+
APIs and subpaths are unchanged; replace the package scope in dependencies and
|
|
9
|
+
imports.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add @k2b/nessi
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { nessi, defineTool, memoryStore } from "@k2b/nessi";
|
|
19
|
+
import { ollama } from "@k2b/nessi/ai";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
const weather = defineTool({
|
|
23
|
+
name: "weather",
|
|
24
|
+
description: "Return a fake weather response",
|
|
25
|
+
inputSchema: z.object({ city: z.string() }),
|
|
26
|
+
}).server(async ({ city }) => {
|
|
27
|
+
return { city, forecast: "sunny" };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const loop = nessi({
|
|
31
|
+
loopId: crypto.randomUUID(),
|
|
32
|
+
provider: ollama("llama3.1", {
|
|
33
|
+
baseURL: "http://localhost:11434",
|
|
34
|
+
}),
|
|
35
|
+
systemPrompt: "You are concise.",
|
|
36
|
+
input: "How is the weather in Berlin?",
|
|
37
|
+
store: memoryStore(),
|
|
38
|
+
tools: [weather],
|
|
39
|
+
temperature: 0,
|
|
40
|
+
maxOutputTokens: 512,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const textBlocks = new Set<string>();
|
|
44
|
+
|
|
45
|
+
for await (const event of loop) {
|
|
46
|
+
if (event.type === "block_start" && event.kind === "text") {
|
|
47
|
+
textBlocks.add(event.blockId);
|
|
48
|
+
}
|
|
49
|
+
if (event.type === "block_delta" && textBlocks.has(event.blockId)) {
|
|
50
|
+
process.stdout.write(event.delta);
|
|
51
|
+
}
|
|
52
|
+
if (event.type === "issue") {
|
|
53
|
+
console.error(event.issue.kind, event.issue.message);
|
|
54
|
+
}
|
|
55
|
+
if (event.type === "loop_end") {
|
|
56
|
+
console.log(event.loopId);
|
|
57
|
+
console.log(event.aggregate.usage);
|
|
58
|
+
console.log(event.aggregate.timing);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every outbound event from one `nessi()` run carries the same `loopId`. Pass your own `loopId` to align events with a persisted request or UI response group, or let Nessi generate one when omitted.
|
|
64
|
+
|
|
65
|
+
`turn_end` reports each internal provider turn. The final `loop_end` event includes `aggregate`, which groups assistant turns, executable tool calls, tool results, validation/execution errors, malformed or cancelled tool streams, summed usage, and timing for the complete logical loop. `aggregate.timing.totalElapsedMs` is model generation plus active tool execution; approval/client-tool waits are tracked separately as `aggregate.timing.actionWaitMs`. Helper exports such as `mergeUsage()`, `cloneLoopAggregate()`, and `mergeLoopAggregates()` are available from `@k2b/nessi`.
|
|
66
|
+
|
|
67
|
+
## Historical tool results
|
|
68
|
+
|
|
69
|
+
Verbose tool output can remain fully persisted without being sent to the model
|
|
70
|
+
in every later loop. A tool may derive a compact historical representation once
|
|
71
|
+
after its output passes validation:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const shell = defineTool({
|
|
75
|
+
name: "shell",
|
|
76
|
+
description: "Run a shell command.",
|
|
77
|
+
inputSchema: z.object({ command: z.string() }),
|
|
78
|
+
outputSchema: z.object({
|
|
79
|
+
exitCode: z.number(),
|
|
80
|
+
stdout: z.string(),
|
|
81
|
+
changedFiles: z.array(z.string()),
|
|
82
|
+
}),
|
|
83
|
+
toHistoricalResult: ({ output }) => ({
|
|
84
|
+
exitCode: output.exitCode,
|
|
85
|
+
changedFiles: output.changedFiles,
|
|
86
|
+
excerpt: output.stdout.slice(0, 500),
|
|
87
|
+
}),
|
|
88
|
+
}).server(runShell);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Nessi persists the full `result` and the optional `historicalResult` together.
|
|
92
|
+
Provider calls in the originating loop, including resumes with the same
|
|
93
|
+
`loopId`, receive the full result. Calls from a different loop receive the
|
|
94
|
+
historical value instead. Stored messages, events, and loop aggregates remain
|
|
95
|
+
full and inspectable. Returning `undefined` skips the historical representation.
|
|
96
|
+
|
|
97
|
+
If derivation throws, Nessi stores the full successful result, emits a non-fatal
|
|
98
|
+
`tool_historical_result_error` issue, and continues the loop. Legacy messages
|
|
99
|
+
without `historicalResult` remain unchanged. `maxToolResultChars`, when set,
|
|
100
|
+
runs after this selection as the final context-size boundary.
|
|
101
|
+
|
|
102
|
+
## Steering
|
|
103
|
+
|
|
104
|
+
Use `loop.steer()` when the process handling new input owns the running loop:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
loop.steer("Skip deployment and only prepare the migration.");
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For loops running in another worker or process, provide a `steering` callback
|
|
111
|
+
that reads pending messages from application-owned persistence:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const loop = nessi({
|
|
115
|
+
provider,
|
|
116
|
+
systemPrompt,
|
|
117
|
+
store,
|
|
118
|
+
input,
|
|
119
|
+
tools,
|
|
120
|
+
steering: ({ loopId, signal }) => steeringQueue.takePending(loopId, { signal }),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The callback may return one message, an ordered array, or `undefined`. Nessi
|
|
125
|
+
checks it before provider calls and before a normal loop completion. Applied
|
|
126
|
+
messages are persisted as user messages and emit the same `steer_applied`
|
|
127
|
+
event as `loop.steer()`. The application owns persistence, claiming, and
|
|
128
|
+
delivery semantics; Nessi only controls when steering can affect the loop.
|
|
129
|
+
|
|
130
|
+
## Structured output
|
|
131
|
+
|
|
132
|
+
Use `nessi.structured()` when an app wants a schema-valid typed result instead
|
|
133
|
+
of a streamed chat response:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { nessi } from "@k2b/nessi";
|
|
137
|
+
import { openrouter } from "@k2b/nessi/ai";
|
|
138
|
+
import { z } from "zod";
|
|
139
|
+
|
|
140
|
+
const result = await nessi.structured({
|
|
141
|
+
provider: openrouter("openai/gpt-4.1-mini", {
|
|
142
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
143
|
+
}),
|
|
144
|
+
input: "Extract a task card for: Ship the onboarding flow by Friday.",
|
|
145
|
+
outputName: "task_card",
|
|
146
|
+
output: z.object({
|
|
147
|
+
title: z.string(),
|
|
148
|
+
due: z.string().nullable(),
|
|
149
|
+
priority: z.enum(["low", "medium", "high"]),
|
|
150
|
+
}),
|
|
151
|
+
temperature: 0,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.log(result.output.title);
|
|
155
|
+
console.log(result.structuredMeta);
|
|
156
|
+
console.log(result.aggregate.usage);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For providers and schemas that are safe for native structured output, Nessi
|
|
160
|
+
passes a provider-specific `responseFormat`. Otherwise it falls back to schema
|
|
161
|
+
instructions and one repair attempt. `input` can be a string, content parts, or
|
|
162
|
+
a full user message, including image file parts when the provider supports
|
|
163
|
+
images.
|
|
164
|
+
|
|
165
|
+
`nessi.structured()` can use server tools for bounded task work. It adds an
|
|
166
|
+
internal `submit_result` tool and returns after that tool receives a valid
|
|
167
|
+
schema value. Client tools, approval tools, and interactive tool bridges remain
|
|
168
|
+
the job of the full `nessi()` loop.
|
|
169
|
+
|
|
170
|
+
## Provider-only usage
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { openrouter } from "@k2b/nessi/ai";
|
|
174
|
+
|
|
175
|
+
const provider = openrouter("openai/gpt-4.1-mini", {
|
|
176
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await provider.complete({
|
|
180
|
+
systemPrompt: "Be concise.",
|
|
181
|
+
messages: [
|
|
182
|
+
{
|
|
183
|
+
role: "user",
|
|
184
|
+
content: [{ type: "text", text: "Summarize this package." }],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.log(result.message.content);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Provider streams use the same block events as the root loop:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const textBlocks = new Set<string>();
|
|
196
|
+
|
|
197
|
+
for await (const event of provider.stream({ messages })) {
|
|
198
|
+
if (event.type === "block_start" && event.kind === "text") {
|
|
199
|
+
textBlocks.add(event.blockId);
|
|
200
|
+
}
|
|
201
|
+
if (event.type === "block_delta" && textBlocks.has(event.blockId)) {
|
|
202
|
+
process.stdout.write(event.delta);
|
|
203
|
+
}
|
|
204
|
+
if (event.type === "block_end" && event.block.type === "tool_call") {
|
|
205
|
+
console.log("tool call", event.block.name, event.block.args);
|
|
206
|
+
}
|
|
207
|
+
if (event.type === "issue") {
|
|
208
|
+
console.error(event.issue.kind, event.issue.message);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Focused provider imports
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { anthropic } from "@k2b/nessi/ai/providers/anthropic";
|
|
217
|
+
import { openai } from "@k2b/nessi/ai/providers/openai";
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Features
|
|
221
|
+
|
|
222
|
+
- Turn-based agent loop with canonical block streaming events
|
|
223
|
+
- Stable `loopId` correlation across all events from one agent loop
|
|
224
|
+
- `loop_start`, `turn_start`, `turn_end`, and `loop_end.aggregate` for logical response grouping
|
|
225
|
+
- Local `loop.steer()` and optional `steering` callbacks for steering at safe loop boundaries
|
|
226
|
+
- Loop timing metadata for wall time, generation time, active tool time, action wait time, and output tokens/second
|
|
227
|
+
- `nessi.structured()` for typed schema-valid task results
|
|
228
|
+
- Server tools and client tools
|
|
229
|
+
- Tool approval flow and explicit `tool_action_request` events
|
|
230
|
+
- Tool execution start/end events with per-tool `timeoutMs`
|
|
231
|
+
- Optional per-tool historical result representations for bounded future context
|
|
232
|
+
- Structured `issue` events for provider errors, timeouts, malformed tool streams, and tool execution failures
|
|
233
|
+
- Pluggable session store
|
|
234
|
+
- Optional history compaction
|
|
235
|
+
- Standalone `compact()` loop with `loop_start`, `compaction_start`, `compaction_end`, `issue`, and `loop_end` events
|
|
236
|
+
- Optional token-credit budgeting
|
|
237
|
+
- Provider adapters with shared `complete()` and `stream()` APIs
|
|
238
|
+
- Native adapters for OpenAI, OpenRouter, vLLM, Ollama, Anthropic, Mistral, and Gemini
|
|
239
|
+
|
|
240
|
+
## Package layout
|
|
241
|
+
|
|
242
|
+
```txt
|
|
243
|
+
@k2b/nessi
|
|
244
|
+
Agent loop, structured task helper, tools, stores, compaction, shared types
|
|
245
|
+
|
|
246
|
+
@k2b/nessi/ai
|
|
247
|
+
Provider factories, provider types, complete(), stream(), responseFormat
|
|
248
|
+
|
|
249
|
+
@k2b/nessi/ai/providers/*
|
|
250
|
+
Focused provider entrypoints
|
|
251
|
+
```
|
package/aggregates.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LoopAggregate, LoopIssueAggregate, LoopTimingAggregate, LoopTurnAggregate, Usage } from "./types.js";
|
|
2
|
+
export declare const cloneUsage: (usage: Usage | undefined) => Usage | undefined;
|
|
3
|
+
export declare const mergeUsage: (left: Usage | undefined, right: Usage | undefined) => Usage | undefined;
|
|
4
|
+
export declare const buildLoopTiming: (timing: Pick<LoopTimingAggregate, "wallMs" | "generationMs" | "toolExecutionMs" | "actionWaitMs">, usage: Usage | undefined) => LoopTimingAggregate;
|
|
5
|
+
export declare const aggregateFromTurns: (turns: LoopTurnAggregate[], loopIssues?: LoopIssueAggregate[], timing?: LoopTimingAggregate) => LoopAggregate;
|
|
6
|
+
export declare const cloneLoopAggregate: (aggregate: LoopAggregate) => LoopAggregate;
|
|
7
|
+
export declare const mergeLoopAggregates: (left: LoopAggregate | undefined, right: LoopAggregate | undefined) => LoopAggregate | undefined;
|
package/aggregates.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// nessi - Loop aggregate helpers
|
|
3
|
+
// ============================================================================
|
|
4
|
+
export const cloneUsage = (usage) => usage ? { ...usage } : undefined;
|
|
5
|
+
export const mergeUsage = (left, right) => {
|
|
6
|
+
if (!right)
|
|
7
|
+
return cloneUsage(left);
|
|
8
|
+
const next = {
|
|
9
|
+
input: (left?.input ?? 0) + right.input,
|
|
10
|
+
output: (left?.output ?? 0) + right.output,
|
|
11
|
+
total: (left?.total ?? 0) + right.total,
|
|
12
|
+
};
|
|
13
|
+
if (left?.cacheRead !== undefined || right.cacheRead !== undefined) {
|
|
14
|
+
next.cacheRead = (left?.cacheRead ?? 0) + (right.cacheRead ?? 0);
|
|
15
|
+
}
|
|
16
|
+
if (left?.creditsUsed !== undefined || right.creditsUsed !== undefined) {
|
|
17
|
+
next.creditsUsed = (left?.creditsUsed ?? 0) + (right.creditsUsed ?? 0);
|
|
18
|
+
}
|
|
19
|
+
return next;
|
|
20
|
+
};
|
|
21
|
+
const cloneToolCall = (toolCall) => ({ ...toolCall });
|
|
22
|
+
const cloneToolIssue = (toolIssue) => ({ ...toolIssue });
|
|
23
|
+
const cloneIssue = (issue) => ({ ...issue });
|
|
24
|
+
const cloneTiming = (timing) => timing
|
|
25
|
+
? {
|
|
26
|
+
...timing,
|
|
27
|
+
totalElapsedMs: timing.totalElapsedMs ?? timing.generationMs + timing.toolExecutionMs,
|
|
28
|
+
}
|
|
29
|
+
: undefined;
|
|
30
|
+
const outputTokensPerSecond = (usage, generationMs) => {
|
|
31
|
+
if (!usage || usage.output <= 0 || generationMs <= 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
return usage.output / (generationMs / 1000);
|
|
34
|
+
};
|
|
35
|
+
export const buildLoopTiming = (timing, usage) => {
|
|
36
|
+
const snapshot = {
|
|
37
|
+
...timing,
|
|
38
|
+
totalElapsedMs: timing.generationMs + timing.toolExecutionMs,
|
|
39
|
+
};
|
|
40
|
+
const throughput = outputTokensPerSecond(usage, timing.generationMs);
|
|
41
|
+
if (throughput !== undefined)
|
|
42
|
+
snapshot.outputTokensPerSecond = throughput;
|
|
43
|
+
return snapshot;
|
|
44
|
+
};
|
|
45
|
+
const mergeTiming = (left, right, usage) => {
|
|
46
|
+
if (!left && !right)
|
|
47
|
+
return undefined;
|
|
48
|
+
const generationMs = (left?.generationMs ?? 0) + (right?.generationMs ?? 0);
|
|
49
|
+
return buildLoopTiming({
|
|
50
|
+
wallMs: (left?.wallMs ?? 0) + (right?.wallMs ?? 0),
|
|
51
|
+
generationMs,
|
|
52
|
+
toolExecutionMs: (left?.toolExecutionMs ?? 0) + (right?.toolExecutionMs ?? 0),
|
|
53
|
+
actionWaitMs: (left?.actionWaitMs ?? 0) + (right?.actionWaitMs ?? 0),
|
|
54
|
+
}, usage);
|
|
55
|
+
};
|
|
56
|
+
const toolIssuesFromAggregate = (aggregate) => (aggregate.toolIssues ?? aggregate.turns.flatMap((turn) => turn.toolIssues ?? [])).map(cloneToolIssue);
|
|
57
|
+
const issuesFromAggregate = (aggregate) => (aggregate.issues ?? aggregate.toolIssues ?? aggregate.turns.flatMap((turn) => turn.issues ?? turn.toolIssues ?? []))
|
|
58
|
+
.map(cloneIssue);
|
|
59
|
+
const cloneTurn = (turn) => ({
|
|
60
|
+
...turn,
|
|
61
|
+
usage: cloneUsage(turn.usage),
|
|
62
|
+
toolCalls: turn.toolCalls.map(cloneToolCall),
|
|
63
|
+
...(turn.toolIssues ? { toolIssues: turn.toolIssues.map(cloneToolIssue) } : {}),
|
|
64
|
+
...(turn.issues ? { issues: turn.issues.map(cloneIssue) } : {}),
|
|
65
|
+
});
|
|
66
|
+
export const aggregateFromTurns = (turns, loopIssues = [], timing) => {
|
|
67
|
+
const clonedTurns = turns.map(cloneTurn);
|
|
68
|
+
const issues = loopIssues.length > 0
|
|
69
|
+
? loopIssues.map(cloneIssue)
|
|
70
|
+
: clonedTurns.flatMap((turn) => turn.issues ?? turn.toolIssues ?? []);
|
|
71
|
+
const toolIssues = issues.filter((issue) => issue.kind === "malformed_tool_call" || issue.kind === "cancelled_tool_call");
|
|
72
|
+
const usage = clonedTurns.reduce((mergedUsage, turn) => mergeUsage(mergedUsage, turn.usage), undefined);
|
|
73
|
+
return {
|
|
74
|
+
turns: clonedTurns,
|
|
75
|
+
usage,
|
|
76
|
+
...(timing ? { timing: cloneTiming(timing) } : {}),
|
|
77
|
+
issueCount: issues.length,
|
|
78
|
+
issues: issues.map(cloneIssue),
|
|
79
|
+
toolCallCount: clonedTurns.reduce((count, turn) => count + turn.toolCalls.length, 0),
|
|
80
|
+
toolErrorCount: clonedTurns.reduce((count, turn) => count + turn.toolCalls.filter((toolCall) => toolCall.isError).length, 0),
|
|
81
|
+
toolIssueCount: toolIssues.length,
|
|
82
|
+
toolMalformedCount: toolIssues.filter((issue) => issue.kind === "malformed_tool_call").length,
|
|
83
|
+
toolCancelledCount: toolIssues.filter((issue) => issue.kind === "cancelled_tool_call").length,
|
|
84
|
+
toolIssues: toolIssues.map(cloneToolIssue),
|
|
85
|
+
assistantMessageCount: clonedTurns.length,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export const cloneLoopAggregate = (aggregate) => {
|
|
89
|
+
const turns = aggregate.turns.map(cloneTurn);
|
|
90
|
+
const issues = issuesFromAggregate(aggregate);
|
|
91
|
+
const toolIssues = toolIssuesFromAggregate(aggregate);
|
|
92
|
+
return {
|
|
93
|
+
turns,
|
|
94
|
+
usage: cloneUsage(aggregate.usage),
|
|
95
|
+
...(aggregate.timing ? { timing: cloneTiming(aggregate.timing) } : {}),
|
|
96
|
+
issueCount: aggregate.issueCount ?? issues.length,
|
|
97
|
+
issues,
|
|
98
|
+
toolCallCount: aggregate.toolCallCount,
|
|
99
|
+
toolErrorCount: aggregate.toolErrorCount,
|
|
100
|
+
toolIssueCount: aggregate.toolIssueCount ?? toolIssues.length,
|
|
101
|
+
toolMalformedCount: aggregate.toolMalformedCount ?? toolIssues.filter((issue) => issue.kind === "malformed_tool_call").length,
|
|
102
|
+
toolCancelledCount: aggregate.toolCancelledCount ?? toolIssues.filter((issue) => issue.kind === "cancelled_tool_call").length,
|
|
103
|
+
toolIssues,
|
|
104
|
+
assistantMessageCount: aggregate.assistantMessageCount,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
export const mergeLoopAggregates = (left, right) => {
|
|
108
|
+
if (!left)
|
|
109
|
+
return right ? cloneLoopAggregate(right) : undefined;
|
|
110
|
+
if (!right)
|
|
111
|
+
return cloneLoopAggregate(left);
|
|
112
|
+
const merged = aggregateFromTurns([...left.turns, ...right.turns], [...issuesFromAggregate(left), ...issuesFromAggregate(right)]);
|
|
113
|
+
const timing = mergeTiming(left.timing, right.timing, merged.usage);
|
|
114
|
+
return timing ? { ...merged, timing } : merged;
|
|
115
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { appendAssistantContentBlock, buildAssistantMessageFromContent } from "./shared/messages.js";
|
|
2
|
+
export const completeFromStream = async (provider, request) => {
|
|
3
|
+
let usage;
|
|
4
|
+
let finishReason;
|
|
5
|
+
const content = [];
|
|
6
|
+
const toolCalls = [];
|
|
7
|
+
for await (const event of provider.stream(request)) {
|
|
8
|
+
switch (event.type) {
|
|
9
|
+
case "block_start":
|
|
10
|
+
case "block_delta":
|
|
11
|
+
break;
|
|
12
|
+
case "block_end":
|
|
13
|
+
if (event.block.type === "tool_call")
|
|
14
|
+
toolCalls.push(event.block);
|
|
15
|
+
appendAssistantContentBlock(content, event.block);
|
|
16
|
+
break;
|
|
17
|
+
case "issue":
|
|
18
|
+
if (event.issue.kind === "provider_error")
|
|
19
|
+
throw new Error(event.issue.message);
|
|
20
|
+
if (event.issue.kind === "timeout" && event.issue.scope !== "tool")
|
|
21
|
+
throw new Error(event.issue.message);
|
|
22
|
+
break;
|
|
23
|
+
case "usage":
|
|
24
|
+
usage = event.usage;
|
|
25
|
+
finishReason = event.finishReason ?? finishReason;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
finishReason ??= toolCalls.length > 0 ? "tool_use" : "stop";
|
|
30
|
+
return {
|
|
31
|
+
message: buildAssistantMessageFromContent(provider.model, content, usage, finishReason),
|
|
32
|
+
usage,
|
|
33
|
+
finishReason,
|
|
34
|
+
providerMeta: { model: provider.model },
|
|
35
|
+
};
|
|
36
|
+
};
|
package/ai/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { completeFromStream } from "./complete-from-stream.js";
|
|
2
|
+
export { openAICompatible } from "./providers/openai-compatible.js";
|
|
3
|
+
export { openai } from "./providers/openai.js";
|
|
4
|
+
export { openrouter } from "./providers/openrouter.js";
|
|
5
|
+
export { vllm } from "./providers/vllm.js";
|
|
6
|
+
export { ollama } from "./providers/ollama.js";
|
|
7
|
+
export { anthropic } from "./providers/anthropic.js";
|
|
8
|
+
export { mistral } from "./providers/mistral.js";
|
|
9
|
+
export { gemini } from "./providers/gemini.js";
|
|
10
|
+
export type { AssistantBlockKind, AssistantContentBlock, AssistantMessage, AssistantStopReason, BlockDeltaEvent, BlockEndEvent, BlockStartEvent, ContentPart, GenerateRequest, GenerateResult, HistoricalToolResult, InputFilePart, JsonSchemaObject, Message, NessiIssue, Provider, ProviderCapabilities, ProviderFamily, ProviderIssue, ProviderTimeouts, RuntimeIssue, ResponseFormat, StreamEvent, TextBlock, ThinkingBlock, ToolCallBlock, ToolExecutionIssue, ToolHistoricalResultIssue, ToolResultMessage, ToolStreamIssue, ToolStreamIssueKind, ToolStreamIssueReason, ToolSpec, TimeoutIssue, Usage, UserMessage, OpenAICompat, OpenAICompatibleConfig, } from "./types.js";
|
package/ai/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { completeFromStream } from "./complete-from-stream.js";
|
|
2
|
+
export { openAICompatible } from "./providers/openai-compatible.js";
|
|
3
|
+
export { openai } from "./providers/openai.js";
|
|
4
|
+
export { openrouter } from "./providers/openrouter.js";
|
|
5
|
+
export { vllm } from "./providers/vllm.js";
|
|
6
|
+
export { ollama } from "./providers/ollama.js";
|
|
7
|
+
export { anthropic } from "./providers/anthropic.js";
|
|
8
|
+
export { mistral } from "./providers/mistral.js";
|
|
9
|
+
export { gemini } from "./providers/gemini.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Provider, ProviderTimeouts } from "../types.js";
|
|
2
|
+
export type AnthropicOptions = {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
baseURL?: string;
|
|
5
|
+
apiVersion?: string;
|
|
6
|
+
contextWindow?: number;
|
|
7
|
+
temperature?: number;
|
|
8
|
+
maxOutputTokens?: number;
|
|
9
|
+
creditsPerInputToken?: number;
|
|
10
|
+
creditsPerOutputToken?: number;
|
|
11
|
+
timeouts?: ProviderTimeouts;
|
|
12
|
+
};
|
|
13
|
+
export declare const anthropic: (model: string, options?: AnthropicOptions) => Provider;
|