@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
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAgent = runAgent;
|
|
4
|
+
const profiles_js_1 = require("../adapters/profiles.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const json_js_1 = require("../json.js");
|
|
7
|
+
const tokens_js_1 = require("../tokens.js");
|
|
8
|
+
const util_js_1 = require("../util.js");
|
|
9
|
+
const tools_js_1 = require("./tools.js");
|
|
10
|
+
function runAgent(opts) {
|
|
11
|
+
let resolveResult;
|
|
12
|
+
const result = new Promise((resolve) => {
|
|
13
|
+
resolveResult = resolve;
|
|
14
|
+
});
|
|
15
|
+
const iterable = run(opts, resolveResult);
|
|
16
|
+
return Object.assign(iterable, { result });
|
|
17
|
+
}
|
|
18
|
+
async function* run(opts, resolveResult) {
|
|
19
|
+
const messages = [...opts.messages];
|
|
20
|
+
const toolMode = resolveToolMode(opts);
|
|
21
|
+
const maxSteps = opts.budget?.maxSteps ?? 8;
|
|
22
|
+
const agentSignal = createAgentSignal(opts.signal, opts.budget?.deadlineMs);
|
|
23
|
+
let usage;
|
|
24
|
+
let finalText = '';
|
|
25
|
+
let step = 0;
|
|
26
|
+
let settled = false;
|
|
27
|
+
// Disclose the resolved tool protocol once up front (a silent promptJson
|
|
28
|
+
// fallback was the friction behind FR‑2).
|
|
29
|
+
yield emit(opts, { type: 'tool_mode', mode: toolMode });
|
|
30
|
+
try {
|
|
31
|
+
while (step < maxSteps) {
|
|
32
|
+
step += 1;
|
|
33
|
+
if (agentSignal.signal?.aborted)
|
|
34
|
+
return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : 'canceled');
|
|
35
|
+
yield emit(opts, { type: 'step_start', step });
|
|
36
|
+
const calls = [];
|
|
37
|
+
let stepText = '';
|
|
38
|
+
let streamFailure;
|
|
39
|
+
// In promptJson mode a tool directive must not leak into the visible
|
|
40
|
+
// stream, so directive-looking text is withheld from `delta` events (still
|
|
41
|
+
// accumulated in `stepText` for parsing). Prose streams normally.
|
|
42
|
+
const filter = toolMode === 'promptJson' ? new PromptJsonDeltaFilter() : undefined;
|
|
43
|
+
const requestMessages = toolMode === 'promptJson' ? withPromptJsonInstruction(messages, opts.tools) : messages;
|
|
44
|
+
for await (const event of opts.handler.stream(opts.connection, {
|
|
45
|
+
model: opts.model,
|
|
46
|
+
messages: requestMessages,
|
|
47
|
+
tools: toolMode === 'promptJson' ? undefined : opts.tools,
|
|
48
|
+
temperature: opts.temperature,
|
|
49
|
+
maxTokens: opts.maxTokens,
|
|
50
|
+
topP: opts.topP,
|
|
51
|
+
stopSequences: opts.stopSequences,
|
|
52
|
+
reasoningEffort: opts.reasoningEffort,
|
|
53
|
+
providerOptions: opts.providerOptions,
|
|
54
|
+
signal: agentSignal.signal,
|
|
55
|
+
metadata: { ...opts.metadata, agentStep: step },
|
|
56
|
+
})) {
|
|
57
|
+
if (event.type === 'delta') {
|
|
58
|
+
stepText += event.text;
|
|
59
|
+
if (filter) {
|
|
60
|
+
const visible = filter.push(event.text);
|
|
61
|
+
if (visible)
|
|
62
|
+
yield emit(opts, { type: 'delta', text: visible });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (event.type === 'tool_call')
|
|
67
|
+
calls.push(event.call);
|
|
68
|
+
if (event.type === 'error')
|
|
69
|
+
streamFailure = event.error;
|
|
70
|
+
if (event.type === 'done') {
|
|
71
|
+
usage = usage ? (0, tokens_js_1.mergeUsage)(usage, event.result.usage) : event.result.usage;
|
|
72
|
+
if (toolMode === 'promptJson' && calls.length === 0)
|
|
73
|
+
calls.push(...callsFromPromptJson(stepText));
|
|
74
|
+
}
|
|
75
|
+
yield emit(opts, event);
|
|
76
|
+
}
|
|
77
|
+
// If withheld text turned out not to be a real directive, flush it so the
|
|
78
|
+
// user still sees the model's answer.
|
|
79
|
+
if (filter && calls.length === 0) {
|
|
80
|
+
const flushed = filter.flush();
|
|
81
|
+
if (flushed)
|
|
82
|
+
yield emit(opts, { type: 'delta', text: flushed });
|
|
83
|
+
}
|
|
84
|
+
// A handler-level failure (auth, policy, cancel, exhausted retries) must
|
|
85
|
+
// stop the loop honestly rather than looking like an empty completion.
|
|
86
|
+
if (streamFailure)
|
|
87
|
+
return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error', errorInfo(streamFailure));
|
|
88
|
+
finalText = stepText;
|
|
89
|
+
messages.push(toolMode === 'promptJson'
|
|
90
|
+
? { role: 'assistant', content: stepText }
|
|
91
|
+
: { role: 'assistant', content: stepText, toolCalls: calls.length ? calls : undefined });
|
|
92
|
+
if (calls.length === 0)
|
|
93
|
+
return yield* yieldDone('finished');
|
|
94
|
+
for (const call of calls) {
|
|
95
|
+
if (agentSignal.signal?.aborted)
|
|
96
|
+
return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : 'canceled');
|
|
97
|
+
const tool = opts.tools.find((candidate) => candidate.name === call.name);
|
|
98
|
+
if (!tool) {
|
|
99
|
+
yield* appendToolError(opts, messages, step, call, `Unknown tool: ${call.name}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const validation = (0, tools_js_1.validateToolArgs)(tool, call);
|
|
103
|
+
if (!validation.ok) {
|
|
104
|
+
yield* appendToolError(opts, messages, step, call, validation.message);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
let args = validation.args;
|
|
108
|
+
const requiresApproval = tool.sideEffects || opts.approvalMode === 'all';
|
|
109
|
+
if (requiresApproval) {
|
|
110
|
+
if (!opts.approval) {
|
|
111
|
+
yield* appendToolDenied(opts, messages, step, call, 'No approval gate configured');
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const approval = await opts.approval({ call, tool, step });
|
|
115
|
+
if (agentSignal.signal?.aborted)
|
|
116
|
+
return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : 'canceled');
|
|
117
|
+
if (approval === 'deny') {
|
|
118
|
+
yield* appendToolDenied(opts, messages, step, call, 'Denied by approval gate');
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (approval !== 'allow') {
|
|
122
|
+
const modified = (0, tools_js_1.validateToolArgs)(tool, { ...call, arguments: approval.modifiedArguments });
|
|
123
|
+
if (!modified.ok) {
|
|
124
|
+
yield* appendToolError(opts, messages, step, call, modified.message);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
args = modified.args;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
yield emit(opts, { type: 'tool_start', step, call });
|
|
131
|
+
let executed;
|
|
132
|
+
try {
|
|
133
|
+
const result = await tool.execute(args, {
|
|
134
|
+
signal: agentSignal.signal ?? new AbortController().signal,
|
|
135
|
+
callId: call.id,
|
|
136
|
+
step,
|
|
137
|
+
metadata: opts.metadata,
|
|
138
|
+
});
|
|
139
|
+
executed = { ok: true, result };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
executed = { ok: false, message: error instanceof Error ? error.message : 'Tool failed' };
|
|
143
|
+
}
|
|
144
|
+
if (!executed.ok) {
|
|
145
|
+
yield* appendToolError(opts, messages, step, call, executed.message);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
// Guard against a tool returning `undefined` (JSON.stringify(undefined)
|
|
150
|
+
// is the value `undefined`, not a string) — an unguarded value here
|
|
151
|
+
// used to crash the next step's message mapper and kill the whole run.
|
|
152
|
+
const serialized = JSON.stringify(executed.result) ?? 'null';
|
|
153
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: formatToolResult(serialized, opts.toolResult) });
|
|
154
|
+
// A tool that returns a structured `{ ok: false, ... }` is a recoverable
|
|
155
|
+
// error the model should react to — surface it as an error result.
|
|
156
|
+
const record = (0, util_js_1.asRecord)(executed.result);
|
|
157
|
+
const isError = record?.ok === false;
|
|
158
|
+
yield emit(opts, { type: 'tool_result', step, call, result: executed.result, isError });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : 'Tool result was not serializable';
|
|
162
|
+
yield* appendToolError(opts, messages, step, call, message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const totalTokens = (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0);
|
|
166
|
+
if (opts.budget?.maxTokens && totalTokens > opts.budget.maxTokens)
|
|
167
|
+
return yield* yieldDone('budget');
|
|
168
|
+
await (0, util_js_1.sleep)(0, agentSignal.signal);
|
|
169
|
+
}
|
|
170
|
+
return yield* yieldDone('max_steps');
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const stopReason = agentSignal.timedOut() ? 'deadline' : (agentSignal.signal?.aborted || (error instanceof errors_js_1.AIError && error.kind === 'canceled')) ? 'canceled' : 'error';
|
|
174
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, stopReason === 'error' ? errorInfo(error) : undefined);
|
|
175
|
+
settled = true;
|
|
176
|
+
resolveResult(result);
|
|
177
|
+
yield emit(opts, { type: 'agent_done', result });
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
agentSignal.dispose();
|
|
181
|
+
if (!settled) {
|
|
182
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, 'canceled');
|
|
183
|
+
resolveResult(result);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function* yieldDone(stopReason, error) {
|
|
187
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, error);
|
|
188
|
+
settled = true;
|
|
189
|
+
resolveResult(result);
|
|
190
|
+
yield emit(opts, { type: 'agent_done', result });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function emit(opts, event) {
|
|
194
|
+
opts.onEvent?.(event);
|
|
195
|
+
return event;
|
|
196
|
+
}
|
|
197
|
+
function makeResult(finalText, messages, usage, steps, stopReason, error) {
|
|
198
|
+
return error ? { finalText, messages, usage, steps, stopReason, error } : { finalText, messages, usage, steps, stopReason };
|
|
199
|
+
}
|
|
200
|
+
function errorInfo(error) {
|
|
201
|
+
if (error instanceof errors_js_1.AIError)
|
|
202
|
+
return { kind: error.kind, message: error.message };
|
|
203
|
+
return { kind: 'tool_error', message: error instanceof Error ? error.message : 'Agent run failed' };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Applies the optional size cap and untrusted-content framing to a serialized
|
|
207
|
+
* tool result before it re-enters the model's context.
|
|
208
|
+
*/
|
|
209
|
+
function formatToolResult(serialized, opts) {
|
|
210
|
+
let out = serialized;
|
|
211
|
+
const max = opts?.maxChars;
|
|
212
|
+
if (max !== undefined && out.length > max) {
|
|
213
|
+
out = `${out.slice(0, max)}\n…[truncated ${out.length - max} chars]`;
|
|
214
|
+
}
|
|
215
|
+
if (opts?.wrapUntrusted) {
|
|
216
|
+
out = `<untrusted_tool_output>\n${out}\n</untrusted_tool_output>`;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Withholds a promptJson tool directive from the visible `delta` stream. Once
|
|
222
|
+
* the leading non-whitespace character is known: a `{`, `[`, or fence marks a
|
|
223
|
+
* directive candidate whose text is buffered (never streamed); anything else is
|
|
224
|
+
* prose that streams through untouched. `flush()` returns any buffered text when
|
|
225
|
+
* the stream ended without yielding a parsed directive.
|
|
226
|
+
*/
|
|
227
|
+
class PromptJsonDeltaFilter {
|
|
228
|
+
decided = false;
|
|
229
|
+
withhold = false;
|
|
230
|
+
buffer = '';
|
|
231
|
+
push(text) {
|
|
232
|
+
if (!this.decided) {
|
|
233
|
+
this.buffer += text;
|
|
234
|
+
const lead = this.buffer.replace(/^\s+/, '');
|
|
235
|
+
if (lead.length === 0)
|
|
236
|
+
return '';
|
|
237
|
+
this.decided = true;
|
|
238
|
+
this.withhold = /^[[{`]/.test(lead);
|
|
239
|
+
if (this.withhold)
|
|
240
|
+
return '';
|
|
241
|
+
const flushed = this.buffer;
|
|
242
|
+
this.buffer = '';
|
|
243
|
+
return flushed;
|
|
244
|
+
}
|
|
245
|
+
if (this.withhold) {
|
|
246
|
+
this.buffer += text;
|
|
247
|
+
return '';
|
|
248
|
+
}
|
|
249
|
+
return text;
|
|
250
|
+
}
|
|
251
|
+
flush() {
|
|
252
|
+
if (!this.withhold)
|
|
253
|
+
return '';
|
|
254
|
+
const out = this.buffer;
|
|
255
|
+
this.buffer = '';
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function* appendToolError(opts, messages, step, call, message) {
|
|
260
|
+
const result = { error: message };
|
|
261
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: JSON.stringify(result) });
|
|
262
|
+
yield emit(opts, { type: 'tool_result', step, call, result, isError: true });
|
|
263
|
+
}
|
|
264
|
+
function* appendToolDenied(opts, messages, step, call, reason) {
|
|
265
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: JSON.stringify({ denied: true, reason }) });
|
|
266
|
+
yield emit(opts, { type: 'tool_denied', step, call, reason });
|
|
267
|
+
}
|
|
268
|
+
function resolveToolMode(opts) {
|
|
269
|
+
const mode = opts.toolMode ?? 'auto';
|
|
270
|
+
if (mode !== 'auto')
|
|
271
|
+
return mode;
|
|
272
|
+
// A model that actually advertises tool support gets native tool-calling even
|
|
273
|
+
// on a local runtime whose profile is conservatively `nativeTools: false`.
|
|
274
|
+
if (opts.modelCapabilities?.includes('tools'))
|
|
275
|
+
return 'native';
|
|
276
|
+
return (0, profiles_js_1.profileFor)(opts.connection.provider, opts.connection.baseUrl).capabilities.nativeTools ? 'native' : 'promptJson';
|
|
277
|
+
}
|
|
278
|
+
function withPromptJsonInstruction(messages, tools) {
|
|
279
|
+
// Include each tool's parameter schema, not just its description, so the model
|
|
280
|
+
// knows the argument shape instead of guessing it (FR‑3).
|
|
281
|
+
const catalog = tools
|
|
282
|
+
.map((tool) => `- ${tool.name}: ${tool.description}\n parameters: ${JSON.stringify(tool.parameters)}`)
|
|
283
|
+
.join('\n');
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
role: 'system',
|
|
287
|
+
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}`,
|
|
288
|
+
},
|
|
289
|
+
...messages.map(toPromptJsonMessage),
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
function toPromptJsonMessage(message) {
|
|
293
|
+
if (message.role === 'tool') {
|
|
294
|
+
return {
|
|
295
|
+
role: 'user',
|
|
296
|
+
content: `Tool ${message.name ?? message.toolCallId ?? 'unknown'} returned: ${textContent(message.content)}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
role: message.role,
|
|
301
|
+
content: textContent(message.content),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Parses a promptJson tool directive into zero or more calls. Accepts the single
|
|
306
|
+
* form ({"tool","input"}), the batched form ({"tools":[...]}), and a bare array
|
|
307
|
+
* of directives, so a promptJson-mode model can request several tools per turn
|
|
308
|
+
* just like native tool-calling can. Malformed entries are skipped, not thrown.
|
|
309
|
+
*/
|
|
310
|
+
function callsFromPromptJson(text) {
|
|
311
|
+
const value = (0, json_js_1.extractJson)(text);
|
|
312
|
+
if (!value || typeof value !== 'object')
|
|
313
|
+
return [];
|
|
314
|
+
const entries = Array.isArray(value)
|
|
315
|
+
? value
|
|
316
|
+
: Array.isArray(value.tools)
|
|
317
|
+
? value.tools
|
|
318
|
+
: [value];
|
|
319
|
+
const calls = [];
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
322
|
+
continue;
|
|
323
|
+
const record = entry;
|
|
324
|
+
if (typeof record.tool !== 'string')
|
|
325
|
+
continue;
|
|
326
|
+
calls.push({
|
|
327
|
+
id: randomId(),
|
|
328
|
+
name: record.tool,
|
|
329
|
+
arguments: record.input && typeof record.input === 'object' ? record.input : {},
|
|
330
|
+
raw: JSON.stringify(record.input ?? {}),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return calls;
|
|
334
|
+
}
|
|
335
|
+
function randomId() {
|
|
336
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
337
|
+
}
|
|
338
|
+
function textContent(content) {
|
|
339
|
+
if (typeof content === 'string')
|
|
340
|
+
return content;
|
|
341
|
+
if (!content)
|
|
342
|
+
return '';
|
|
343
|
+
return content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n');
|
|
344
|
+
}
|
|
345
|
+
function createAgentSignal(signal, deadlineMs) {
|
|
346
|
+
if (!signal && deadlineMs === undefined)
|
|
347
|
+
return { signal: undefined, timedOut: () => false, dispose: () => undefined };
|
|
348
|
+
const controller = new AbortController();
|
|
349
|
+
let didTimeOut = false;
|
|
350
|
+
let timer;
|
|
351
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
352
|
+
if (signal) {
|
|
353
|
+
if (signal.aborted)
|
|
354
|
+
onAbort();
|
|
355
|
+
else
|
|
356
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
357
|
+
}
|
|
358
|
+
if (deadlineMs !== undefined) {
|
|
359
|
+
timer = setTimeout(() => {
|
|
360
|
+
didTimeOut = true;
|
|
361
|
+
controller.abort(new errors_js_1.AIError(`Agent deadline exceeded after ${deadlineMs}ms`, { kind: 'budget_exceeded', retryable: false }));
|
|
362
|
+
}, deadlineMs);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
signal: controller.signal,
|
|
366
|
+
timedOut: () => didTimeOut,
|
|
367
|
+
dispose() {
|
|
368
|
+
if (timer)
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
signal?.removeEventListener('abort', onAbort);
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineTool = defineTool;
|
|
4
|
+
exports.validateToolArgs = validateToolArgs;
|
|
5
|
+
function defineTool(spec) {
|
|
6
|
+
return spec;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Light validation only: object-ness, `required` presence, and top-level
|
|
10
|
+
* `properties[key].type` against JS typeof/Array.isArray/Number.isInteger.
|
|
11
|
+
* It does not implement full JSON Schema — no `enum`, nested `properties`,
|
|
12
|
+
* `oneOf`/`anyOf`, numeric bounds, string `pattern`, array `items` schemas, or
|
|
13
|
+
* `additionalProperties`. That's intentional for a zero-dependency nugget; a
|
|
14
|
+
* caller that needs stricter guarantees should validate `args` again inside
|
|
15
|
+
* `tool.execute` with its own schema library.
|
|
16
|
+
*/
|
|
17
|
+
function validateToolArgs(tool, call) {
|
|
18
|
+
const schema = tool.parameters;
|
|
19
|
+
if (schema.type && schema.type !== 'object')
|
|
20
|
+
return { ok: false, message: `Tool ${tool.name} parameters must be an object schema` };
|
|
21
|
+
if (typeof call.arguments !== 'object' || call.arguments === null || Array.isArray(call.arguments)) {
|
|
22
|
+
return { ok: false, message: `Tool ${tool.name} expected object arguments` };
|
|
23
|
+
}
|
|
24
|
+
const args = call.arguments;
|
|
25
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((key) => typeof key === 'string') : [];
|
|
26
|
+
for (const key of required) {
|
|
27
|
+
if (!(key in args))
|
|
28
|
+
return { ok: false, message: `Tool ${tool.name} missing required argument: ${key}` };
|
|
29
|
+
}
|
|
30
|
+
const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
|
|
31
|
+
for (const [key, spec] of Object.entries(properties)) {
|
|
32
|
+
if (!(key in args) || spec.type === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
const value = args[key];
|
|
35
|
+
const ok = matchesType(value, spec.type);
|
|
36
|
+
if (!ok)
|
|
37
|
+
return { ok: false, message: `Tool ${tool.name} argument ${key} must be ${spec.type}` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, args };
|
|
40
|
+
}
|
|
41
|
+
function matchesType(value, type) {
|
|
42
|
+
if (type === 'array')
|
|
43
|
+
return Array.isArray(value);
|
|
44
|
+
if (type === 'integer')
|
|
45
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
46
|
+
if (type === 'null')
|
|
47
|
+
return value === null;
|
|
48
|
+
if (type === 'object')
|
|
49
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
|
+
return typeof value === type;
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.envConnection = envConnection;
|
|
4
|
+
const util_js_1 = require("./util.js");
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a server-side `Connection` plus a default `model` from app-owned
|
|
7
|
+
* env vars (`AI_PROVIDER`, `AI_MODEL`, `AI_KEY_ENV`, `AI_BASE_URL` unless
|
|
8
|
+
* overridden). This only collapses the config-reading boilerplate that was
|
|
9
|
+
* being hand-rolled the same way in every small server app; it does not add
|
|
10
|
+
* policy. `provider`/`baseUrl` still come from the server's own environment,
|
|
11
|
+
* never from client input, and the returned `keyRef` still resolves through
|
|
12
|
+
* `KeySource` at call time like any other `Connection`.
|
|
13
|
+
*/
|
|
14
|
+
function envConnection(options = {}) {
|
|
15
|
+
const env = options.env ?? (0, util_js_1.globalEnv)();
|
|
16
|
+
const provider = env[options.providerVar ?? 'AI_PROVIDER']?.trim() || options.defaultProvider || 'openai';
|
|
17
|
+
const model = env[options.modelVar ?? 'AI_MODEL']?.trim() || options.defaultModel || 'gpt-4o-mini';
|
|
18
|
+
const keyEnvName = env[options.keyEnvVar ?? 'AI_KEY_ENV']?.trim() || options.defaultKeyEnv || 'OPENAI_API_KEY';
|
|
19
|
+
const baseUrl = env[options.baseUrlVar ?? 'AI_BASE_URL']?.trim();
|
|
20
|
+
const connection = {
|
|
21
|
+
id: options.id ?? 'app',
|
|
22
|
+
provider,
|
|
23
|
+
keyRef: { kind: 'env', name: keyEnvName },
|
|
24
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
25
|
+
};
|
|
26
|
+
return { connection, model };
|
|
27
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AIError = void 0;
|
|
4
|
+
exports.defaultRetryable = defaultRetryable;
|
|
5
|
+
exports.classify = classify;
|
|
6
|
+
exports.fromUnknown = fromUnknown;
|
|
7
|
+
const redact_js_1 = require("./redact.js");
|
|
8
|
+
/**
|
|
9
|
+
* Pattern-based redaction applied at the wire boundary, before an error object
|
|
10
|
+
* exists. `AIHandler` redacts again on the way out (adding session-registered
|
|
11
|
+
* exact secrets), but doing it here means a provider body that echoes a
|
|
12
|
+
* recognizable secret can never be carried by an `AIError` in the first place —
|
|
13
|
+
* including on code paths that never reach the handler's own catch blocks.
|
|
14
|
+
*/
|
|
15
|
+
const wireRedactor = (0, redact_js_1.createDefaultRedactor)();
|
|
16
|
+
class AIError extends Error {
|
|
17
|
+
kind;
|
|
18
|
+
status;
|
|
19
|
+
retryable;
|
|
20
|
+
provider;
|
|
21
|
+
raw;
|
|
22
|
+
retryAfterMs;
|
|
23
|
+
constructor(message, opts) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'AIError';
|
|
26
|
+
this.kind = opts.kind;
|
|
27
|
+
this.retryable = opts.retryable ?? defaultRetryable(opts.kind);
|
|
28
|
+
if (opts.status !== undefined)
|
|
29
|
+
this.status = opts.status;
|
|
30
|
+
if (opts.provider !== undefined)
|
|
31
|
+
this.provider = opts.provider;
|
|
32
|
+
if (opts.raw !== undefined)
|
|
33
|
+
this.raw = opts.raw;
|
|
34
|
+
if (opts.retryAfterMs !== undefined)
|
|
35
|
+
this.retryAfterMs = opts.retryAfterMs;
|
|
36
|
+
if (opts.cause !== undefined)
|
|
37
|
+
this.cause = opts.cause;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.AIError = AIError;
|
|
41
|
+
function defaultRetryable(kind) {
|
|
42
|
+
return kind === 'rate_limit' || kind === 'timeout' || kind === 'network' || kind === 'server' || kind === 'invalid_response';
|
|
43
|
+
}
|
|
44
|
+
function classify(status, body = '', provider, headers) {
|
|
45
|
+
const excerpt = wireRedactor.redact(body.slice(0, 200));
|
|
46
|
+
const lower = body.toLowerCase();
|
|
47
|
+
let kind = 'server';
|
|
48
|
+
// 403 is classified as `auth` (non-retryable) deliberately: a forbidden
|
|
49
|
+
// response almost always means a bad/insufficient credential or a blocked
|
|
50
|
+
// region/model, none of which a retry fixes. Callers that know a specific
|
|
51
|
+
// provider returns a retryable 403 can reclassify in a `beforeCall`/adapter.
|
|
52
|
+
if (status === 401 || status === 403)
|
|
53
|
+
kind = 'auth';
|
|
54
|
+
else if (status === 408)
|
|
55
|
+
kind = 'timeout';
|
|
56
|
+
else if (status === 429)
|
|
57
|
+
kind = 'rate_limit';
|
|
58
|
+
else if (status === 400 || status === 422) {
|
|
59
|
+
kind = lower.includes('context') || lower.includes('maximum context') || lower.includes('token limit')
|
|
60
|
+
? 'context_length'
|
|
61
|
+
: 'invalid_request';
|
|
62
|
+
}
|
|
63
|
+
else if (status === 404 || status === 410) {
|
|
64
|
+
// A missing route/deployment is not a malformed request: it is usually a
|
|
65
|
+
// wrong baseUrl or model name, and sometimes a temporarily misrouted load
|
|
66
|
+
// balancer. `not_found` keeps it distinguishable from `invalid_request` so
|
|
67
|
+
// apps don't tell a user "your request was invalid" for an infra problem.
|
|
68
|
+
// Non-retryable by default — a caller that knows its 404s are transient can
|
|
69
|
+
// retry on `kind === 'not_found'` itself.
|
|
70
|
+
kind = 'not_found';
|
|
71
|
+
}
|
|
72
|
+
else if (status >= 500)
|
|
73
|
+
kind = 'server';
|
|
74
|
+
// Everything else in 4xx (409 conflict, 405, 415, 451, …) is a genuine
|
|
75
|
+
// client-side request problem, so `invalid_request` is honest for it.
|
|
76
|
+
else
|
|
77
|
+
kind = 'invalid_request';
|
|
78
|
+
return new AIError(`HTTP ${status}: ${excerpt}`, {
|
|
79
|
+
kind,
|
|
80
|
+
status,
|
|
81
|
+
provider,
|
|
82
|
+
raw: excerpt,
|
|
83
|
+
retryAfterMs: parseRetryAfter(headers?.get('retry-after') ?? null),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function fromUnknown(error, provider) {
|
|
87
|
+
if (error instanceof AIError)
|
|
88
|
+
return error;
|
|
89
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
90
|
+
return new AIError('Request was canceled', { kind: 'canceled', retryable: false, provider, cause: error });
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof Error && /timeout|timed out/i.test(error.message)) {
|
|
93
|
+
return new AIError(error.message, { kind: 'timeout', provider, cause: error });
|
|
94
|
+
}
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
return new AIError(error.message, { kind: 'network', provider, cause: error });
|
|
97
|
+
}
|
|
98
|
+
return new AIError('Unknown AI provider error', { kind: 'network', provider, cause: error });
|
|
99
|
+
}
|
|
100
|
+
function parseRetryAfter(value) {
|
|
101
|
+
if (!value)
|
|
102
|
+
return undefined;
|
|
103
|
+
const seconds = Number(value);
|
|
104
|
+
if (Number.isFinite(seconds))
|
|
105
|
+
return Math.max(0, seconds * 1000);
|
|
106
|
+
const time = Date.parse(value);
|
|
107
|
+
if (!Number.isNaN(time))
|
|
108
|
+
return Math.max(0, time - Date.now());
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|