@k2b/nessi 0.10.0-rc.0 → 0.11.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 +55 -0
- package/index.d.ts +1 -1
- package/nessi.js +29 -15
- package/package.json +1 -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
|
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
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;
|