bazis 0.138.0 → 0.139.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 +13 -5
- package/core/agent/AgentContextBuilder.ts +119 -16
- package/core/agent/AgentRuntime.ts +8 -0
- package/core/agent/AgentToolExecutor.ts +7 -0
- package/core/agent/ModelProvider.ts +6 -0
- package/core/agent/README.md +35 -3
- package/core/agent/errors.ts +32 -0
- package/core/agent/index.ts +7 -0
- package/core/agent/internal/AgentExecutionDriver.ts +598 -24
- package/core/agent/internal/AgentExecutionState.ts +26 -3
- package/core/agent/semantic.ts +60 -1
- package/core/cache/SPEC.md +3 -3
- package/core/di/README.md +10 -3
- package/core/infra/anthropic.ts +11 -0
- package/core/infra/connectors/anthropic.ts +440 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/llm.ts +12 -0
- package/core/infra/connectors/llmRouter.ts +12 -0
- package/core/infra/connectors/openaiCompatible.ts +128 -63
- package/core/infra/connectors/openaiTextStream.ts +5 -2
- package/core/infra/connectors/toolNames.ts +49 -0
- package/core/websocket/DESIGN.md +2 -2
- package/index.ts +5 -0
- package/library/orm/Repository/README.md +14 -11
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -71,8 +71,11 @@ npm page shows the source commit and the workflow that built it. The release
|
|
|
71
71
|
tarball is also attached to each
|
|
72
72
|
[GitHub Release](https://github.com/asus97178/bazis/releases).
|
|
73
73
|
|
|
74
|
-
`bazis new` creates a project with `src/
|
|
75
|
-
`
|
|
74
|
+
`bazis new` creates a project with `src/app/App.ts` (the application declared by
|
|
75
|
+
`@App`), `src/index.ts` (`runApp(Application)`, no options), a root `AppModule`,
|
|
76
|
+
`bazis.config.json`, scripts, a `/health` test that starts the declared application
|
|
77
|
+
in the test process and an `.env.example` with `BAZIS_HTTP__HOST` and
|
|
78
|
+
`BAZIS_HTTP__PORT`.
|
|
76
79
|
|
|
77
80
|
## CLI
|
|
78
81
|
|
|
@@ -141,20 +144,25 @@ import { dbConfig } from "../config/db.config"; // defineConfig("db", { default:
|
|
|
141
144
|
@Infra({ db: ormBazisConnect(dbConfig) })
|
|
142
145
|
export class AppInfra {}
|
|
143
146
|
|
|
144
|
-
// src/
|
|
147
|
+
// src/app/App.ts
|
|
145
148
|
@App({ modules: [AppModule], infra: AppInfra, http: { health: true } })
|
|
146
149
|
export class Application {}
|
|
150
|
+
|
|
151
|
+
// src/index.ts
|
|
152
|
+
await registerBazisGeneratedRuntime();
|
|
153
|
+
await runApp(Application);
|
|
147
154
|
```
|
|
148
155
|
|
|
149
156
|
A feature module attaches its own `DbContext` and entities with
|
|
150
|
-
`ormBazis: { context
|
|
157
|
+
`ormBazis: { context }` (the entities of its sets are mapped by themselves); its services take that context in their
|
|
151
158
|
constructors. `bunx bazis g module Task --minimal` generates such a module.
|
|
152
159
|
Configuration values can be overridden from the environment
|
|
153
160
|
(`BAZIS_DB__HOST`, `BAZIS_DB__PASSWORD`, ...). Invalid configuration or a
|
|
154
161
|
missing production secret stops startup with an error that names the key.
|
|
155
162
|
|
|
156
163
|
A unique index violation in `saveChanges()` rejects with
|
|
157
|
-
`UniqueViolationError` (with `constraint` and `
|
|
164
|
+
`UniqueViolationError` (with `constraint`, `table` and `properties`); uncaught, it
|
|
165
|
+
answers 409 by itself, as other database errors answer 404, 422 or 503.
|
|
158
166
|
Request-body fields declared as `string`, `number` or `boolean` are checked
|
|
159
167
|
against the JSON type automatically (400 on mismatch); other rules come from
|
|
160
168
|
`@Validator`.
|
|
@@ -2,6 +2,8 @@ import type { AgentDefinition } from "./AgentRegistry";
|
|
|
2
2
|
import { AgentRuntimeError } from "./errors";
|
|
3
3
|
import {
|
|
4
4
|
agentMessage,
|
|
5
|
+
agentToolResult,
|
|
6
|
+
agentToolResultPart,
|
|
5
7
|
type AgentContentPart,
|
|
6
8
|
type AgentMessage,
|
|
7
9
|
type AgentMessageRole,
|
|
@@ -9,10 +11,20 @@ import {
|
|
|
9
11
|
type JsonValue,
|
|
10
12
|
} from "./semantic";
|
|
11
13
|
|
|
14
|
+
/** A Tool result longer than this reaches the model cut, unless `maxToolResultChars` says otherwise. */
|
|
15
|
+
export const DEFAULT_MAX_TOOL_RESULT_CHARS = 100_000;
|
|
16
|
+
|
|
12
17
|
export interface AgentContextLimits {
|
|
13
18
|
readonly maxMessages?: number;
|
|
14
19
|
readonly maxChars?: number;
|
|
15
20
|
readonly maxTokens?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The longest Tool output, as JSON text, the model gets in full. A longer
|
|
23
|
+
* one is sent as `{ truncated: true, totalChars, text }` with its beginning,
|
|
24
|
+
* so one large result does not fill the model's window; `result.toolResults`
|
|
25
|
+
* and `result.messages` keep the whole output. 100 000 by default.
|
|
26
|
+
*/
|
|
27
|
+
readonly maxToolResultChars?: number;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
export interface AgentContextBuildInput {
|
|
@@ -21,6 +33,12 @@ export interface AgentContextBuildInput {
|
|
|
21
33
|
readonly messages: readonly AgentMessage[];
|
|
22
34
|
readonly metadata: JsonObject;
|
|
23
35
|
readonly limits?: AgentContextLimits;
|
|
36
|
+
/**
|
|
37
|
+
* How many real tokens one estimated token turned out to be for this model:
|
|
38
|
+
* the runtime measures it from the usage the provider reports. Estimates are
|
|
39
|
+
* multiplied by it before `maxTokens` is applied. 1 when nothing is known yet.
|
|
40
|
+
*/
|
|
41
|
+
readonly tokenScale?: number;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
export interface AgentContextTraceEntry {
|
|
@@ -55,6 +73,8 @@ interface ContextCandidate {
|
|
|
55
73
|
readonly message: AgentMessage;
|
|
56
74
|
readonly chars: number;
|
|
57
75
|
readonly tokens: number;
|
|
76
|
+
/** The Tool result was too long and is sent cut. */
|
|
77
|
+
readonly cut?: true;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
function errorMessageOf(error: unknown): string {
|
|
@@ -83,7 +103,11 @@ export function normalizeAgentContextLimits(
|
|
|
83
103
|
return undefined;
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
const normalized: { maxMessages?: number; maxChars?: number; maxTokens?: number } = {};
|
|
106
|
+
const normalized: { maxMessages?: number; maxChars?: number; maxTokens?: number; maxToolResultChars?: number } = {};
|
|
107
|
+
const maxToolResultChars = optionalPositiveInteger(limits.maxToolResultChars, `${field}.maxToolResultChars`);
|
|
108
|
+
if (maxToolResultChars !== undefined) {
|
|
109
|
+
normalized.maxToolResultChars = maxToolResultChars;
|
|
110
|
+
}
|
|
87
111
|
const maxMessages = optionalPositiveInteger(limits.maxMessages, `${field}.maxMessages`);
|
|
88
112
|
const maxChars = optionalPositiveInteger(limits.maxChars, `${field}.maxChars`);
|
|
89
113
|
const maxTokens = optionalPositiveInteger(limits.maxTokens, `${field}.maxTokens`);
|
|
@@ -129,10 +153,48 @@ function cloneMessage(message: AgentMessage, field: string): AgentMessage {
|
|
|
129
153
|
}
|
|
130
154
|
}
|
|
131
155
|
|
|
156
|
+
/** The message the model gets instead of a Tool result that is too long; `undefined` when it fits. */
|
|
157
|
+
function cutToolResult(message: AgentMessage, maxChars: number): AgentMessage | undefined {
|
|
158
|
+
const part = message.content[0];
|
|
159
|
+
if (message.role !== "tool" || part?.kind !== "tool-result" || part.result.status !== "success") return undefined;
|
|
160
|
+
const json = JSON.stringify(part.result.output) ?? "";
|
|
161
|
+
if (json.length <= maxChars) return undefined;
|
|
162
|
+
const result = agentToolResult({
|
|
163
|
+
callId: part.result.callId,
|
|
164
|
+
name: part.result.name,
|
|
165
|
+
status: "success",
|
|
166
|
+
output: { truncated: true, totalChars: json.length, text: json.slice(0, maxChars) },
|
|
167
|
+
durationMs: part.result.durationMs,
|
|
168
|
+
metadata: part.result.metadata,
|
|
169
|
+
});
|
|
170
|
+
return agentMessage("tool", agentToolResultPart(result), {
|
|
171
|
+
id: message.id, name: message.name, toolCallId: message.toolCallId, createdAtUnixMs: message.createdAtUnixMs, metadata: message.metadata,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function withoutReasoning(message: AgentMessage): AgentMessage {
|
|
176
|
+
if (!message.content.some((part) => part.kind === "reasoning")) return message;
|
|
177
|
+
const content = message.content.filter((part) => part.kind !== "reasoning");
|
|
178
|
+
if (content.length === 0) return message;
|
|
179
|
+
return agentMessage(message.role, content, { id: message.id, name: message.name, toolCallId: message.toolCallId, createdAtUnixMs: message.createdAtUnixMs, metadata: message.metadata });
|
|
180
|
+
}
|
|
181
|
+
|
|
132
182
|
function jsonLength(value: unknown): number {
|
|
133
183
|
return JSON.stringify(value)?.length ?? 0;
|
|
134
184
|
}
|
|
135
185
|
|
|
186
|
+
/** Tokens of an image whatever its size: providers scale images down to about this much. */
|
|
187
|
+
const IMAGE_TOKENS = 1_200;
|
|
188
|
+
|
|
189
|
+
function isDataUri(uri: string): boolean {
|
|
190
|
+
return uri.startsWith("data:");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A file in the request costs what its content does; an address alone says nothing about it. */
|
|
194
|
+
function fileTokens(uri: string): number {
|
|
195
|
+
return isDataUri(uri) ? Math.min(200_000, Math.max(500, Math.ceil(uri.length / 12))) : 2_000;
|
|
196
|
+
}
|
|
197
|
+
|
|
136
198
|
function contentPartChars(part: AgentContentPart): number {
|
|
137
199
|
switch (part.kind) {
|
|
138
200
|
case "text":
|
|
@@ -140,9 +202,12 @@ function contentPartChars(part: AgentContentPart): number {
|
|
|
140
202
|
case "data":
|
|
141
203
|
return jsonLength(part.value) + (part.name?.length ?? 0);
|
|
142
204
|
case "image":
|
|
143
|
-
|
|
205
|
+
// Bytes written into the dialog are not characters of a prompt.
|
|
206
|
+
return (isDataUri(part.uri) ? IMAGE_TOKENS * 4 : part.uri.length) + (part.mediaType?.length ?? 0) + part.detail.length;
|
|
144
207
|
case "file":
|
|
145
|
-
return part.uri.length + (part.mediaType?.length ?? 0) + (part.name?.length ?? 0);
|
|
208
|
+
return (isDataUri(part.uri) ? fileTokens(part.uri) * 4 : part.uri.length) + (part.mediaType?.length ?? 0) + (part.name?.length ?? 0);
|
|
209
|
+
case "reasoning":
|
|
210
|
+
return part.text.length + (part.data === undefined ? 0 : jsonLength(part.data));
|
|
146
211
|
case "tool-result":
|
|
147
212
|
return jsonLength(part.result);
|
|
148
213
|
case "tool-call":
|
|
@@ -150,9 +215,35 @@ function contentPartChars(part: AgentContentPart): number {
|
|
|
150
215
|
}
|
|
151
216
|
}
|
|
152
217
|
|
|
218
|
+
/**
|
|
219
|
+
* An estimate close to what tokenizers produce, without a tokenizer: Latin
|
|
220
|
+
* letters and digits are about four characters a token, letters of other
|
|
221
|
+
* alphabets about two, a CJK character is a token, punctuation is about two
|
|
222
|
+
* marks a token. The runtime corrects the rest by the usage a provider reports.
|
|
223
|
+
*/
|
|
153
224
|
function textTokenCount(text: string): number {
|
|
154
|
-
|
|
155
|
-
|
|
225
|
+
let latin = 0;
|
|
226
|
+
let letters = 0;
|
|
227
|
+
let marks = 0;
|
|
228
|
+
let whole = 0;
|
|
229
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
230
|
+
const code = text.charCodeAt(index);
|
|
231
|
+
if (code <= 0x20) continue;
|
|
232
|
+
if (code < 0x80) {
|
|
233
|
+
if ((code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a) || code === 0x5f) latin += 1;
|
|
234
|
+
else marks += 1;
|
|
235
|
+
} else if (code === 0xa0 || (code >= 0x2000 && code <= 0x200b) || code === 0x3000) {
|
|
236
|
+
continue;
|
|
237
|
+
} else if ((code >= 0x2e80 && code <= 0x9fff) || (code >= 0xac00 && code <= 0xd7af) || (code >= 0xf900 && code <= 0xfaff)) {
|
|
238
|
+
whole += 1;
|
|
239
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
240
|
+
// The second half of a surrogate pair: its first half was counted.
|
|
241
|
+
continue;
|
|
242
|
+
} else {
|
|
243
|
+
letters += 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return Math.ceil(latin / 4 + letters / 2 + marks / 2 + whole);
|
|
156
247
|
}
|
|
157
248
|
|
|
158
249
|
function contentPartTokens(part: AgentContentPart): number {
|
|
@@ -162,9 +253,12 @@ function contentPartTokens(part: AgentContentPart): number {
|
|
|
162
253
|
case "data":
|
|
163
254
|
return textTokenCount(JSON.stringify(part.value) ?? "") + (part.name === undefined ? 0 : textTokenCount(part.name));
|
|
164
255
|
case "image":
|
|
165
|
-
return
|
|
256
|
+
return IMAGE_TOKENS;
|
|
166
257
|
case "file":
|
|
167
|
-
return
|
|
258
|
+
return fileTokens(part.uri);
|
|
259
|
+
case "reasoning":
|
|
260
|
+
// The provider's record of a block is about as long as the reasoning it holds.
|
|
261
|
+
return Math.max(textTokenCount(part.text), part.data === undefined ? 0 : Math.ceil(jsonLength(part.data) / 4));
|
|
168
262
|
case "tool-result":
|
|
169
263
|
return textTokenCount(JSON.stringify(part.result) ?? "");
|
|
170
264
|
case "tool-call":
|
|
@@ -387,13 +481,15 @@ export class DefaultAgentContextBuilder implements AgentContextBuilder {
|
|
|
387
481
|
|
|
388
482
|
build(input: AgentContextBuildInput): AgentContext {
|
|
389
483
|
const limits = normalizeAgentContextLimits(input.limits);
|
|
390
|
-
const
|
|
484
|
+
const tokenScale = input.tokenScale !== undefined && Number.isFinite(input.tokenScale) && input.tokenScale > 0 ? input.tokenScale : 1;
|
|
485
|
+
const candidates = this.candidates(input.agent, input.messages, limits?.maxToolResultChars ?? DEFAULT_MAX_TOOL_RESULT_CHARS, tokenScale);
|
|
391
486
|
if (candidates.length === 0) {
|
|
392
487
|
throw new AgentRuntimeError("agent context requires at least one message.");
|
|
393
488
|
}
|
|
394
489
|
|
|
395
490
|
const included = candidates.map(() => true);
|
|
396
|
-
|
|
491
|
+
// A result sent cut stays in the context; the trace says why it is shorter.
|
|
492
|
+
const reasons = candidates.map((candidate) => candidate.cut ? "maxToolResultChars" : undefined as string | undefined);
|
|
397
493
|
const protectedItems = protectedIndexes(candidates);
|
|
398
494
|
const selection = new ContextSelection(candidates, included, reasons, protectedItems);
|
|
399
495
|
selection.apply("maxMessages", limits?.maxMessages);
|
|
@@ -402,12 +498,15 @@ export class DefaultAgentContextBuilder implements AgentContextBuilder {
|
|
|
402
498
|
|
|
403
499
|
const messages: AgentMessage[] = [];
|
|
404
500
|
const trace: AgentContextTraceEntry[] = [];
|
|
501
|
+
// A provider ties reasoning to the dialog it was written in: once older
|
|
502
|
+
// messages are dropped, the reasoning in the rest no longer matches it.
|
|
503
|
+
const trimmed = included.includes(false);
|
|
405
504
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
406
505
|
const candidate = candidates[index] as ContextCandidate;
|
|
407
506
|
const isIncluded = included[index] === true;
|
|
408
507
|
trace.push(traceEntry(candidate, isIncluded, reasons[index]));
|
|
409
508
|
if (isIncluded) {
|
|
410
|
-
messages.push(candidate.message);
|
|
509
|
+
messages.push(trimmed ? withoutReasoning(candidate.message) : candidate.message);
|
|
411
510
|
}
|
|
412
511
|
}
|
|
413
512
|
|
|
@@ -421,16 +520,17 @@ export class DefaultAgentContextBuilder implements AgentContextBuilder {
|
|
|
421
520
|
messageCount: messages.length,
|
|
422
521
|
charCount,
|
|
423
522
|
tokenCount,
|
|
523
|
+
...(tokenScale === 1 ? {} : { tokenScale }),
|
|
424
524
|
trace: traceToJson(trace),
|
|
425
525
|
}),
|
|
426
526
|
});
|
|
427
527
|
}
|
|
428
528
|
|
|
429
|
-
private candidates(agent: AgentDefinition, messages: readonly AgentMessage[]): readonly ContextCandidate[] {
|
|
529
|
+
private candidates(agent: AgentDefinition, messages: readonly AgentMessage[], maxToolResultChars: number, tokenScale: number): readonly ContextCandidate[] {
|
|
430
530
|
const candidates: ContextCandidate[] = [];
|
|
431
531
|
const prompt = this.promptMessage(agent);
|
|
432
532
|
if (prompt !== undefined) {
|
|
433
|
-
const tokens = this.estimatedTokens(prompt, "prompt");
|
|
533
|
+
const tokens = this.estimatedTokens(prompt, "prompt", tokenScale);
|
|
434
534
|
candidates.push(Object.freeze({
|
|
435
535
|
section: "prompt",
|
|
436
536
|
message: prompt,
|
|
@@ -440,19 +540,22 @@ export class DefaultAgentContextBuilder implements AgentContextBuilder {
|
|
|
440
540
|
}
|
|
441
541
|
|
|
442
542
|
for (let index = 0; index < messages.length; index += 1) {
|
|
443
|
-
const
|
|
444
|
-
const
|
|
543
|
+
const whole = cloneMessage(messages[index] as AgentMessage, `agentContext.messages[${index}]`);
|
|
544
|
+
const cut = cutToolResult(whole, maxToolResultChars);
|
|
545
|
+
const message = cut ?? whole;
|
|
546
|
+
const tokens = this.estimatedTokens(message, `conversation[${index}]`, tokenScale);
|
|
445
547
|
candidates.push(Object.freeze({
|
|
446
548
|
section: `conversation[${index}]`,
|
|
447
549
|
message,
|
|
448
550
|
chars: messageChars(message),
|
|
449
551
|
tokens,
|
|
552
|
+
...(cut === undefined ? {} : { cut: true as const }),
|
|
450
553
|
}));
|
|
451
554
|
}
|
|
452
555
|
return Object.freeze(candidates);
|
|
453
556
|
}
|
|
454
557
|
|
|
455
|
-
private estimatedTokens(message: AgentMessage, field: string): number {
|
|
558
|
+
private estimatedTokens(message: AgentMessage, field: string, tokenScale: number): number {
|
|
456
559
|
let estimate: unknown;
|
|
457
560
|
try { estimate = this.tokenEstimator.estimateMessage(message); } catch (error) {
|
|
458
561
|
throw new AgentRuntimeError(`agentContext token estimator failed for ${field}: ${errorMessageOf(error)}`);
|
|
@@ -460,7 +563,7 @@ export class DefaultAgentContextBuilder implements AgentContextBuilder {
|
|
|
460
563
|
if (!Number.isFinite(estimate) || typeof estimate !== "number" || estimate < 0) {
|
|
461
564
|
throw new AgentRuntimeError(`agentContext token estimator returned an invalid estimate for ${field}.`);
|
|
462
565
|
}
|
|
463
|
-
return estimate;
|
|
566
|
+
return tokenScale === 1 ? estimate : Math.ceil(estimate * tokenScale);
|
|
464
567
|
}
|
|
465
568
|
|
|
466
569
|
/** The definition's instructions are the system prompt, sent as written. */
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import type { AgentModelProvider } from "./ModelProvider";
|
|
15
15
|
|
|
16
16
|
export {
|
|
17
|
+
type AgentCompactionOptions,
|
|
18
|
+
type AgentModelRetryPolicy,
|
|
17
19
|
type AgentRuntimeInvokeOptions,
|
|
18
20
|
type AgentExecutionDriverOptions,
|
|
19
21
|
type AgentRuntimeOptions,
|
|
@@ -58,7 +60,13 @@ export class AgentRuntime {
|
|
|
58
60
|
this.options = {
|
|
59
61
|
...options,
|
|
60
62
|
toolExecutorOptions: { schemaValidator: modelValidatorAdapter, ...toolExecutorOptions },
|
|
63
|
+
// What a model's tokens turned out to cost is learned once for every run.
|
|
64
|
+
tokenScales: new Map<string, number>(),
|
|
61
65
|
};
|
|
66
|
+
const summaryProfile = options.compaction === undefined || options.compaction === false ? undefined : options.compaction.modelProfile;
|
|
67
|
+
if (summaryProfile !== undefined && typeof modelProvider.modelProfiles === "function" && !modelProvider.modelProfiles().includes(summaryProfile)) {
|
|
68
|
+
throw new AgentSetupError(`AgentRuntime.compaction: model profile "${summaryProfile}" is not configured (configured: ${[...modelProvider.modelProfiles()].sort().join(", ")}).`);
|
|
69
|
+
}
|
|
62
70
|
// Invalid options and hook setup fail here, at the first resolve, not on the first run.
|
|
63
71
|
new AgentExecutionDriver(services, this.catalog, modelProvider, this.options);
|
|
64
72
|
}
|
|
@@ -903,6 +903,13 @@ export class AgentToolExecutor {
|
|
|
903
903
|
return this.finish(call, result, startedAt, metadata, options, audit, tool, options.agentName, undefined, executionState);
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
+
// The provider could not read the arguments the model wrote: the Tool is
|
|
907
|
+
// not run, and the model gets the reason so it can write the call again.
|
|
908
|
+
if (call.inputError !== undefined) {
|
|
909
|
+
const result = this.errorResult(call, "TOOL_ARGUMENTS_INVALID", `Tool "${tool.metadata.name}" arguments are not valid JSON: ${call.inputError}`, metadata, startedAt, { phase: "pre-execute" });
|
|
910
|
+
return this.finish(call, result, startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
911
|
+
}
|
|
912
|
+
|
|
906
913
|
const schema = this.schemaSettings(options);
|
|
907
914
|
const schemaGate = this.ensureSchemaValidator(call, tool, schema, metadata, startedAt);
|
|
908
915
|
if (schemaGate) {
|
|
@@ -22,6 +22,12 @@ export interface AgentModelProvider {
|
|
|
22
22
|
* the model is called. Without it any profile is passed through.
|
|
23
23
|
*/
|
|
24
24
|
modelProfiles?(): readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* The context window of the model behind a profile, in tokens, when infra
|
|
27
|
+
* knows it (`contextTokens` of the LLM connector). The runtime starts
|
|
28
|
+
* compacting a long dialog before it reaches the window.
|
|
29
|
+
*/
|
|
30
|
+
contextTokens?(modelProfile?: string): number | undefined;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export const AGENT_MODEL_PROVIDER = createToken<AgentModelProvider>("AgentModelProvider");
|
package/core/agent/README.md
CHANGED
|
@@ -86,11 +86,11 @@ are shared by agents and `ToolRunner`:
|
|
|
86
86
|
```ts
|
|
87
87
|
@Module({
|
|
88
88
|
providers: [
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
singletonValue(AGENT_RUNTIME_OPTIONS, { maxSteps: 6 }),
|
|
90
|
+
singletonValue(TOOL_EXECUTION_OPTIONS, {
|
|
91
91
|
approvalPolicy: (request) => request.tool.metadata.sideEffect === "read",
|
|
92
92
|
auditSink: (entry) => auditStore.write(entry),
|
|
93
|
-
})
|
|
93
|
+
}),
|
|
94
94
|
],
|
|
95
95
|
})
|
|
96
96
|
export class AgentSettingsModule {}
|
|
@@ -144,6 +144,38 @@ await this.agents.run(support, {
|
|
|
144
144
|
conversation message when trimming old history. Provider requests include
|
|
145
145
|
`metadata.agentContext.trace` so context assembly is inspectable.
|
|
146
146
|
|
|
147
|
+
The runtime recovers before it fails a run. A model request the provider
|
|
148
|
+
cannot serve right now (`AgentModelUnavailableError`: HTTP 429, 5xx, a broken
|
|
149
|
+
connection) is repeated, three attempts by default (`modelRetry`). A text
|
|
150
|
+
answer cut by the output limit is continued (`maxOutputContinuations`, 3), a
|
|
151
|
+
structured answer that is not valid is sent back to the model with the issues
|
|
152
|
+
(`maxOutputRepairs`, 2); the finished dialog keeps one answer. Tool arguments
|
|
153
|
+
the provider could not read (`AgentToolCall.inputError`) give the model
|
|
154
|
+
`TOOL_ARGUMENTS_INVALID` instead of failing the run. Tools of one step that
|
|
155
|
+
change nothing (`none`, `read`) and stand next to each other run together
|
|
156
|
+
(`maxParallelToolCalls`, 8); `write` and `external` run one by one.
|
|
157
|
+
`runTimeoutMs` bounds a whole run (`AGENT_RUN_TIMEOUT`), `result.usage` sums
|
|
158
|
+
the tokens of every model call, and a Tool output longer than
|
|
159
|
+
`contextLimits.maxToolResultChars` (100 000) reaches the model cut.
|
|
160
|
+
|
|
161
|
+
A long dialog is compacted: its older part is replaced by a summary the
|
|
162
|
+
model writes (a user message with `metadata.runtime: "summary"`), the newest
|
|
163
|
+
messages stay as written and a Tool call is never separated from its results.
|
|
164
|
+
It starts at 80% of the model's window when the LLM connector states
|
|
165
|
+
`contextTokens`, at `compaction.maxTokens`, or after the provider refuses a
|
|
166
|
+
request as too long (`AgentModelContextOverflowError`); `result.compactions`
|
|
167
|
+
says it happened and `compaction: false` turns it off. Token estimates are
|
|
168
|
+
corrected by the usage a provider reports (`metadata.agentContext.tokenScale`).
|
|
169
|
+
|
|
170
|
+
A model's reasoning is a `reasoning` content part of its assistant message
|
|
171
|
+
(`agentReasoning(text, { data })`): `data` is the provider's own record of the
|
|
172
|
+
block and goes back to it unchanged with the dialog. The runtime drops
|
|
173
|
+
reasoning wherever it rewrites a dialog (compaction, trimming by
|
|
174
|
+
`contextLimits`, dropped drafts). `AgentUsage` also reports `cacheReadTokens`,
|
|
175
|
+
`cacheWriteTokens` and `reasoningTokens`; `inputTokens` is the whole request,
|
|
176
|
+
cached tokens included. Claude is served by `anthropicAdapter` from
|
|
177
|
+
`bazis/core/infra/anthropic`, on the official SDK.
|
|
178
|
+
|
|
147
179
|
With `llmRouter`, `check` and `run` reject a `modelProfile` the router does
|
|
148
180
|
not serve; a fallback to another profile is logged as `LLM model profile
|
|
149
181
|
failed, using fallback`. A model provider that gives up after its own deadline
|
package/core/agent/errors.ts
CHANGED
|
@@ -69,6 +69,38 @@ export class AgentModelTimeoutError extends Error {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A model provider could not serve the request right now: it is overloaded,
|
|
74
|
+
* limits the rate (HTTP 429), answered 5xx or the connection broke. The runtime
|
|
75
|
+
* repeats such a call (`AgentRuntimeOptions.modelRetry`); any other provider
|
|
76
|
+
* error fails the run at once.
|
|
77
|
+
*/
|
|
78
|
+
export class AgentModelUnavailableError extends Error {
|
|
79
|
+
/** How long the provider asked to wait (`Retry-After`), when it said so. */
|
|
80
|
+
readonly retryAfterMs?: number;
|
|
81
|
+
|
|
82
|
+
constructor(message: string, options: { readonly retryAfterMs?: number } = {}) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "AgentModelUnavailableError";
|
|
85
|
+
if (options.retryAfterMs !== undefined && Number.isFinite(options.retryAfterMs) && options.retryAfterMs >= 0) {
|
|
86
|
+
this.retryAfterMs = Math.floor(options.retryAfterMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The request does not fit the model's context window. The runtime replaces
|
|
93
|
+
* the older part of the dialog with a summary and sends the request again
|
|
94
|
+
* (`AgentRuntimeOptions.compaction`); a run that still does not fit fails
|
|
95
|
+
* with AGENT_CONTEXT_OVERFLOW.
|
|
96
|
+
*/
|
|
97
|
+
export class AgentModelContextOverflowError extends Error {
|
|
98
|
+
constructor(message: string) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "AgentModelContextOverflowError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
/** Error thrown when the agent runtime is configured incorrectly. */
|
|
73
105
|
export class AgentRuntimeError extends Error {
|
|
74
106
|
constructor(message: string) {
|
package/core/agent/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export {
|
|
|
25
25
|
AgentSetupError,
|
|
26
26
|
AgentToolExecutionError,
|
|
27
27
|
AgentModelTimeoutError,
|
|
28
|
+
AgentModelUnavailableError,
|
|
29
|
+
AgentModelContextOverflowError,
|
|
28
30
|
AgentToolPreCommitError,
|
|
29
31
|
ToolError,
|
|
30
32
|
} from "./errors";
|
|
@@ -33,6 +35,8 @@ export {
|
|
|
33
35
|
AgentRuntime,
|
|
34
36
|
type AgentRuntimeInvokeOptions,
|
|
35
37
|
type AgentRuntimeOptions,
|
|
38
|
+
type AgentModelRetryPolicy,
|
|
39
|
+
type AgentCompactionOptions,
|
|
36
40
|
type AgentRuntimeResult,
|
|
37
41
|
type AgentRuntimeStatus,
|
|
38
42
|
type AgentRuntimeToolCallContext,
|
|
@@ -102,6 +106,7 @@ export {
|
|
|
102
106
|
agentInvocation,
|
|
103
107
|
agentJsonSchema,
|
|
104
108
|
agentMessage,
|
|
109
|
+
agentMessageText,
|
|
105
110
|
agentModelCapabilities,
|
|
106
111
|
agentModelRequest,
|
|
107
112
|
agentModelResponse,
|
|
@@ -112,12 +117,14 @@ export {
|
|
|
112
117
|
agentToolContract,
|
|
113
118
|
agentToolResult,
|
|
114
119
|
agentToolResultPart,
|
|
120
|
+
agentReasoning,
|
|
115
121
|
normalizeJsonValue,
|
|
116
122
|
type AgentClassSchemaContract,
|
|
117
123
|
type AgentContentInput,
|
|
118
124
|
type AgentContentPart,
|
|
119
125
|
type AgentDataPart,
|
|
120
126
|
type AgentFilePart,
|
|
127
|
+
type AgentReasoningPart,
|
|
121
128
|
type AgentFinishReason,
|
|
122
129
|
type AgentImagePart,
|
|
123
130
|
type AgentInvocation,
|