bazis 0.137.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 +19 -8
- package/cli/generateProject.ts +11 -18
- package/cli/main.ts +1 -1
- package/cli/templates/passport.ts +1 -1
- package/core/access/catalog/moduleFields.ts +25 -2
- package/core/access/testing/index.ts +26 -4
- 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/ToolRunner.ts +2 -1
- package/core/agent/defineTool.ts +109 -0
- package/core/agent/errors.ts +32 -0
- package/core/agent/index.ts +15 -3
- package/core/agent/internal/AgentExecutionDriver.ts +598 -24
- package/core/agent/internal/AgentExecutionState.ts +26 -3
- package/core/agent/moduleContributions-v1.ts +3 -10
- package/core/agent/semantic.ts +60 -1
- package/core/app/index.ts +1 -1
- package/core/app/messages.ts +3 -3
- package/core/app/runApp.ts +90 -26
- package/core/cache/ICache.ts +1 -1
- package/core/cache/README.md +4 -2
- package/core/cache/SPEC.md +19 -12
- package/core/cache/http/missingCacheModuleCheck.ts +5 -4
- package/core/cache/internal/buildCacheProviders.ts +4 -5
- package/core/cache/providers/cachedProviders.ts +1 -1
- package/core/di/README.md +10 -3
- package/core/di/index.ts +0 -4
- package/core/di/lowLevel.ts +12 -0
- package/core/di/module/index.ts +0 -3
- package/core/di/module/shortcuts.ts +31 -86
- package/core/generatedRuntime.ts +11 -0
- package/core/grpc/Grpc.module.ts +2 -1
- package/core/grpc/grpcClients.ts +2 -1
- package/core/http/Authorization/metadata.ts +1 -1
- package/core/http/Errors/HttpError.ts +1 -1
- package/core/http/Http.config.ts +22 -0
- package/core/http/HttpContext/HttpContext.ts +1 -1
- package/core/http/HttpServer.ts +10 -7
- package/core/http/OpenApi/openApiDocument.ts +1 -1
- package/core/http/httpModule.ts +10 -8
- package/core/http/index.ts +1 -0
- package/core/http/options.ts +5 -1
- package/core/http-client/httpClients.ts +2 -1
- package/core/infra/Infra.ts +3 -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/codex/contracts.ts +3 -3
- package/core/infra/connectors/llm.ts +14 -1
- 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/redis.ts +2 -1
- package/core/infra/connectors/toolNames.ts +49 -0
- package/core/kernel/KernelBuilder.ts +2 -1
- package/core/kernel/config/ConfigRegistry.ts +2 -1
- package/core/kernel/config/addConfigOptions.ts +2 -1
- package/core/kernel/config/defineConfig.ts +1 -1
- package/core/kernel/events/EventBus.ts +1 -0
- package/core/kernel/events/eventSubscribers.ts +1 -1
- package/core/kernel/events/onEvent.ts +2 -1
- package/core/orm/buildOrmProviders.ts +2 -1
- package/core/orm/encryptedWith.ts +1 -1
- package/core/orm/ormModule.ts +2 -1
- package/core/orm/registerRepositories.ts +2 -1
- package/core/scripts/di-generate.ts +52 -8
- package/core/testing/index.ts +51 -11
- package/core/tools/index.ts +1 -1
- package/core/tools/orm/ormEntityTools.ts +54 -28
- package/core/websocket/DESIGN.md +7 -5
- package/core/websocket/middleware.ts +1 -1
- package/core/websocket/websocketModule.ts +3 -3
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +1 -2
- package/core/workflow/definitions_module/code/defineProcess.ts +213 -11
- package/core/workflow/definitions_module/code/inspectProcesses.ts +12 -2
- package/core/workflow/index.ts +14 -1
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +10 -6
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +5 -2
- package/core/workflow/testing/index.ts +46 -4
- package/index.ts +9 -3
- package/library/orm/Repository/README.md +14 -11
- package/library/terminal/messages.ts +6 -6
- package/library/validation/README.md +1 -1
- package/library/validation/SPEC.md +1 -1
- package/library/workflow/index.ts +1 -0
- package/library/workflow/nodeConfigs.ts +130 -0
- package/package.json +8 -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,19 +144,25 @@ import { dbConfig } from "../config/db.config"; // defineConfig("db", { default:
|
|
|
141
144
|
@Infra({ db: ormBazisConnect(dbConfig) })
|
|
142
145
|
export class AppInfra {}
|
|
143
146
|
|
|
147
|
+
// src/app/App.ts
|
|
148
|
+
@App({ modules: [AppModule], infra: AppInfra, http: { health: true } })
|
|
149
|
+
export class Application {}
|
|
150
|
+
|
|
144
151
|
// src/index.ts
|
|
145
|
-
await
|
|
152
|
+
await registerBazisGeneratedRuntime();
|
|
153
|
+
await runApp(Application);
|
|
146
154
|
```
|
|
147
155
|
|
|
148
156
|
A feature module attaches its own `DbContext` and entities with
|
|
149
|
-
`ormBazis: { context
|
|
157
|
+
`ormBazis: { context }` (the entities of its sets are mapped by themselves); its services take that context in their
|
|
150
158
|
constructors. `bunx bazis g module Task --minimal` generates such a module.
|
|
151
159
|
Configuration values can be overridden from the environment
|
|
152
160
|
(`BAZIS_DB__HOST`, `BAZIS_DB__PASSWORD`, ...). Invalid configuration or a
|
|
153
161
|
missing production secret stops startup with an error that names the key.
|
|
154
162
|
|
|
155
163
|
A unique index violation in `saveChanges()` rejects with
|
|
156
|
-
`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.
|
|
157
166
|
Request-body fields declared as `string`, `number` or `boolean` are checked
|
|
158
167
|
against the JSON type automatically (400 on mismatch); other rules come from
|
|
159
168
|
`@Validator`.
|
|
@@ -180,11 +189,13 @@ import { CodexError, RU_CODEX_MESSAGES } from "bazis/core/infra";
|
|
|
180
189
|
import { RU_UI_LABELS } from "bazis/core/app";
|
|
181
190
|
|
|
182
191
|
CodexError.useMessages(RU_CODEX_MESSAGES); // Codex connector errors
|
|
183
|
-
|
|
192
|
+
@App({
|
|
193
|
+
modules: [AppModule],
|
|
184
194
|
language: "ru", // response texts: validation, 404/405, 401/403, binding errors
|
|
185
195
|
messages: { notFound: "Нет такой страницы" }, // optional: own texts for single keys
|
|
186
|
-
ui: { labels: RU_UI_LABELS, surfaces: [/* ... */] }, // generated UI texts
|
|
187
|
-
})
|
|
196
|
+
ui: { labels: RU_UI_LABELS, surfaces: [/* ... */] }, // generated UI texts,
|
|
197
|
+
})
|
|
198
|
+
export class Application {}
|
|
188
199
|
```
|
|
189
200
|
|
|
190
201
|
`language` is per application: it is applied when the response is written, so
|
package/cli/generateProject.ts
CHANGED
|
@@ -169,39 +169,32 @@ function buildProjectFiles(name: string, dependency: string, frameworkMode: Fram
|
|
|
169
169
|
["bazis.config.json", `${JSON.stringify(codegen, null, 2)}\n`],
|
|
170
170
|
// Bun reads .env, .env.local and .env.<NODE_ENV>: none of them belongs in git.
|
|
171
171
|
[".gitignore", "node_modules/\nbin/\nsrc/generated/\n.env\n.env.*\n!.env.example\n"],
|
|
172
|
-
[".env.example", "# Copy to .env (Bun loads it automatically).\nBAZIS_ENV=development\
|
|
172
|
+
[".env.example", "# Copy to .env (Bun loads it automatically).\nBAZIS_ENV=development\nBAZIS_HTTP__HOST=127.0.0.1\nBAZIS_HTTP__PORT=3000\n"],
|
|
173
173
|
["src/app/test/health.test.ts", HEALTH_TEST],
|
|
174
174
|
["AGENTS.md", "# Working on this bazis project\n\nBefore changing the application, read the [module architecture](docs/architecture/MODULE_ARCHITECTURE.md). Create new modules only with `bunx bazis g module` or `bunx bazis g pack`; fill in the generated `MODULE.md` afterwards. Only codegen updates `src/generated/`.\n"],
|
|
175
|
-
["docs/architecture/MODULE_ARCHITECTURE.md", "# Application module architecture\n\n`src/index.ts` calls `runApp`; `src/app/modules/App.module.ts` composes feature modules through `imports`. The application root owns no domain logic.\n\nOne self-contained function is an atomic module. It owns its data, services, HTTP and background handlers. A composite module is only for several independent functions; its root does composition. Layers and file counts alone do not create submodules.\n\nCreate new modules only with `bunx bazis g module <Name> --empty|--minimal|--full` or `bunx bazis g pack <Name> --parts <a,b>`; add a part to an existing composite with `bunx bazis g module <Name> --pack <Pack>`. The generated `MODULE.md` records the creation command. Before implementing, define the responsibility and public entries, then fill in the generated `MODULE.md`: fields, errors, dependencies, exports and checks. Use the public APIs of the `bazis` package and its DI and ORM. Do not edit `src/generated/` by hand; run `bunx bazis codegen`.\n\n`--minimal` creates a sample CRUD with the ORM. To run it the application needs a database provider and a ready schema. For a first function without a database use `--empty`. Check types and the binary build after changes that affect startup.\n"],
|
|
175
|
+
["docs/architecture/MODULE_ARCHITECTURE.md", "# Application module architecture\n\n`src/index.ts` calls `runApp(Application)`; `src/app/App.ts` declares the application with `@App`: its modules, infrastructure and servers; `src/app/modules/App.module.ts` composes feature modules through `imports`. The application root owns no domain logic.\n\nOne self-contained function is an atomic module. It owns its data, services, HTTP and background handlers. A composite module is only for several independent functions; its root does composition. Layers and file counts alone do not create submodules.\n\nCreate new modules only with `bunx bazis g module <Name> --empty|--minimal|--full` or `bunx bazis g pack <Name> --parts <a,b>`; add a part to an existing composite with `bunx bazis g module <Name> --pack <Pack>`. The generated `MODULE.md` records the creation command. Before implementing, define the responsibility and public entries, then fill in the generated `MODULE.md`: fields, errors, dependencies, exports and checks. Use the public APIs of the `bazis` package and its DI and ORM. Do not edit `src/generated/` by hand; run `bunx bazis codegen`.\n\n`--minimal` creates a sample CRUD with the ORM. To run it the application needs a database provider and a ready schema. For a first function without a database use `--empty`. Check types and the binary build after changes that affect startup.\n"],
|
|
176
176
|
["src/app/modules/App.module.ts", 'import { Module } from "bazis/core/di";\n\n@Module({ imports: [], exports: [] })\nexport class AppModule {}\n'],
|
|
177
|
-
["src/
|
|
177
|
+
["src/app/App.ts", 'import { App } from "bazis/core/app";\nimport { AppModule } from "./modules/App.module";\n\n/** The application: its modules and the servers it starts. `src/index.ts` runs it, tests start the same one. */\n@App({\n modules: [AppModule],\n http: { health: true },\n})\nexport class Application {}\n'],
|
|
178
|
+
["src/index.ts", 'import { runApp } from "bazis/core/app";\nimport { Application } from "./app/App";\nimport { registerBazisGeneratedRuntime } from "./generated/bazis/runtime";\n\nawait registerBazisGeneratedRuntime();\nawait runApp(Application);\n'],
|
|
178
179
|
["README.md", `# ${name}\n\nA [bazis](https://www.npmjs.com/package/bazis) application. ${frameworkMode === "registry"
|
|
179
180
|
? "The framework comes from npm as the `bazis` dependency; update it with `bun update bazis`."
|
|
180
181
|
: frameworkMode === "snapshot"
|
|
181
182
|
? "The framework package is copied to `vendor/bazis`; keep it in Git and move it with the project. The framework checkout is no longer needed. Framework updates do not reach this copy automatically."
|
|
182
|
-
: "The `bazis` dependency links an external local checkout (`--link-framework`). Moving the project needs the same package and an updated path in `package.json`."}\nRead the [local architecture](docs/architecture/MODULE_ARCHITECTURE.md) before changing modules.\n\nNeeds Bun ≥ 1.4.0. Settings go to \`.env\` (example: \`.env.example\`).\n\n\`\`\`sh\nbun install\nbunx bazis dev\n# GET http://127.0.0.1:3000/health\n# If the port is busy:
|
|
183
|
+
: "The `bazis` dependency links an external local checkout (`--link-framework`). Moving the project needs the same package and an updated path in `package.json`."}\nRead the [local architecture](docs/architecture/MODULE_ARCHITECTURE.md) before changing modules.\n\nNeeds Bun ≥ 1.4.0. Settings go to \`.env\` (example: \`.env.example\`).\n\n\`\`\`sh\nbun install\nbunx bazis dev\n# GET http://127.0.0.1:3000/health\n# If the port is busy: BAZIS_HTTP__PORT=3100 bunx bazis dev\n\`\`\`\n\nAdd an atomic module from the project root: \`bunx bazis g module Task --empty\`.\nAfter filling in its passport and implementation, run \`bunx bazis codegen\`.\nTests: \`bunx bazis test\`. Typecheck: \`bunx bazis build\`. Binary: \`bunx bazis build --bin\`, run it with \`bun run start\`.\n\n\`--minimal\` creates a sample CRUD with paging (20 records by default, at most 100 over HTTP); it needs a database provider and a schema to run. The service returns \`PageResult\`; the HTTP controller builds JSON:API. \`DbContext.saveChanges()\` saves every change of its context. In ORM predicates combine conditions with \`.and()\` and \`.or()\`; codegen rejects \`&&\` and \`||\` between predicates.\n`],
|
|
183
184
|
];
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
const HEALTH_TEST = `import { expect, test } from "bun:test";
|
|
188
|
+
import { startTestApp } from "bazis/core/testing";
|
|
189
|
+
import { Application } from "../App";
|
|
187
190
|
|
|
188
|
-
// Starts the
|
|
191
|
+
// Starts the application declared in src/app/App.ts on a free port and checks /health.
|
|
189
192
|
test("app starts and answers /health", async () => {
|
|
190
|
-
|
|
191
|
-
const probe = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() });
|
|
192
|
-
const port = String(probe.port);
|
|
193
|
-
probe.stop(true);
|
|
194
|
-
const app = Bun.spawn([process.execPath, "run", "src/index.ts"], { env: { ...process.env, HOST: "127.0.0.1", PORT: port }, stdout: "ignore", stderr: "inherit" });
|
|
193
|
+
const app = await startTestApp(Application);
|
|
195
194
|
try {
|
|
196
|
-
|
|
197
|
-
for (let attempt = 0; attempt < 100 && status !== 200; attempt++) {
|
|
198
|
-
await Bun.sleep(100);
|
|
199
|
-
status = await fetch(\`http://127.0.0.1:\${port}/health\`).then((response) => response.status, () => 0);
|
|
200
|
-
}
|
|
201
|
-
expect(status).toBe(200);
|
|
195
|
+
expect((await app.fetch("/health")).status).toBe(200);
|
|
202
196
|
} finally {
|
|
203
|
-
app.
|
|
204
|
-
await app.exited;
|
|
197
|
+
await app.stop();
|
|
205
198
|
}
|
|
206
199
|
});
|
|
207
200
|
`;
|
package/cli/main.ts
CHANGED
|
@@ -48,7 +48,7 @@ Generation options:
|
|
|
48
48
|
--minimal Compact example CRUD and MODULE.md (default)
|
|
49
49
|
--full Example CRUD/list/cache/auth/background/AI and MODULE.md;
|
|
50
50
|
uses src/app/modules/auth helpers when the project has them.
|
|
51
|
-
To run, the host needs a cache (
|
|
51
|
+
To run, the host needs a cache (@App({ cache: memory() }))
|
|
52
52
|
and a database provider. Legacy alias: --enterprise
|
|
53
53
|
--parts <a,b,...> Independent empty atomic parts; pack only, at least two
|
|
54
54
|
--dry-run Validate and list planned changes without writing
|
|
@@ -76,7 +76,7 @@ exports: [I${m}Service]. TypeScript contract: services/I${e}.service.ts;
|
|
|
76
76
|
HTTP access is defined by the controller and the host policy, separately from DI exports.
|
|
77
77
|
I${m}Service → ${m}Service, scoped lifetime. The context and DbSet belong to the ORM.
|
|
78
78
|
${profile === "full" ? `${m}SummaryTool is listed in tools: [${m}SummaryTool]; the framework registers it as scoped, the tool is not exported.
|
|
79
|
-
The service is scoped; its keyed @Cacheable works when the host installs the cache (
|
|
79
|
+
The service is scoped; its keyed @Cacheable works when the host installs the cache (@App cache: memory()).
|
|
80
80
|
The background service gets its own scope per tick and disposes it in finally.
|
|
81
81
|
The host also provides auth from src/app/modules/auth (TokenKind.Admin/Client)
|
|
82
82
|
and an LLM connector, which registers AgentRuntime. The CLI does not create them.
|
|
@@ -26,6 +26,8 @@ const REFERENCE_CHANNEL = createModuleOwnedMetadataChannel<ReferencePayload>("ac
|
|
|
26
26
|
const ROLE_CHANNEL = createModuleOwnedMetadataChannel<RolePayload>("access.role/v1");
|
|
27
27
|
/** The classes of `references`: the field registers them, like `tools` and `controllers`. */
|
|
28
28
|
const REFERENCE_CLASS_CHANNEL = createModuleOwnedProviderChannel<{ readonly kind: string }, object>("access.reference-class/v1");
|
|
29
|
+
/** The `lookup` classes of `permissions`: the field registers them too. */
|
|
30
|
+
const LOOKUP_CLASS_CHANNEL = createModuleOwnedProviderChannel<{ readonly permission: string }, object>("access.lookup-class/v1");
|
|
29
31
|
|
|
30
32
|
/** The values of `in`/`notIn` a role may list; the run-time limit is `access.maxInList`. */
|
|
31
33
|
const STARTUP_MAX_IN_LIST = 1000;
|
|
@@ -46,7 +48,20 @@ registerModuleOwnedProviderContributor((metadata: BazisModuleMetadata, context)
|
|
|
46
48
|
if (!isDeclaredPermissions(fields.permissions)) throw new AccessSetupError(`${module}: permissions must be a definePermissions(...) result.`);
|
|
47
49
|
const problems = Object.entries(fields.permissions).flatMap(([key, spec]) => permissionProblems(key, spec));
|
|
48
50
|
if (problems.length > 0) throw setupError(module, "permissions", problems);
|
|
49
|
-
|
|
51
|
+
const lookups = new Set<unknown>();
|
|
52
|
+
for (const [key, spec] of Object.entries(fields.permissions)) {
|
|
53
|
+
context.addMetadata(PERMISSION_CHANNEL, { key, spec });
|
|
54
|
+
for (const lookup of lookupsOf(spec)) {
|
|
55
|
+
// A lookup another module owns and exports is taken from it.
|
|
56
|
+
if (lookups.has(lookup) || importsExport(metadata, lookup)) continue;
|
|
57
|
+
lookups.add(lookup);
|
|
58
|
+
try {
|
|
59
|
+
context.registerScoped(LOOKUP_CLASS_CHANNEL, scoped(lookup as Class<object>), { permission: key });
|
|
60
|
+
} catch {
|
|
61
|
+
// The module registered the class its own way: that registration stands.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
}
|
|
51
66
|
if (fields.references !== undefined) {
|
|
52
67
|
const references = fields.references;
|
|
@@ -117,7 +132,7 @@ registerModuleOwnedContributionValidator((snapshot) => {
|
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
for (const lookup of lookupsOf(payload.spec)) {
|
|
120
|
-
if (snapshot.countProviders(lookup) === 0) throw new AccessSetupError(`${ownerName}: ${lookup.name} of permission "${payload.key}" is not registered
|
|
135
|
+
if (snapshot.countProviders(lookup) === 0) throw new AccessSetupError(`${ownerName}: ${lookup.name} of permission "${payload.key}" is not registered.`);
|
|
121
136
|
}
|
|
122
137
|
}
|
|
123
138
|
|
|
@@ -136,6 +151,14 @@ registerModuleOwnedContributionValidator((snapshot) => {
|
|
|
136
151
|
}
|
|
137
152
|
});
|
|
138
153
|
|
|
154
|
+
/** Whether a module the given one imports exports this class. */
|
|
155
|
+
function importsExport(metadata: BazisModuleMetadata, type: unknown): boolean {
|
|
156
|
+
return (metadata.imports ?? []).some((imported) => {
|
|
157
|
+
const exported = (imported as { readonly exports?: unknown }).exports;
|
|
158
|
+
return Array.isArray(exported) && exported.includes(type);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
/** What the modules of the application's container declared. */
|
|
140
163
|
export function declarationsOf(services: ServiceProvider): { permissions: OwnedPermission[]; kinds: OwnedReferenceKind[]; roles: DeclaredRole[] } {
|
|
141
164
|
if (!(services instanceof DiContainer)) throw new AccessSetupError("Access declarations require the framework DiContainer.");
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* Test helpers of access (ACCESS-001 §8): a rights system whose grants are set
|
|
3
3
|
* right in the test.
|
|
4
4
|
*/
|
|
5
|
+
import { singletonValue, type ProviderDefinition } from "../../di";
|
|
5
6
|
import { PRINCIPAL_STATE_KEY, type AuthorizeContext, type RequestPrincipal } from "../../http";
|
|
6
7
|
import { AccessPolicy, type ScopePrincipals } from "../contracts/AccessPolicy";
|
|
7
8
|
import type { AccessSubject, Grants, Scope } from "../contracts/types";
|
|
8
9
|
|
|
9
10
|
export interface StaticAccessOptions {
|
|
10
|
-
/** Subject → permission → grants; a permission not listed gives
|
|
11
|
-
readonly grants
|
|
11
|
+
/** Subject → permission → grants; a permission not listed gives {@link otherwise}. */
|
|
12
|
+
readonly grants?: Readonly<Record<string, Readonly<Record<string, Grants>>>>;
|
|
13
|
+
/** What a permission not listed in `grants` gives: nothing by default, `"all"` to allow everything. */
|
|
14
|
+
readonly otherwise?: Grants;
|
|
15
|
+
/** Who a request without a signed-in principal is; by default nobody, and the request is refused. */
|
|
16
|
+
readonly anonymous?: string;
|
|
12
17
|
/** Subject → the owner ids meaning it; by default `sub` after the last `:`. */
|
|
13
18
|
readonly owners?: Readonly<Record<string, readonly string[]>>;
|
|
14
19
|
/** Subject → its addressees in every partition; by default `user:<owner id>`. */
|
|
@@ -32,11 +37,12 @@ export class StaticAccessPolicy extends AccessPolicy {
|
|
|
32
37
|
|
|
33
38
|
subject(ctx: AuthorizeContext): AccessSubject | undefined {
|
|
34
39
|
const sub = (ctx.state.get(PRINCIPAL_STATE_KEY) as RequestPrincipal | undefined)?.subject;
|
|
35
|
-
|
|
40
|
+
if (sub !== undefined && sub !== "") return { sub };
|
|
41
|
+
return this.options.anonymous === undefined ? undefined : { sub: this.options.anonymous };
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
grants(subject: AccessSubject, permission: string): Grants {
|
|
39
|
-
return this.options.grants[subject.sub]?.[permission] ?? [];
|
|
45
|
+
return this.options.grants?.[subject.sub]?.[permission] ?? this.options.otherwise ?? [];
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
ownerIds(subject: AccessSubject): readonly string[] {
|
|
@@ -49,6 +55,22 @@ export class StaticAccessPolicy extends AccessPolicy {
|
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A rights system that allows everything, for a test that is not about rights:
|
|
60
|
+
* every subject has every permission in every partition.
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* const app = await startTestApp(OrdersApp, { overrides: [allowAll()] });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* Requests still need a signed-in principal; `allowAll({ anonymous: "test" })`
|
|
67
|
+
* also lets in a request without one, as the subject `test`. `grants` narrows
|
|
68
|
+
* single subjects: `allowAll({ grants: { "user:2": { "orders.delete": [] } } })`.
|
|
69
|
+
*/
|
|
70
|
+
export function allowAll(options: Omit<StaticAccessOptions, "otherwise"> = {}): ProviderDefinition {
|
|
71
|
+
return singletonValue(AccessPolicy, new StaticAccessPolicy({ ...options, otherwise: "all" }));
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
function ownerId(sub: string): string {
|
|
53
75
|
return sub.slice(sub.lastIndexOf(":") + 1);
|
|
54
76
|
}
|
|
@@ -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/ToolRunner.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DiError, Global, HOSTED_SERVICE, Module, registerModuleMetadataExpander, SERVICE_PROVIDER, type BazisModuleMetadata, type ServiceProvider } from "../di";
|
|
2
|
+
import { DI } from "../di/lowLevel";
|
|
2
3
|
import { LOGGER } from "../kernel/logging/Logger";
|
|
3
4
|
import { modelValidatorAdapter } from "../../library/validation";
|
|
4
5
|
import { AgentRegistry, type ToolDefinition } from "./AgentRegistry";
|