@k2b/nessi 0.10.0 → 0.12.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 +108 -1
- package/ai/index.d.ts +3 -0
- package/ai/index.js +1 -0
- package/ai/providers/openai-compatible-transcription.d.ts +8 -0
- package/ai/providers/openai-compatible-transcription.js +60 -0
- package/ai/transcription.d.ts +18 -0
- package/ai/transcription.js +0 -0
- package/index.d.ts +1 -1
- package/nessi.js +29 -15
- package/package.json +5 -1
- package/structured.js +27 -4
- package/types.d.ts +8 -2
package/README.md
CHANGED
|
@@ -64,6 +64,61 @@ Every outbound event from one `nessi()` run carries the same `loopId`. Pass your
|
|
|
64
64
|
|
|
65
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
66
|
|
|
67
|
+
## Dynamic tools
|
|
68
|
+
|
|
69
|
+
Pass a resolver when the active tools depend on application state:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const loop = nessi({
|
|
73
|
+
provider,
|
|
74
|
+
systemPrompt,
|
|
75
|
+
input,
|
|
76
|
+
store,
|
|
77
|
+
tools: async () => toolRegistry.activeFor(userId),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Nessi resolves dynamic tools before every provider turn. One copied and
|
|
82
|
+
validated snapshot supplies both the provider schemas and all tool execution
|
|
83
|
+
for that turn. Changes made during tool execution therefore become visible on
|
|
84
|
+
the following provider turn, while calls already emitted by the provider remain
|
|
85
|
+
executable from their original snapshot. Duplicate names and resolver failures
|
|
86
|
+
end the loop with a `runtime_error` issue.
|
|
87
|
+
|
|
88
|
+
Pending tool calls restored from history resolve one fresh snapshot before
|
|
89
|
+
execution because an in-memory snapshot cannot survive a restart. Keep the
|
|
90
|
+
resolver side-effect free; persistence, discovery, authorization, and cleanup
|
|
91
|
+
remain application-owned.
|
|
92
|
+
|
|
93
|
+
Server tools receive the provider tool call ID through `ctx.callId`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const inspect = defineTool({
|
|
97
|
+
name: "inspect",
|
|
98
|
+
description: "Inspect an item",
|
|
99
|
+
inputSchema: z.object({ id: z.string() }),
|
|
100
|
+
}).server(async ({ id }, ctx) => {
|
|
101
|
+
return inspectItem(id, { callId: ctx.callId, signal: ctx.signal });
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`nessi.structured()` accepts the same pattern for server tools:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const result = await nessi.structured({
|
|
109
|
+
provider,
|
|
110
|
+
input: "Resolve the current task.",
|
|
111
|
+
output: taskSchema,
|
|
112
|
+
tools: () => toolRegistry.activeServerTools(),
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A structured tool resolver always selects `tool_loop` mode, even when a
|
|
117
|
+
snapshot is empty, because later turns may add tools. Nessi appends its internal
|
|
118
|
+
`submit_result` tool to every snapshot. Client tools, approval tools, and a
|
|
119
|
+
user-defined `submit_result` remain unsupported. A static empty array keeps the
|
|
120
|
+
direct native/fallback structured-output path.
|
|
121
|
+
|
|
67
122
|
## Historical tool results
|
|
68
123
|
|
|
69
124
|
Verbose tool output can remain fully persisted without being sent to the model
|
|
@@ -210,11 +265,62 @@ for await (const event of provider.stream({ messages })) {
|
|
|
210
265
|
}
|
|
211
266
|
```
|
|
212
267
|
|
|
268
|
+
## Audio transcription
|
|
269
|
+
|
|
270
|
+
Use `openAICompatibleTranscription()` to upload an audio file to a service that
|
|
271
|
+
implements the OpenAI-compatible `/audio/transcriptions` endpoint. Configure
|
|
272
|
+
the service URL, API key and transcription model explicitly:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { openAICompatibleTranscription } from "@k2b/nessi/ai";
|
|
276
|
+
|
|
277
|
+
const speech = openAICompatibleTranscription("whisper-large-v3", {
|
|
278
|
+
baseURL: "https://api.scaleway.ai/v1",
|
|
279
|
+
apiKey: process.env.SCW_SECRET_KEY,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const result = await speech.transcribe({
|
|
283
|
+
file: Bun.file("./aufnahme.mp3"),
|
|
284
|
+
filename: "aufnahme.mp3",
|
|
285
|
+
language: "de",
|
|
286
|
+
signal: AbortSignal.timeout(120_000),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
console.log(result.text);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
For OpenAI, set `baseURL: "https://api.openai.com/v1"`, use your OpenAI key and
|
|
293
|
+
a supported transcription model such as `whisper-1`. Local compatible services
|
|
294
|
+
can omit `apiKey`. No environment variable is read automatically. Optional
|
|
295
|
+
`headers` support gateways; `apiKey` overrides their Authorization header.
|
|
296
|
+
|
|
297
|
+
`file` accepts a `Blob`, `File` or `Bun.file()`. Use `filename` to supply an
|
|
298
|
+
extension for an unnamed Blob or override the filename. Automatically derived
|
|
299
|
+
filenames omit local directories. Omit `language`
|
|
300
|
+
for automatic detection. Optional `prompt` supplies vocabulary or context
|
|
301
|
+
when supported by the model. File formats, size limits and optional parameter
|
|
302
|
+
support depend on the service; Nessi does not convert or split audio.
|
|
303
|
+
|
|
304
|
+
The result is `{ text: string }`, including an empty string for an empty
|
|
305
|
+
transcript. HTTP, connection and malformed-response errors reject the promise.
|
|
306
|
+
Pass `signal` for cancellation or a timeout; cancellation preserves the signal's
|
|
307
|
+
reason. There are no automatic retries or default timeout.
|
|
308
|
+
|
|
309
|
+
Transcription uses its own `TranscriptionProvider` contract with `name`, `model`
|
|
310
|
+
and `transcribe(request)`. Custom adapters can implement that interface for other
|
|
311
|
+
protocols. It is separate from the chat provider passed to `nessi()`; pass the
|
|
312
|
+
resulting text into the agent when needed. Streaming transcription, timestamps
|
|
313
|
+
and speaker identification are not exposed.
|
|
314
|
+
|
|
315
|
+
The example follows [Scaleway's audio API documentation](https://www.scaleway.com/en/docs/generative-apis/how-to/query-audio-models/).
|
|
316
|
+
Keep API keys on the server when integrating a browser application.
|
|
317
|
+
|
|
213
318
|
## Focused provider imports
|
|
214
319
|
|
|
215
320
|
```ts
|
|
216
321
|
import { anthropic } from "@k2b/nessi/ai/providers/anthropic";
|
|
217
322
|
import { openai } from "@k2b/nessi/ai/providers/openai";
|
|
323
|
+
import { openAICompatibleTranscription } from "@k2b/nessi/ai/providers/openai-compatible-transcription";
|
|
218
324
|
```
|
|
219
325
|
|
|
220
326
|
## Features
|
|
@@ -235,6 +341,7 @@ import { openai } from "@k2b/nessi/ai/providers/openai";
|
|
|
235
341
|
- Standalone `compact()` loop with `loop_start`, `compaction_start`, `compaction_end`, `issue`, and `loop_end` events
|
|
236
342
|
- Optional token-credit budgeting
|
|
237
343
|
- Provider adapters with shared `complete()` and `stream()` APIs
|
|
344
|
+
- Audio transcription through configurable OpenAI-compatible services
|
|
238
345
|
- Native adapters for OpenAI, OpenRouter, vLLM, Ollama, Anthropic, Mistral, and Gemini
|
|
239
346
|
|
|
240
347
|
## Package layout
|
|
@@ -244,7 +351,7 @@ import { openai } from "@k2b/nessi/ai/providers/openai";
|
|
|
244
351
|
Agent loop, structured task helper, tools, stores, compaction, shared types
|
|
245
352
|
|
|
246
353
|
@k2b/nessi/ai
|
|
247
|
-
Provider factories, provider types, complete(), stream(), responseFormat
|
|
354
|
+
Provider factories, provider types, complete(), stream(), responseFormat, transcribe()
|
|
248
355
|
|
|
249
356
|
@k2b/nessi/ai/providers/*
|
|
250
357
|
Focused provider entrypoints
|
package/ai/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { completeFromStream } from "./complete-from-stream.js";
|
|
2
|
+
export { openAICompatibleTranscription } from "./providers/openai-compatible-transcription.js";
|
|
3
|
+
export type { OpenAICompatibleTranscriptionOptions } from "./providers/openai-compatible-transcription.js";
|
|
4
|
+
export type { TranscriptionProvider, TranscriptionRequest, TranscriptionResult } from "./transcription.js";
|
|
2
5
|
export { openAICompatible } from "./providers/openai-compatible.js";
|
|
3
6
|
export { openai } from "./providers/openai.js";
|
|
4
7
|
export { openrouter } from "./providers/openrouter.js";
|
package/ai/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { completeFromStream } from "./complete-from-stream.js";
|
|
2
|
+
export { openAICompatibleTranscription } from "./providers/openai-compatible-transcription.js";
|
|
2
3
|
export { openAICompatible } from "./providers/openai-compatible.js";
|
|
3
4
|
export { openai } from "./providers/openai.js";
|
|
4
5
|
export { openrouter } from "./providers/openrouter.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TranscriptionProvider } from "../transcription.js";
|
|
2
|
+
export type OpenAICompatibleTranscriptionOptions = {
|
|
3
|
+
baseURL: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const openAICompatibleTranscription: (model: string, options: OpenAICompatibleTranscriptionOptions) => TranscriptionProvider;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { formatConnectionError, normalizeHttpError } from "../shared/errors.js";
|
|
2
|
+
export const openAICompatibleTranscription = (model, options) => {
|
|
3
|
+
const baseURL = options.baseURL.replace(/\/+$/, "");
|
|
4
|
+
const name = options.name ?? "openai-compatible";
|
|
5
|
+
const headers = new Headers(options.headers);
|
|
6
|
+
// Fetch must supply the multipart boundary, including with custom headers.
|
|
7
|
+
headers.delete("Content-Type");
|
|
8
|
+
if (options.apiKey)
|
|
9
|
+
headers.set("Authorization", `Bearer ${options.apiKey}`);
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
model,
|
|
13
|
+
async transcribe(request) {
|
|
14
|
+
request.signal?.throwIfAborted();
|
|
15
|
+
const body = new FormData();
|
|
16
|
+
// Bun.file().name can contain a local path; only upload the basename.
|
|
17
|
+
const fileName = "name" in request.file && typeof request.file.name === "string"
|
|
18
|
+
? request.file.name.split(/[\\/]/).at(-1)
|
|
19
|
+
: undefined;
|
|
20
|
+
const filename = request.filename ?? fileName;
|
|
21
|
+
if (filename !== undefined)
|
|
22
|
+
body.append("file", request.file, filename);
|
|
23
|
+
else
|
|
24
|
+
body.append("file", request.file);
|
|
25
|
+
body.append("model", model);
|
|
26
|
+
body.append("response_format", "json");
|
|
27
|
+
if (request.language !== undefined)
|
|
28
|
+
body.append("language", request.language);
|
|
29
|
+
if (request.prompt !== undefined)
|
|
30
|
+
body.append("prompt", request.prompt);
|
|
31
|
+
const response = await fetch(`${baseURL}/audio/transcriptions`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers,
|
|
34
|
+
body,
|
|
35
|
+
signal: request.signal,
|
|
36
|
+
}).catch((error) => {
|
|
37
|
+
request.signal?.throwIfAborted();
|
|
38
|
+
throw new Error(formatConnectionError(name, error));
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const normalized = await normalizeHttpError(name, response);
|
|
42
|
+
request.signal?.throwIfAborted();
|
|
43
|
+
throw new Error(normalized.error);
|
|
44
|
+
}
|
|
45
|
+
const raw = await response.text();
|
|
46
|
+
let payload;
|
|
47
|
+
try {
|
|
48
|
+
payload = JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`${name} returned invalid transcription JSON.`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof payload !== "object" || payload === null
|
|
54
|
+
|| !("text" in payload) || typeof payload.text !== "string") {
|
|
55
|
+
throw new Error(`${name} returned a transcription without a string text field.`);
|
|
56
|
+
}
|
|
57
|
+
return { text: payload.text };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type TranscriptionRequest = {
|
|
2
|
+
file: Blob;
|
|
3
|
+
/** Override the file name, or supply one for an unnamed Blob. */
|
|
4
|
+
filename?: string;
|
|
5
|
+
/** ISO-639-1 language code, for example "de". Omit for automatic detection. */
|
|
6
|
+
language?: string;
|
|
7
|
+
/** Optional vocabulary or context hint, subject to model support. */
|
|
8
|
+
prompt?: string;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
};
|
|
11
|
+
export type TranscriptionResult = {
|
|
12
|
+
text: string;
|
|
13
|
+
};
|
|
14
|
+
export type TranscriptionProvider = {
|
|
15
|
+
name: string;
|
|
16
|
+
model: string;
|
|
17
|
+
transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
|
|
18
|
+
};
|
|
File without changes
|
package/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { defineTool, toolToJsonSchema, toolToSpec } from "./tools.js";
|
|
|
8
8
|
export { memoryStore } from "./stores.js";
|
|
9
9
|
export { estimateTokens, truncateMiddle, truncateToolResults } from "./utils.js";
|
|
10
10
|
export { cloneLoopAggregate, cloneUsage, mergeLoopAggregates, mergeUsage } from "./aggregates.js";
|
|
11
|
-
export type { NessiOptions, NessiLoop, SteeringContext, SteeringFn, StructuredInput, StructuredMeta, StructuredMode, StructuredOptions, StructuredResult, ContentPart, JsonSchemaObject, Input, OutboundEvent, InboundEvent, DoneReason, LoopAggregate, LoopTimingAggregate, LoopTurnAggregate, LoopToolCallAggregate, LoopToolIssueAggregate, Message, UserMessage, AssistantMessage, AssistantStopReason, HistoricalToolResult, ToolResultMessage, AssistantContentBlock, TextBlock, ThinkingBlock, ToolCallBlock, ToolStreamIssue, ToolStreamIssueKind, ToolStreamIssueReason, ToolHistoricalResultIssue, Usage, ToolDefinition, HistoricalToolResultContext, ServerTool, ClientTool, Tool, ToolContext, Provider, ProviderRequest, ProviderEvent, ResponseFormat, StoreEntry, SessionStore, CompactFn, CompactContext, CompactOptions, CompactResult, CompactDoneReason, CompactEvent, CompactLoop, CreditStore, } from "./types.js";
|
|
11
|
+
export type { NessiOptions, NessiLoop, SteeringContext, SteeringFn, StructuredInput, StructuredMeta, StructuredMode, StructuredOptions, StructuredResult, StructuredToolResolver, ContentPart, JsonSchemaObject, Input, OutboundEvent, InboundEvent, DoneReason, LoopAggregate, LoopTimingAggregate, LoopTurnAggregate, LoopToolCallAggregate, LoopToolIssueAggregate, Message, UserMessage, AssistantMessage, AssistantStopReason, HistoricalToolResult, ToolResultMessage, AssistantContentBlock, TextBlock, ThinkingBlock, ToolCallBlock, ToolStreamIssue, ToolStreamIssueKind, ToolStreamIssueReason, ToolHistoricalResultIssue, Usage, ToolDefinition, HistoricalToolResultContext, ServerTool, ClientTool, Tool, ToolResolver, ToolContext, Provider, ProviderRequest, ProviderEvent, ResponseFormat, StoreEntry, SessionStore, CompactFn, CompactContext, CompactOptions, CompactResult, CompactDoneReason, CompactEvent, CompactLoop, CreditStore, } from "./types.js";
|
package/nessi.js
CHANGED
|
@@ -5,6 +5,20 @@ import { aggregateFromTurns, buildLoopTiming, cloneUsage } from "./aggregates.js
|
|
|
5
5
|
import { appendAssistantContentBlock, buildAssistantMessageFromContent } from "./ai/shared/messages.js";
|
|
6
6
|
import { toolToSpec } from "./tools.js";
|
|
7
7
|
import { createLoopId, projectHistoricalToolResults, toErrorMessage, truncateToolResults, zeroUsage, } from "./utils.js";
|
|
8
|
+
const createToolSnapshot = (value) => {
|
|
9
|
+
if (!Array.isArray(value))
|
|
10
|
+
throw new Error("Tool resolver must return an array");
|
|
11
|
+
const tools = [...value];
|
|
12
|
+
const names = tools.map((tool) => tool.def.name);
|
|
13
|
+
if (new Set(names).size !== names.length) {
|
|
14
|
+
const duplicate = names.find((name, index) => names.indexOf(name) !== index);
|
|
15
|
+
throw new Error(`Duplicate tool name: ${duplicate}`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
providerTools: tools.map(toolToSpec),
|
|
19
|
+
toolMap: new Map(tools.map((tool) => [tool.def.name, tool])),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
8
22
|
class PullCancelledError extends Error {
|
|
9
23
|
constructor() {
|
|
10
24
|
super("channel pull cancelled");
|
|
@@ -320,7 +334,7 @@ const coalesceOutboundEvents = async function* (source, options) {
|
|
|
320
334
|
// nessi()
|
|
321
335
|
// ----------------------------------------------------------------------------
|
|
322
336
|
export const nessi = (options) => {
|
|
323
|
-
const { agentId = "main", loopId: requestedLoopId, input, provider, systemPrompt, tools = [], store, creditStore, compact, steering, maxTurns = Infinity, temperature, maxOutputTokens, disableReasoning, coalesce, maxToolResultChars, signal: externalSignal, } = options;
|
|
337
|
+
const { agentId = "main", loopId: requestedLoopId, input, provider, systemPrompt, tools: toolSource = [], store, creditStore, compact, steering, maxTurns = Infinity, temperature, maxOutputTokens, disableReasoning, coalesce, maxToolResultChars, signal: externalSignal, } = options;
|
|
324
338
|
const channel = createChannel();
|
|
325
339
|
const deferredInbound = [];
|
|
326
340
|
const steerQueue = [];
|
|
@@ -432,6 +446,9 @@ export const nessi = (options) => {
|
|
|
432
446
|
externalSignal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
433
447
|
}
|
|
434
448
|
const signal = abortController.signal;
|
|
449
|
+
const toolResolver = typeof toolSource === "function" ? toolSource : undefined;
|
|
450
|
+
const staticToolSnapshot = toolResolver ? undefined : createToolSnapshot(toolSource);
|
|
451
|
+
const resolveToolSnapshot = async () => staticToolSnapshot ?? createToolSnapshot(await toolResolver());
|
|
435
452
|
async function* applyPendingSteering() {
|
|
436
453
|
const pending = steerQueue.splice(0);
|
|
437
454
|
const supplied = await steering?.({ agentId, loopId, signal });
|
|
@@ -451,13 +468,6 @@ export const nessi = (options) => {
|
|
|
451
468
|
}
|
|
452
469
|
return applied;
|
|
453
470
|
}
|
|
454
|
-
const names = tools.map((tool) => tool.def.name);
|
|
455
|
-
if (new Set(names).size !== names.length) {
|
|
456
|
-
const dup = names.find((name, index) => names.indexOf(name) !== index);
|
|
457
|
-
throw new Error(`Duplicate tool name: ${dup}`);
|
|
458
|
-
}
|
|
459
|
-
const toolMap = new Map(tools.map((tool) => [tool.def.name, tool]));
|
|
460
|
-
const isTerminalTool = (name) => Boolean(toolMap.get(name)?.def?.terminal);
|
|
461
471
|
const appendToolResult = async (callId, name, result, isError = false) => {
|
|
462
472
|
const msg = { role: "tool_result", callId, name, result, isError };
|
|
463
473
|
await store.append(msg);
|
|
@@ -516,10 +526,10 @@ export const nessi = (options) => {
|
|
|
516
526
|
toolExecutionMs: timing.toolExecutionMs,
|
|
517
527
|
actionWaitMs: timing.actionWaitMs,
|
|
518
528
|
}, usage);
|
|
519
|
-
async function* executeToolCall(tc, turnCtx, updateAggregateToolCall, turnIssues) {
|
|
529
|
+
async function* executeToolCall(tc, toolSnapshot, turnCtx, updateAggregateToolCall, turnIssues) {
|
|
520
530
|
const eventFields = { agentId, loopId, ...turnCtx };
|
|
521
531
|
yield { type: "tool_execution_start", ...eventFields, callId: tc.id, name: tc.name, args: tc.args };
|
|
522
|
-
const tool = toolMap.get(tc.name);
|
|
532
|
+
const tool = toolSnapshot.toolMap.get(tc.name);
|
|
523
533
|
if (!tool) {
|
|
524
534
|
yield* failToolCall(tc, turnCtx, updateAggregateToolCall, toolExecutionIssue("unknown_tool", `Unknown tool: ${tc.name}`, tc), turnIssues);
|
|
525
535
|
return;
|
|
@@ -640,6 +650,7 @@ export const nessi = (options) => {
|
|
|
640
650
|
let approvalCounter = 0;
|
|
641
651
|
let clientToolCounter = 0;
|
|
642
652
|
const ctx = {
|
|
653
|
+
callId: tc.id,
|
|
643
654
|
signal: toolAbort.signal,
|
|
644
655
|
requestApproval(message) {
|
|
645
656
|
return new Promise((resolve) => {
|
|
@@ -734,7 +745,7 @@ export const nessi = (options) => {
|
|
|
734
745
|
}
|
|
735
746
|
while (clientToolQueue.length > 0) {
|
|
736
747
|
const req = clientToolQueue.shift();
|
|
737
|
-
const bridgeTool = toolMap.get(req.name);
|
|
748
|
+
const bridgeTool = toolSnapshot.toolMap.get(req.name);
|
|
738
749
|
let requestArgs = req.args;
|
|
739
750
|
if (bridgeTool) {
|
|
740
751
|
if (bridgeTool.kind !== "client") {
|
|
@@ -872,6 +883,7 @@ export const nessi = (options) => {
|
|
|
872
883
|
const pending = toolCallBlocks.filter((block) => !resolvedCallIds.has(block.id));
|
|
873
884
|
if (pending.length === 0)
|
|
874
885
|
return;
|
|
886
|
+
const toolSnapshot = await resolveToolSnapshot();
|
|
875
887
|
const aggregateTurn = loopTurns.findLast((turn) => turn.message === assistantMessage);
|
|
876
888
|
const aggregateToolCallMap = new Map((aggregateTurn?.toolCalls ?? []).map((toolCall) => [toolCall.callId, toolCall]));
|
|
877
889
|
const updateAggregateToolCall = aggregateTurn
|
|
@@ -886,7 +898,7 @@ export const nessi = (options) => {
|
|
|
886
898
|
for (const tc of pending) {
|
|
887
899
|
if (signal.aborted)
|
|
888
900
|
return;
|
|
889
|
-
yield* executeToolCall(tc, turnCtx, updateAggregateToolCall, turnIssues);
|
|
901
|
+
yield* executeToolCall(tc, toolSnapshot, turnCtx, updateAggregateToolCall, turnIssues);
|
|
890
902
|
}
|
|
891
903
|
if (aggregateTurn && turnIssues.issues.length > 0) {
|
|
892
904
|
aggregateTurn.issues = [...(aggregateTurn.issues ?? []), ...turnIssues.issues.map((issue) => ({ ...issue }))];
|
|
@@ -901,7 +913,6 @@ export const nessi = (options) => {
|
|
|
901
913
|
let providerTurn = 0;
|
|
902
914
|
let eventTurnIndex = 0;
|
|
903
915
|
let compactionRetried = false;
|
|
904
|
-
const providerTools = tools.map(toolToSpec);
|
|
905
916
|
const prepareProviderMessages = (sourceEntries) => {
|
|
906
917
|
const rawMessages = sourceEntries.map((entry) => entry.message);
|
|
907
918
|
const projectedMessages = projectHistoricalToolResults(rawMessages, loopId);
|
|
@@ -950,6 +961,8 @@ export const nessi = (options) => {
|
|
|
950
961
|
yield loopEndEvent("max_turns");
|
|
951
962
|
return;
|
|
952
963
|
}
|
|
964
|
+
const toolSnapshot = await resolveToolSnapshot();
|
|
965
|
+
const providerTools = toolSnapshot.providerTools;
|
|
953
966
|
let entries = await store.load();
|
|
954
967
|
let messages = prepareProviderMessages(entries);
|
|
955
968
|
const contextWindow = provider.contextWindow;
|
|
@@ -1168,9 +1181,10 @@ export const nessi = (options) => {
|
|
|
1168
1181
|
};
|
|
1169
1182
|
let terminalToolCompleted = false;
|
|
1170
1183
|
for (const tc of toolCalls) {
|
|
1171
|
-
yield* executeToolCall(tc, turnCtx, updateAggregateToolCall, turnIssues);
|
|
1184
|
+
yield* executeToolCall(tc, toolSnapshot, turnCtx, updateAggregateToolCall, turnIssues);
|
|
1172
1185
|
const aggregateToolCall = aggregateToolCallMap.get(tc.id);
|
|
1173
|
-
|
|
1186
|
+
const isTerminalTool = Boolean(toolSnapshot.toolMap.get(tc.name)?.def?.terminal);
|
|
1187
|
+
if (isTerminalTool && aggregateToolCall && !aggregateToolCall.isError) {
|
|
1174
1188
|
terminalToolCompleted = true;
|
|
1175
1189
|
break;
|
|
1176
1190
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/nessi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Minimal agent loop and provider adapters for nessi.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"import": "./ai/index.js",
|
|
17
17
|
"types": "./ai/index.d.ts"
|
|
18
18
|
},
|
|
19
|
+
"./ai/providers/openai-compatible-transcription": {
|
|
20
|
+
"import": "./ai/providers/openai-compatible-transcription.js",
|
|
21
|
+
"types": "./ai/providers/openai-compatible-transcription.d.ts"
|
|
22
|
+
},
|
|
19
23
|
"./ai/providers/openai-compatible": {
|
|
20
24
|
"import": "./ai/providers/openai-compatible.js",
|
|
21
25
|
"types": "./ai/providers/openai-compatible.d.ts"
|
package/structured.js
CHANGED
|
@@ -220,6 +220,7 @@ const wrapStructuredTool = (tool) => ({
|
|
|
220
220
|
def: tool.def,
|
|
221
221
|
execute(input, ctx) {
|
|
222
222
|
return tool.execute(input, {
|
|
223
|
+
callId: ctx.callId,
|
|
223
224
|
signal: ctx.signal,
|
|
224
225
|
async requestApproval() {
|
|
225
226
|
throw new Error("nessi.structured() does not support tool approvals. Use nessi() for interactive tools.");
|
|
@@ -324,8 +325,7 @@ const loopErrorCode = (reason) => {
|
|
|
324
325
|
return "loop_failed";
|
|
325
326
|
};
|
|
326
327
|
const toolLoopStructured = async (options, inputMessage, jsonSchema, loopId) => {
|
|
327
|
-
const
|
|
328
|
-
validateStructuredTools(tools);
|
|
328
|
+
const toolSource = options.tools ?? [];
|
|
329
329
|
let submitted;
|
|
330
330
|
const submitDef = defineTool({
|
|
331
331
|
name: SUBMIT_RESULT_TOOL_NAME,
|
|
@@ -338,6 +338,24 @@ const toolLoopStructured = async (options, inputMessage, jsonSchema, loopId) =>
|
|
|
338
338
|
return { accepted: true };
|
|
339
339
|
});
|
|
340
340
|
submitTool.def.terminal = true;
|
|
341
|
+
let structuredToolError;
|
|
342
|
+
const prepareTools = (value) => {
|
|
343
|
+
if (!Array.isArray(value))
|
|
344
|
+
throw new Error("Structured tool resolver must return an array");
|
|
345
|
+
const tools = [...value];
|
|
346
|
+
try {
|
|
347
|
+
validateStructuredTools(tools);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof StructuredOutputError)
|
|
351
|
+
structuredToolError = error;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
return [...tools.map(wrapStructuredTool), submitTool];
|
|
355
|
+
};
|
|
356
|
+
const tools = typeof toolSource === "function"
|
|
357
|
+
? async () => prepareTools(await toolSource())
|
|
358
|
+
: prepareTools(toolSource);
|
|
341
359
|
const store = memoryStore();
|
|
342
360
|
await store.append(inputMessage);
|
|
343
361
|
const loop = nessi({
|
|
@@ -352,7 +370,7 @@ const toolLoopStructured = async (options, inputMessage, jsonSchema, loopId) =>
|
|
|
352
370
|
"Do not return the final structured result as normal assistant text.",
|
|
353
371
|
schemaInstruction(jsonSchema, options.outputName),
|
|
354
372
|
].join("\n\n"),
|
|
355
|
-
tools
|
|
373
|
+
tools,
|
|
356
374
|
maxTurns: options.maxTurns ?? 8,
|
|
357
375
|
temperature: options.temperature,
|
|
358
376
|
maxOutputTokens: options.maxOutputTokens,
|
|
@@ -372,6 +390,11 @@ const toolLoopStructured = async (options, inputMessage, jsonSchema, loopId) =>
|
|
|
372
390
|
throw new StructuredOutputError("Structured tool loop ended without loop_end.", "loop_failed");
|
|
373
391
|
}
|
|
374
392
|
if (loopEnd.reason !== "stop") {
|
|
393
|
+
if (structuredToolError) {
|
|
394
|
+
throw new StructuredOutputError(structuredToolError.message, structuredToolError.code, {
|
|
395
|
+
aggregate: loopEnd.aggregate,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
375
398
|
throw new StructuredOutputError(`Structured tool loop ended with reason: ${loopEnd.reason}`, loopErrorCode(loopEnd.reason), {
|
|
376
399
|
aggregate: loopEnd.aggregate,
|
|
377
400
|
});
|
|
@@ -406,7 +429,7 @@ export const structured = async (options) => {
|
|
|
406
429
|
const loopId = options.loopId?.trim() ? options.loopId : createLoopId();
|
|
407
430
|
const inputMessage = normalizeStructuredInput(options.input);
|
|
408
431
|
const jsonSchema = schemaFor(options.output);
|
|
409
|
-
const hasTools = (options.tools?.length ?? 0) > 0;
|
|
432
|
+
const hasTools = typeof options.tools === "function" || (options.tools?.length ?? 0) > 0;
|
|
410
433
|
if (hasTools)
|
|
411
434
|
return toolLoopStructured(options, inputMessage, jsonSchema, loopId);
|
|
412
435
|
return directStructured(options, inputMessage, jsonSchema, loopId);
|
package/types.d.ts
CHANGED
|
@@ -137,7 +137,10 @@ export type ClientTool<TInput extends z.ZodType = z.ZodType, TOutput extends z.Z
|
|
|
137
137
|
execute(input: z.infer<TInput>): z.infer<TOutput> | Promise<z.infer<TOutput>>;
|
|
138
138
|
};
|
|
139
139
|
export type Tool = ServerTool | ClientTool;
|
|
140
|
+
export type ToolResolver = () => Tool[] | Promise<Tool[]>;
|
|
140
141
|
export type ToolContext = {
|
|
142
|
+
/** Provider-assigned ID of the tool call currently being executed. */
|
|
143
|
+
callId?: string;
|
|
141
144
|
signal: AbortSignal;
|
|
142
145
|
/** Request user approval mid-execution. Returns true if approved, false if denied. */
|
|
143
146
|
requestApproval(message: string): Promise<boolean>;
|
|
@@ -158,7 +161,8 @@ export type NessiOptions = {
|
|
|
158
161
|
input?: Input;
|
|
159
162
|
provider: Provider;
|
|
160
163
|
systemPrompt: string;
|
|
161
|
-
tools
|
|
164
|
+
/** Static tools or a resolver evaluated once before every provider turn. */
|
|
165
|
+
tools?: Tool[] | ToolResolver;
|
|
162
166
|
store: SessionStore;
|
|
163
167
|
creditStore?: CreditStore;
|
|
164
168
|
compact?: CompactFn;
|
|
@@ -198,6 +202,7 @@ export type StructuredMeta = {
|
|
|
198
202
|
attempts: number;
|
|
199
203
|
usedResponseFormat: boolean;
|
|
200
204
|
};
|
|
205
|
+
export type StructuredToolResolver = () => ServerTool[] | Promise<ServerTool[]>;
|
|
201
206
|
export type StructuredOptions<TOutput extends z.ZodType = z.ZodType> = {
|
|
202
207
|
agentId?: string;
|
|
203
208
|
/** Correlates the internal structured task. Generated when omitted. */
|
|
@@ -207,7 +212,8 @@ export type StructuredOptions<TOutput extends z.ZodType = z.ZodType> = {
|
|
|
207
212
|
input: StructuredInput;
|
|
208
213
|
output: TOutput;
|
|
209
214
|
outputName?: string;
|
|
210
|
-
tools
|
|
215
|
+
/** Static server tools or a resolver evaluated once before every provider turn. */
|
|
216
|
+
tools?: ServerTool[] | StructuredToolResolver;
|
|
211
217
|
maxTurns?: number;
|
|
212
218
|
temperature?: number;
|
|
213
219
|
maxOutputTokens?: number;
|