@jxburros/ai-nugget 0.4.1 → 0.7.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 +158 -382
- package/UPGRADING.md +182 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +82 -37
- package/dist/adapters/engines/base.d.ts +29 -2
- package/dist/adapters/engines/base.js +52 -8
- package/dist/adapters/engines/google.d.ts +7 -1
- package/dist/adapters/engines/google.js +80 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +57 -15
- package/dist/adapters/engines/openaiChat.d.ts +8 -1
- package/dist/adapters/engines/openaiChat.js +109 -29
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +45 -1
- package/dist/agent/loop.js +117 -12
- package/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +291 -0
- package/dist/cjs/adapters/engines/ollama.js +205 -0
- package/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/dist/cjs/adapters/index.js +22 -0
- package/dist/cjs/adapters/profiles.js +125 -0
- package/dist/cjs/agent/index.js +18 -0
- package/dist/cjs/agent/loop.js +373 -0
- package/dist/cjs/agent/tools.js +51 -0
- package/dist/cjs/connect.js +27 -0
- package/dist/cjs/errors.js +110 -0
- package/dist/cjs/handler.js +586 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/json.js +121 -0
- package/dist/cjs/keys.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/policy.js +42 -0
- package/dist/cjs/reasoning.js +159 -0
- package/dist/cjs/redact.js +78 -0
- package/dist/cjs/tokens.js +22 -0
- package/dist/cjs/transport.js +172 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/util.js +126 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +67 -1
- package/dist/handler.js +266 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reasoning.d.ts +45 -0
- package/dist/reasoning.js +154 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +29 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +21 -1
- package/dist/types.d.ts +112 -2
- package/dist/util.d.ts +22 -0
- package/dist/util.js +59 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/anthropic.js +323 -0
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
- package/nugget/dist/adapters/engines/base.d.ts +50 -0
- package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/base.js +137 -0
- package/nugget/dist/adapters/engines/base.js.map +1 -0
- package/nugget/dist/adapters/engines/google.d.ts +24 -0
- package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/google.js +288 -0
- package/nugget/dist/adapters/engines/google.js.map +1 -0
- package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
- package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/ollama.js +202 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +308 -0
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
- package/nugget/dist/adapters/index.d.ts +5 -0
- package/nugget/dist/adapters/index.d.ts.map +1 -0
- package/nugget/dist/adapters/index.js +17 -0
- package/nugget/dist/adapters/index.js.map +1 -0
- package/nugget/dist/adapters/profiles.d.ts +62 -0
- package/nugget/dist/adapters/profiles.d.ts.map +1 -0
- package/nugget/dist/adapters/profiles.js +121 -0
- package/nugget/dist/adapters/profiles.js.map +1 -0
- package/nugget/dist/agent/index.d.ts +3 -0
- package/nugget/dist/agent/index.d.ts.map +1 -0
- package/nugget/dist/agent/index.js +3 -0
- package/nugget/dist/agent/index.js.map +1 -0
- package/nugget/dist/agent/loop.d.ts +114 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +371 -0
- package/nugget/dist/agent/loop.js.map +1 -0
- package/nugget/dist/agent/tools.d.ts +29 -0
- package/nugget/dist/agent/tools.d.ts.map +1 -0
- package/nugget/dist/agent/tools.js +48 -0
- package/nugget/dist/agent/tools.js.map +1 -0
- package/nugget/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +291 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/nugget/dist/cjs/adapters/index.js +22 -0
- package/nugget/dist/cjs/adapters/profiles.js +125 -0
- package/nugget/dist/cjs/agent/index.js +18 -0
- package/nugget/dist/cjs/agent/loop.js +373 -0
- package/nugget/dist/cjs/agent/tools.js +51 -0
- package/nugget/dist/cjs/connect.js +27 -0
- package/nugget/dist/cjs/errors.js +110 -0
- package/nugget/dist/cjs/handler.js +586 -0
- package/nugget/dist/cjs/index.js +28 -0
- package/nugget/dist/cjs/json.js +121 -0
- package/nugget/dist/cjs/keys.js +81 -0
- package/nugget/dist/cjs/package.json +3 -0
- package/nugget/dist/cjs/policy.js +42 -0
- package/nugget/dist/cjs/reasoning.js +159 -0
- package/nugget/dist/cjs/redact.js +78 -0
- package/nugget/dist/cjs/tokens.js +22 -0
- package/nugget/dist/cjs/transport.js +172 -0
- package/nugget/dist/cjs/types.js +6 -0
- package/nugget/dist/cjs/util.js +126 -0
- package/nugget/dist/connect.d.ts +31 -0
- package/nugget/dist/connect.d.ts.map +1 -0
- package/nugget/dist/connect.js +25 -0
- package/nugget/dist/connect.js.map +1 -0
- package/nugget/dist/errors.d.ts +22 -0
- package/nugget/dist/errors.d.ts.map +1 -0
- package/nugget/dist/errors.js +104 -0
- package/nugget/dist/errors.js.map +1 -0
- package/nugget/dist/handler.d.ts +114 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +582 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +13 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +13 -0
- package/nugget/dist/index.js.map +1 -0
- package/nugget/dist/json.d.ts +7 -0
- package/nugget/dist/json.d.ts.map +1 -0
- package/nugget/dist/json.js +114 -0
- package/nugget/dist/json.js.map +1 -0
- package/nugget/dist/keys.d.ts +7 -0
- package/nugget/dist/keys.d.ts.map +1 -0
- package/nugget/dist/keys.js +75 -0
- package/nugget/dist/keys.js.map +1 -0
- package/nugget/dist/policy.d.ts +6 -0
- package/nugget/dist/policy.d.ts.map +1 -0
- package/nugget/dist/policy.js +37 -0
- package/nugget/dist/policy.js.map +1 -0
- package/nugget/dist/reasoning.d.ts +45 -0
- package/nugget/dist/reasoning.d.ts.map +1 -0
- package/nugget/dist/reasoning.js +154 -0
- package/nugget/dist/reasoning.js.map +1 -0
- package/nugget/dist/redact.d.ts +26 -0
- package/nugget/dist/redact.d.ts.map +1 -0
- package/nugget/dist/redact.js +74 -0
- package/nugget/dist/redact.js.map +1 -0
- package/nugget/dist/tokens.d.ts +5 -0
- package/nugget/dist/tokens.d.ts.map +1 -0
- package/nugget/dist/tokens.js +18 -0
- package/nugget/dist/tokens.js.map +1 -0
- package/nugget/dist/transport.d.ts +30 -0
- package/nugget/dist/transport.d.ts.map +1 -0
- package/nugget/dist/transport.js +165 -0
- package/nugget/dist/transport.js.map +1 -0
- package/nugget/dist/types.d.ts +301 -0
- package/nugget/dist/types.d.ts.map +1 -0
- package/nugget/dist/types.js +4 -0
- package/nugget/dist/types.js.map +1 -0
- package/nugget/dist/util.d.ts +31 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +114 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +78 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +79 -29
- package/nugget/src/adapters/engines/ollama.ts +56 -17
- package/nugget/src/adapters/engines/openaiChat.ts +104 -30
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +156 -13
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +308 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/reasoning.ts +174 -0
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +113 -1
- package/nugget/src/util.ts +64 -0
- package/package.json +18 -5
package/nugget/src/agent/loop.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { AIError } from '../errors.js';
|
|
|
3
3
|
import { extractJson } from '../json.js';
|
|
4
4
|
import { mergeUsage } from '../tokens.js';
|
|
5
5
|
import type { AIHandler } from '../handler.js';
|
|
6
|
-
import type { ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
|
|
7
|
-
import { sleep } from '../util.js';
|
|
6
|
+
import type { AIErrorKind, ChatMessage, Connection, ReasoningEffort, StreamEvent, ToolCall, Usage } from '../types.js';
|
|
7
|
+
import { asRecord, sleep } from '../util.js';
|
|
8
8
|
import type { ToolSpec } from './tools.js';
|
|
9
9
|
import { validateToolArgs } from './tools.js';
|
|
10
10
|
|
|
@@ -25,12 +25,42 @@ export interface AgentOptions {
|
|
|
25
25
|
* dependent, not protocol-guaranteed.
|
|
26
26
|
*/
|
|
27
27
|
toolMode?: 'native' | 'promptJson' | 'auto';
|
|
28
|
+
/**
|
|
29
|
+
* Discovered capabilities of the specific model (e.g. Ollama `/api/show`
|
|
30
|
+
* `["tools"]`). When `toolMode` is `auto`, a `tools` capability here upgrades a
|
|
31
|
+
* local model to `native` tool-calling instead of the profile's conservative
|
|
32
|
+
* `promptJson` default — so a capable local model isn't forced onto the fallback.
|
|
33
|
+
*/
|
|
34
|
+
modelCapabilities?: string[];
|
|
35
|
+
/** Sampler/decoding options forwarded to every model turn (parity with a plain chat call). */
|
|
36
|
+
temperature?: number;
|
|
37
|
+
maxTokens?: number;
|
|
38
|
+
topP?: number;
|
|
39
|
+
stopSequences?: string[];
|
|
40
|
+
/** Reasoning effort forwarded to every turn — see {@link ChatRequest.reasoningEffort}. */
|
|
41
|
+
reasoningEffort?: ReasoningEffort;
|
|
42
|
+
/** Provider-native passthrough forwarded to every turn — see {@link ChatRequest.providerOptions}. */
|
|
43
|
+
providerOptions?: Record<string, unknown>;
|
|
28
44
|
budget?: {
|
|
29
45
|
maxSteps?: number;
|
|
30
46
|
maxTokens?: number;
|
|
31
47
|
deadlineMs?: number;
|
|
32
48
|
};
|
|
33
49
|
approval?: ApprovalGate;
|
|
50
|
+
/**
|
|
51
|
+
* Which tools require the approval gate. `sideEffectsOnly` (default) gates only
|
|
52
|
+
* tools declared `sideEffects: true`; `all` gates every tool — so `tool_denied`
|
|
53
|
+
* is reachable without marking each tool, and a consumer can require approval
|
|
54
|
+
* for a whole run in one place.
|
|
55
|
+
*/
|
|
56
|
+
approvalMode?: 'sideEffectsOnly' | 'all';
|
|
57
|
+
/**
|
|
58
|
+
* Bounds and framing applied to each tool result before it re-enters the
|
|
59
|
+
* model's context. `maxChars` truncates oversized results (with a notice) so a
|
|
60
|
+
* single large return can't blow the next turn's window; `wrapUntrusted` fences
|
|
61
|
+
* the result in an `<untrusted_tool_output>` envelope. Both are opt-in.
|
|
62
|
+
*/
|
|
63
|
+
toolResult?: { maxChars?: number; wrapUntrusted?: boolean };
|
|
34
64
|
onEvent?: (e: AgentEvent) => void;
|
|
35
65
|
signal?: AbortSignal;
|
|
36
66
|
metadata?: Record<string, unknown>;
|
|
@@ -45,6 +75,9 @@ export type ApprovalGate = (req: {
|
|
|
45
75
|
export type AgentEvent =
|
|
46
76
|
| StreamEvent
|
|
47
77
|
| { type: 'step_start'; step: number }
|
|
78
|
+
// Discloses which tool-calling protocol this run resolved to, so a
|
|
79
|
+
// `promptJson` fallback is visible rather than silent.
|
|
80
|
+
| { type: 'tool_mode'; mode: 'native' | 'promptJson' }
|
|
48
81
|
| { type: 'tool_start'; step: number; call: ToolCall }
|
|
49
82
|
| { type: 'tool_result'; step: number; call: ToolCall; result: unknown; isError: boolean }
|
|
50
83
|
| { type: 'tool_denied'; step: number; call: ToolCall; reason: string }
|
|
@@ -56,6 +89,11 @@ export interface AgentResult {
|
|
|
56
89
|
usage: Usage;
|
|
57
90
|
steps: number;
|
|
58
91
|
stopReason: 'finished' | 'max_steps' | 'budget' | 'deadline' | 'canceled' | 'error';
|
|
92
|
+
/**
|
|
93
|
+
* Populated when `stopReason` is `error`: the failure that ended the run, so
|
|
94
|
+
* the caller can see what went wrong without replaying the event stream.
|
|
95
|
+
*/
|
|
96
|
+
error?: { kind: AIErrorKind; message: string };
|
|
59
97
|
}
|
|
60
98
|
|
|
61
99
|
export function runAgent(opts: AgentOptions): AsyncIterable<AgentEvent> & { result: Promise<AgentResult> } {
|
|
@@ -77,6 +115,10 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
77
115
|
let step = 0;
|
|
78
116
|
let settled = false;
|
|
79
117
|
|
|
118
|
+
// Disclose the resolved tool protocol once up front (a silent promptJson
|
|
119
|
+
// fallback was the friction behind FR‑2).
|
|
120
|
+
yield emit(opts, { type: 'tool_mode', mode: toolMode });
|
|
121
|
+
|
|
80
122
|
try {
|
|
81
123
|
while (step < maxSteps) {
|
|
82
124
|
step += 1;
|
|
@@ -86,15 +128,32 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
86
128
|
const calls: ToolCall[] = [];
|
|
87
129
|
let stepText = '';
|
|
88
130
|
let streamFailure: AIError | undefined;
|
|
131
|
+
// In promptJson mode a tool directive must not leak into the visible
|
|
132
|
+
// stream, so directive-looking text is withheld from `delta` events (still
|
|
133
|
+
// accumulated in `stepText` for parsing). Prose streams normally.
|
|
134
|
+
const filter = toolMode === 'promptJson' ? new PromptJsonDeltaFilter() : undefined;
|
|
89
135
|
const requestMessages = toolMode === 'promptJson' ? withPromptJsonInstruction(messages, opts.tools) : messages;
|
|
90
136
|
for await (const event of opts.handler.stream(opts.connection, {
|
|
91
137
|
model: opts.model,
|
|
92
138
|
messages: requestMessages,
|
|
93
139
|
tools: toolMode === 'promptJson' ? undefined : opts.tools,
|
|
140
|
+
temperature: opts.temperature,
|
|
141
|
+
maxTokens: opts.maxTokens,
|
|
142
|
+
topP: opts.topP,
|
|
143
|
+
stopSequences: opts.stopSequences,
|
|
144
|
+
reasoningEffort: opts.reasoningEffort,
|
|
145
|
+
providerOptions: opts.providerOptions,
|
|
94
146
|
signal: agentSignal.signal,
|
|
95
147
|
metadata: { ...opts.metadata, agentStep: step },
|
|
96
148
|
})) {
|
|
97
|
-
if (event.type === 'delta')
|
|
149
|
+
if (event.type === 'delta') {
|
|
150
|
+
stepText += event.text;
|
|
151
|
+
if (filter) {
|
|
152
|
+
const visible = filter.push(event.text);
|
|
153
|
+
if (visible) yield emit(opts, { type: 'delta', text: visible });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
98
157
|
if (event.type === 'tool_call') calls.push(event.call);
|
|
99
158
|
if (event.type === 'error') streamFailure = event.error;
|
|
100
159
|
if (event.type === 'done') {
|
|
@@ -103,10 +162,16 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
103
162
|
}
|
|
104
163
|
yield emit(opts, event);
|
|
105
164
|
}
|
|
165
|
+
// If withheld text turned out not to be a real directive, flush it so the
|
|
166
|
+
// user still sees the model's answer.
|
|
167
|
+
if (filter && calls.length === 0) {
|
|
168
|
+
const flushed = filter.flush();
|
|
169
|
+
if (flushed) yield emit(opts, { type: 'delta', text: flushed });
|
|
170
|
+
}
|
|
106
171
|
|
|
107
172
|
// A handler-level failure (auth, policy, cancel, exhausted retries) must
|
|
108
173
|
// stop the loop honestly rather than looking like an empty completion.
|
|
109
|
-
if (streamFailure) return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error');
|
|
174
|
+
if (streamFailure) return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error', errorInfo(streamFailure));
|
|
110
175
|
|
|
111
176
|
finalText = stepText;
|
|
112
177
|
messages.push(toolMode === 'promptJson'
|
|
@@ -127,7 +192,8 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
127
192
|
continue;
|
|
128
193
|
}
|
|
129
194
|
let args = validation.args;
|
|
130
|
-
|
|
195
|
+
const requiresApproval = tool.sideEffects || opts.approvalMode === 'all';
|
|
196
|
+
if (requiresApproval) {
|
|
131
197
|
if (!opts.approval) {
|
|
132
198
|
yield* appendToolDenied(opts, messages, step, call, 'No approval gate configured');
|
|
133
199
|
continue;
|
|
@@ -165,8 +231,16 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
165
231
|
continue;
|
|
166
232
|
}
|
|
167
233
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
234
|
+
// Guard against a tool returning `undefined` (JSON.stringify(undefined)
|
|
235
|
+
// is the value `undefined`, not a string) — an unguarded value here
|
|
236
|
+
// used to crash the next step's message mapper and kill the whole run.
|
|
237
|
+
const serialized = JSON.stringify(executed.result) ?? 'null';
|
|
238
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: formatToolResult(serialized, opts.toolResult) });
|
|
239
|
+
// A tool that returns a structured `{ ok: false, ... }` is a recoverable
|
|
240
|
+
// error the model should react to — surface it as an error result.
|
|
241
|
+
const record = asRecord(executed.result);
|
|
242
|
+
const isError = record?.ok === false;
|
|
243
|
+
yield emit(opts, { type: 'tool_result', step, call, result: executed.result, isError });
|
|
170
244
|
} catch (error) {
|
|
171
245
|
const message = error instanceof Error ? error.message : 'Tool result was not serializable';
|
|
172
246
|
yield* appendToolError(opts, messages, step, call, message);
|
|
@@ -179,7 +253,8 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
179
253
|
}
|
|
180
254
|
return yield* yieldDone('max_steps');
|
|
181
255
|
} catch (error) {
|
|
182
|
-
const
|
|
256
|
+
const stopReason = agentSignal.timedOut() ? 'deadline' : (agentSignal.signal?.aborted || (error instanceof AIError && error.kind === 'canceled')) ? 'canceled' : 'error';
|
|
257
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, stopReason === 'error' ? errorInfo(error) : undefined);
|
|
183
258
|
settled = true;
|
|
184
259
|
resolveResult(result);
|
|
185
260
|
yield emit(opts, { type: 'agent_done', result });
|
|
@@ -191,8 +266,8 @@ async function* run(opts: AgentOptions, resolveResult: (result: AgentResult) =>
|
|
|
191
266
|
}
|
|
192
267
|
}
|
|
193
268
|
|
|
194
|
-
function* yieldDone(stopReason: AgentResult['stopReason']): Generator<AgentEvent> {
|
|
195
|
-
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason);
|
|
269
|
+
function* yieldDone(stopReason: AgentResult['stopReason'], error?: AgentResult['error']): Generator<AgentEvent> {
|
|
270
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, error);
|
|
196
271
|
settled = true;
|
|
197
272
|
resolveResult(result);
|
|
198
273
|
yield emit(opts, { type: 'agent_done', result });
|
|
@@ -204,8 +279,68 @@ function emit<T extends AgentEvent>(opts: AgentOptions, event: T): T {
|
|
|
204
279
|
return event;
|
|
205
280
|
}
|
|
206
281
|
|
|
207
|
-
function makeResult(finalText: string, messages: ChatMessage[], usage: Usage, steps: number, stopReason: AgentResult['stopReason']): AgentResult {
|
|
208
|
-
return { finalText, messages, usage, steps, stopReason };
|
|
282
|
+
function makeResult(finalText: string, messages: ChatMessage[], usage: Usage, steps: number, stopReason: AgentResult['stopReason'], error?: AgentResult['error']): AgentResult {
|
|
283
|
+
return error ? { finalText, messages, usage, steps, stopReason, error } : { finalText, messages, usage, steps, stopReason };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function errorInfo(error: unknown): { kind: AIErrorKind; message: string } {
|
|
287
|
+
if (error instanceof AIError) return { kind: error.kind, message: error.message };
|
|
288
|
+
return { kind: 'tool_error', message: error instanceof Error ? error.message : 'Agent run failed' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Applies the optional size cap and untrusted-content framing to a serialized
|
|
293
|
+
* tool result before it re-enters the model's context.
|
|
294
|
+
*/
|
|
295
|
+
function formatToolResult(serialized: string, opts: AgentOptions['toolResult']): string {
|
|
296
|
+
let out = serialized;
|
|
297
|
+
const max = opts?.maxChars;
|
|
298
|
+
if (max !== undefined && out.length > max) {
|
|
299
|
+
out = `${out.slice(0, max)}\n…[truncated ${out.length - max} chars]`;
|
|
300
|
+
}
|
|
301
|
+
if (opts?.wrapUntrusted) {
|
|
302
|
+
out = `<untrusted_tool_output>\n${out}\n</untrusted_tool_output>`;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Withholds a promptJson tool directive from the visible `delta` stream. Once
|
|
309
|
+
* the leading non-whitespace character is known: a `{`, `[`, or fence marks a
|
|
310
|
+
* directive candidate whose text is buffered (never streamed); anything else is
|
|
311
|
+
* prose that streams through untouched. `flush()` returns any buffered text when
|
|
312
|
+
* the stream ended without yielding a parsed directive.
|
|
313
|
+
*/
|
|
314
|
+
class PromptJsonDeltaFilter {
|
|
315
|
+
private decided = false;
|
|
316
|
+
private withhold = false;
|
|
317
|
+
private buffer = '';
|
|
318
|
+
|
|
319
|
+
push(text: string): string {
|
|
320
|
+
if (!this.decided) {
|
|
321
|
+
this.buffer += text;
|
|
322
|
+
const lead = this.buffer.replace(/^\s+/, '');
|
|
323
|
+
if (lead.length === 0) return '';
|
|
324
|
+
this.decided = true;
|
|
325
|
+
this.withhold = /^[[{`]/.test(lead);
|
|
326
|
+
if (this.withhold) return '';
|
|
327
|
+
const flushed = this.buffer;
|
|
328
|
+
this.buffer = '';
|
|
329
|
+
return flushed;
|
|
330
|
+
}
|
|
331
|
+
if (this.withhold) {
|
|
332
|
+
this.buffer += text;
|
|
333
|
+
return '';
|
|
334
|
+
}
|
|
335
|
+
return text;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
flush(): string {
|
|
339
|
+
if (!this.withhold) return '';
|
|
340
|
+
const out = this.buffer;
|
|
341
|
+
this.buffer = '';
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
209
344
|
}
|
|
210
345
|
|
|
211
346
|
function* appendToolError(opts: AgentOptions, messages: ChatMessage[], step: number, call: ToolCall, message: string): Generator<AgentEvent> {
|
|
@@ -222,14 +357,22 @@ function* appendToolDenied(opts: AgentOptions, messages: ChatMessage[], step: nu
|
|
|
222
357
|
function resolveToolMode(opts: AgentOptions): 'native' | 'promptJson' {
|
|
223
358
|
const mode = opts.toolMode ?? 'auto';
|
|
224
359
|
if (mode !== 'auto') return mode;
|
|
360
|
+
// A model that actually advertises tool support gets native tool-calling even
|
|
361
|
+
// on a local runtime whose profile is conservatively `nativeTools: false`.
|
|
362
|
+
if (opts.modelCapabilities?.includes('tools')) return 'native';
|
|
225
363
|
return profileFor(opts.connection.provider, opts.connection.baseUrl).capabilities.nativeTools ? 'native' : 'promptJson';
|
|
226
364
|
}
|
|
227
365
|
|
|
228
366
|
function withPromptJsonInstruction(messages: ChatMessage[], tools: ToolSpec[]): ChatMessage[] {
|
|
367
|
+
// Include each tool's parameter schema, not just its description, so the model
|
|
368
|
+
// knows the argument shape instead of guessing it (FR‑3).
|
|
369
|
+
const catalog = tools
|
|
370
|
+
.map((tool) => `- ${tool.name}: ${tool.description}\n parameters: ${JSON.stringify(tool.parameters)}`)
|
|
371
|
+
.join('\n');
|
|
229
372
|
return [
|
|
230
373
|
{
|
|
231
374
|
role: 'system',
|
|
232
|
-
content: `When you need tools, respond only with JSON. For one tool: {"tool":"name","input":{...}}. For several in one turn: {"tools":[{"tool":"name","input":{...}}]}.
|
|
375
|
+
content: `When you need tools, respond only with JSON. For one tool: {"tool":"name","input":{...}}. For several in one turn: {"tools":[{"tool":"name","input":{...}}]}. The "input" object must match the tool's parameters schema. Available tools:\n${catalog}`,
|
|
233
376
|
},
|
|
234
377
|
...messages.map(toPromptJsonMessage),
|
|
235
378
|
];
|
package/nugget/src/errors.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { createDefaultRedactor } from './redact.js';
|
|
1
2
|
import type { AIErrorKind } from './types.js';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Pattern-based redaction applied at the wire boundary, before an error object
|
|
6
|
+
* exists. `AIHandler` redacts again on the way out (adding session-registered
|
|
7
|
+
* exact secrets), but doing it here means a provider body that echoes a
|
|
8
|
+
* recognizable secret can never be carried by an `AIError` in the first place —
|
|
9
|
+
* including on code paths that never reach the handler's own catch blocks.
|
|
10
|
+
*/
|
|
11
|
+
const wireRedactor = createDefaultRedactor();
|
|
12
|
+
|
|
3
13
|
export class AIError extends Error {
|
|
4
14
|
kind: AIErrorKind;
|
|
5
15
|
status?: number;
|
|
@@ -34,9 +44,13 @@ export function defaultRetryable(kind: AIErrorKind): boolean {
|
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export function classify(status: number, body = '', provider?: string, headers?: Headers): AIError {
|
|
37
|
-
const excerpt = body.slice(0, 200);
|
|
47
|
+
const excerpt = wireRedactor.redact(body.slice(0, 200));
|
|
38
48
|
const lower = body.toLowerCase();
|
|
39
49
|
let kind: AIErrorKind = 'server';
|
|
50
|
+
// 403 is classified as `auth` (non-retryable) deliberately: a forbidden
|
|
51
|
+
// response almost always means a bad/insufficient credential or a blocked
|
|
52
|
+
// region/model, none of which a retry fixes. Callers that know a specific
|
|
53
|
+
// provider returns a retryable 403 can reclassify in a `beforeCall`/adapter.
|
|
40
54
|
if (status === 401 || status === 403) kind = 'auth';
|
|
41
55
|
else if (status === 408) kind = 'timeout';
|
|
42
56
|
else if (status === 429) kind = 'rate_limit';
|
|
@@ -44,7 +58,17 @@ export function classify(status: number, body = '', provider?: string, headers?:
|
|
|
44
58
|
kind = lower.includes('context') || lower.includes('maximum context') || lower.includes('token limit')
|
|
45
59
|
? 'context_length'
|
|
46
60
|
: 'invalid_request';
|
|
61
|
+
} else if (status === 404 || status === 410) {
|
|
62
|
+
// A missing route/deployment is not a malformed request: it is usually a
|
|
63
|
+
// wrong baseUrl or model name, and sometimes a temporarily misrouted load
|
|
64
|
+
// balancer. `not_found` keeps it distinguishable from `invalid_request` so
|
|
65
|
+
// apps don't tell a user "your request was invalid" for an infra problem.
|
|
66
|
+
// Non-retryable by default — a caller that knows its 404s are transient can
|
|
67
|
+
// retry on `kind === 'not_found'` itself.
|
|
68
|
+
kind = 'not_found';
|
|
47
69
|
} else if (status >= 500) kind = 'server';
|
|
70
|
+
// Everything else in 4xx (409 conflict, 405, 415, 451, …) is a genuine
|
|
71
|
+
// client-side request problem, so `invalid_request` is honest for it.
|
|
48
72
|
else kind = 'invalid_request';
|
|
49
73
|
|
|
50
74
|
return new AIError(`HTTP ${status}: ${excerpt}`, {
|