bazis 0.121.0 → 0.123.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/cli/templates/module.ts +5 -4
- package/cli/templates/passport.ts +1 -1
- package/core/agent/AgentRuntime.ts +7 -2
- package/core/agent/AgentToolExecutor.ts +34 -11
- package/core/agent/README.md +10 -9
- package/core/agent/ToolRunner.ts +135 -0
- package/core/agent/index.ts +2 -0
- package/core/agent/internal/AgentExecutionDriver.ts +9 -2
- package/core/agent/internal/ToolAudit.projector.ts +16 -0
- package/core/agent/metadata.ts +25 -0
- package/core/agent/moduleContributions-v1.ts +5 -1
- package/core/agent/toolExecutionOptions.ts +15 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/package.json +1 -1
package/cli/templates/module.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function buildModuleTemplates(
|
|
|
49
49
|
return [
|
|
50
50
|
...base,
|
|
51
51
|
{ relativePath: `background/${naming.module}.reporter.ts`, content: statsReporterFile(naming) },
|
|
52
|
-
{ relativePath: `
|
|
52
|
+
{ relativePath: `tools/${naming.module}Summary.tool.ts`, content: toolFile(naming) },
|
|
53
53
|
];
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -57,7 +57,7 @@ function moduleFile(n: ModuleNaming, profile: ModuleTemplateProfile): string {
|
|
|
57
57
|
const moduleImport = "import { Module, scoped } from \"bazis/core/di\";";
|
|
58
58
|
const backgroundImport = profile === "full" ? `import { ${n.module}Reporter } from "./background/${n.module}.reporter";\n` : "";
|
|
59
59
|
const aiImports = profile === "full"
|
|
60
|
-
? `import { ${n.module}SummaryTool } from "./
|
|
60
|
+
? `import { ${n.module}SummaryTool } from "./tools/${n.module}Summary.tool";
|
|
61
61
|
`
|
|
62
62
|
: "";
|
|
63
63
|
const backgroundLine = profile === "full" ? ` background: [${n.module}Reporter],\n` : "";
|
|
@@ -397,10 +397,10 @@ export class ${n.module}Reporter extends PeriodicBackgroundService {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
|
|
400
|
-
function
|
|
400
|
+
function toolFile(n: ModuleNaming): string {
|
|
401
401
|
return `import { Tool, type AgentToolExecutionContext } from "bazis/core/agent";
|
|
402
402
|
import { Validator } from "bazis/library/validation";
|
|
403
|
-
import type { I${n.module}Service } from "
|
|
403
|
+
import type { I${n.module}Service } from "../services/I${n.module}.service";
|
|
404
404
|
|
|
405
405
|
export class ${n.module}SummaryToolInput {
|
|
406
406
|
@Validator({ required: true, minLength: 3 })
|
|
@@ -420,6 +420,7 @@ export class ${n.module}SummaryToolOutput {
|
|
|
420
420
|
input: ${n.module}SummaryToolInput,
|
|
421
421
|
output: ${n.module}SummaryToolOutput,
|
|
422
422
|
sideEffect: "read",
|
|
423
|
+
callers: ["agent"],
|
|
423
424
|
})
|
|
424
425
|
export class ${n.module}SummaryTool {
|
|
425
426
|
constructor(private readonly ${n.collection}: I${n.module}Service) {}
|
|
@@ -66,7 +66,7 @@ adapters that access it.
|
|
|
66
66
|
| ${e}Response / ${m}Summary / to${e}Response | [responses](http/contracts/${m}.responses.ts) | ORM entity | Public data projection |
|
|
67
67
|
| ${m}ListQuery | [list](http/contracts/${m}List.query.ts) | Query string | ListQuery, filters/sorting/pages |
|
|
68
68
|
${profile === "full" ? `| ${m}Reporter | [background](background/${m}.reporter.ts) | ServiceProvider, Logger, AbortSignal | Runs count and logs it |
|
|
69
|
-
| ${m}SummaryTool | [tool](
|
|
69
|
+
| ${m}SummaryTool | [tool](tools/${m}Summary.tool.ts) | I${m}Service, input, context | Summary for an agent |
|
|
70
70
|
` : ""}
|
|
71
71
|
|
|
72
72
|
## Wiring, DI and data
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createToken, type ServiceProvider } from "../di";
|
|
2
2
|
import { modelValidatorAdapter } from "../../library/validation";
|
|
3
3
|
import { AgentSetupError } from "./errors";
|
|
4
|
+
import { TOOL_EXECUTION_OPTIONS } from "./toolExecutionOptions";
|
|
4
5
|
import type { AgentDataDefinition } from "./AgentDataDefinition";
|
|
5
6
|
import { AgentRegistry, type ToolDefinition } from "./AgentRegistry";
|
|
6
7
|
import {
|
|
7
8
|
AgentExecutionDriver,
|
|
8
9
|
type AgentRuntimeInvokeOptions,
|
|
10
|
+
type AgentExecutionDriverOptions,
|
|
9
11
|
type AgentRuntimeOptions,
|
|
10
12
|
type AgentRuntimeResult,
|
|
11
13
|
} from "./internal/AgentExecutionDriver";
|
|
@@ -13,6 +15,7 @@ import type { AgentModelProvider } from "./ModelProvider";
|
|
|
13
15
|
|
|
14
16
|
export {
|
|
15
17
|
type AgentRuntimeInvokeOptions,
|
|
18
|
+
type AgentExecutionDriverOptions,
|
|
16
19
|
type AgentRuntimeOptions,
|
|
17
20
|
type AgentRuntimeResult,
|
|
18
21
|
type AgentRuntimeStatus,
|
|
@@ -42,7 +45,7 @@ export const AGENT_RUNTIME_OPTIONS = createToken<AgentRuntimeOptions>("AgentRunt
|
|
|
42
45
|
*/
|
|
43
46
|
export class AgentRuntime {
|
|
44
47
|
private readonly catalog: AgentRegistry;
|
|
45
|
-
private readonly options:
|
|
48
|
+
private readonly options: AgentExecutionDriverOptions;
|
|
46
49
|
|
|
47
50
|
constructor(
|
|
48
51
|
private readonly services: ServiceProvider,
|
|
@@ -50,9 +53,11 @@ export class AgentRuntime {
|
|
|
50
53
|
options: AgentRuntimeOptions = {},
|
|
51
54
|
) {
|
|
52
55
|
this.catalog = AgentRegistry.fromContainer(services);
|
|
56
|
+
// One Tool execution setup for agents and ToolRunner (TOOLS-001 §6).
|
|
57
|
+
const { logger: _toolLogger, ...toolExecutorOptions } = services.has(TOOL_EXECUTION_OPTIONS) ? services.resolve(TOOL_EXECUTION_OPTIONS) : {};
|
|
53
58
|
this.options = {
|
|
54
59
|
...options,
|
|
55
|
-
toolExecutorOptions: { schemaValidator: modelValidatorAdapter, ...
|
|
60
|
+
toolExecutorOptions: { schemaValidator: modelValidatorAdapter, ...toolExecutorOptions },
|
|
56
61
|
};
|
|
57
62
|
// Invalid options and hook setup fail here, at the first resolve, not on the first run.
|
|
58
63
|
new AgentExecutionDriver(services, this.catalog, modelProvider, this.options);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ToolContractValidator, toolContractName, toolErrorMessage as errorMessageOf, type AgentToolSchemaValidator } from "./internal/ToolContract.validator";
|
|
2
|
+
import type { ToolActor } from "./metadata";
|
|
2
3
|
import { ToolAuditProjector, type AgentToolAuditEntry, type AgentToolAuditFailureMode } from "./internal/ToolAudit.projector";
|
|
3
4
|
export type { AgentToolValidationIssue, AgentToolValidationResult, AgentToolSchemaValidator } from "./internal/ToolContract.validator";
|
|
4
5
|
export type { AgentToolAuditPhase, AgentToolAuditFailureMode, AgentToolAuditToolInfo, AgentToolAuditEntry } from "./internal/ToolAudit.projector";
|
|
@@ -69,6 +70,9 @@ export interface AgentToolApprovalRequest {
|
|
|
69
70
|
readonly call: AgentToolCall;
|
|
70
71
|
readonly tool: ToolDefinition;
|
|
71
72
|
readonly reason: AgentToolApprovalReason;
|
|
73
|
+
/** `"agent"` for an agent call, otherwise the runner's caller id. */
|
|
74
|
+
readonly caller: string;
|
|
75
|
+
readonly actor?: ToolActor;
|
|
72
76
|
readonly agentName?: string;
|
|
73
77
|
readonly invocationId?: string;
|
|
74
78
|
readonly idempotencyKey?: string;
|
|
@@ -111,7 +115,6 @@ export interface AgentToolExecutorOptions {
|
|
|
111
115
|
readonly defaultTimeoutMs?: number;
|
|
112
116
|
/** Maximum time to wait for a tool DI scope disposer after execution settles. */
|
|
113
117
|
readonly scopeDisposeTimeoutMs?: number;
|
|
114
|
-
readonly allowUnboundTools?: boolean;
|
|
115
118
|
readonly allowUnvalidatedSchemas?: boolean;
|
|
116
119
|
/** Side-effecting tools require an audit sink unless this explicit escape hatch is set. */
|
|
117
120
|
readonly allowUnauditedSideEffects?: boolean;
|
|
@@ -123,6 +126,13 @@ export interface AgentToolExecutorOptions {
|
|
|
123
126
|
|
|
124
127
|
export interface AgentToolExecutionOptions {
|
|
125
128
|
readonly agentName?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Who calls the Tool when there is no agent (`ToolRunner.as("workflow")`).
|
|
131
|
+
* An agent call is the `"agent"` caller. The Tool's `callers` must list it.
|
|
132
|
+
*/
|
|
133
|
+
readonly caller?: string;
|
|
134
|
+
/** On whose behalf the call runs; reaches the approval policy and the audit. */
|
|
135
|
+
readonly actor?: ToolActor;
|
|
126
136
|
readonly invocationId?: string;
|
|
127
137
|
readonly metadata?: unknown;
|
|
128
138
|
readonly approvalPolicy?: AgentToolApprovalPolicy;
|
|
@@ -135,7 +145,6 @@ export interface AgentToolExecutionOptions {
|
|
|
135
145
|
readonly timeoutMs?: number;
|
|
136
146
|
/** Per-call override for the bounded DI scope disposal wait. */
|
|
137
147
|
readonly scopeDisposeTimeoutMs?: number;
|
|
138
|
-
readonly allowUnboundTools?: boolean;
|
|
139
148
|
readonly allowUnvalidatedSchemas?: boolean;
|
|
140
149
|
readonly allowUnauditedSideEffects?: boolean;
|
|
141
150
|
readonly signal?: AbortSignal;
|
|
@@ -418,7 +427,6 @@ export class AgentToolExecutor {
|
|
|
418
427
|
private readonly retryPolicy: RetrySettings;
|
|
419
428
|
private readonly defaultTimeoutMs?: number;
|
|
420
429
|
private readonly scopeDisposeTimeoutMs: number;
|
|
421
|
-
private readonly allowUnboundTools: boolean;
|
|
422
430
|
private readonly allowUnvalidatedSchemas: boolean;
|
|
423
431
|
private readonly allowUnauditedSideEffects: boolean;
|
|
424
432
|
private readonly auditProjector: ToolAuditProjector;
|
|
@@ -445,7 +453,6 @@ export class AgentToolExecutor {
|
|
|
445
453
|
? undefined
|
|
446
454
|
: options.defaultTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
447
455
|
this.scopeDisposeTimeoutMs = options.scopeDisposeTimeoutMs ?? DEFAULT_SCOPE_DISPOSE_TIMEOUT_MS;
|
|
448
|
-
this.allowUnboundTools = options.allowUnboundTools === true;
|
|
449
456
|
this.allowUnvalidatedSchemas = options.allowUnvalidatedSchemas === true;
|
|
450
457
|
this.allowUnauditedSideEffects = options.allowUnauditedSideEffects === true;
|
|
451
458
|
this.auditProjector = new ToolAuditProjector(options.auditRedaction ?? {});
|
|
@@ -1240,18 +1247,27 @@ export class AgentToolExecutor {
|
|
|
1240
1247
|
tool: ToolDefinition,
|
|
1241
1248
|
options: AgentToolExecutionOptions,
|
|
1242
1249
|
): AgentVisibilityDecision {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
return { allowed: true };
|
|
1247
|
-
}
|
|
1250
|
+
// Who may call is declared on the Tool (TOOLS-001); tags never decide it.
|
|
1251
|
+
const caller = callerOf(options);
|
|
1252
|
+
if (caller === undefined) {
|
|
1248
1253
|
return {
|
|
1249
1254
|
allowed: false,
|
|
1250
|
-
code: "
|
|
1251
|
-
message: `Tool "${tool.metadata.name}" execution requires
|
|
1255
|
+
code: "TOOL_CALLER_REQUIRED",
|
|
1256
|
+
message: `Tool "${tool.metadata.name}" execution requires an agent or a caller.`,
|
|
1252
1257
|
status: "error",
|
|
1253
1258
|
};
|
|
1254
1259
|
}
|
|
1260
|
+
if (!tool.metadata.callers.includes(caller)) {
|
|
1261
|
+
return {
|
|
1262
|
+
allowed: false,
|
|
1263
|
+
code: "TOOL_CALLER_NOT_ALLOWED",
|
|
1264
|
+
message: `Tool "${tool.metadata.name}" may not be called by "${caller}" (callers: ${tool.metadata.callers.join(", ")}).`,
|
|
1265
|
+
status: "denied",
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (!options.agentName) {
|
|
1269
|
+
return { allowed: true };
|
|
1270
|
+
}
|
|
1255
1271
|
|
|
1256
1272
|
const agent = this.registry.getAgent(options.agentName);
|
|
1257
1273
|
if (!agent) {
|
|
@@ -1314,6 +1330,8 @@ export class AgentToolExecutor {
|
|
|
1314
1330
|
call,
|
|
1315
1331
|
tool,
|
|
1316
1332
|
reason,
|
|
1333
|
+
caller: callerOf(options) ?? "",
|
|
1334
|
+
...(options.actor === undefined ? {} : { actor: options.actor }),
|
|
1317
1335
|
agentName: options.agentName,
|
|
1318
1336
|
invocationId: options.invocationId,
|
|
1319
1337
|
idempotencyKey,
|
|
@@ -2206,3 +2224,8 @@ export class AgentToolExecutor {
|
|
|
2206
2224
|
return Math.max(0, Date.now() - startedAt);
|
|
2207
2225
|
}
|
|
2208
2226
|
}
|
|
2227
|
+
|
|
2228
|
+
/** The caller of a call: an agent call is `"agent"`, otherwise the explicit caller. */
|
|
2229
|
+
function callerOf(options: AgentToolExecutionOptions): string | undefined {
|
|
2230
|
+
return options.agentName ? "agent" : options.caller;
|
|
2231
|
+
}
|
package/core/agent/README.md
CHANGED
|
@@ -79,24 +79,25 @@ A class schema comes from codegen (the class must be reachable for OpenAPI or
|
|
|
79
79
|
be a Tool contract); `agentJsonSchema(name, schema)` takes a JSON Schema
|
|
80
80
|
directly.
|
|
81
81
|
|
|
82
|
-
Runtime settings
|
|
83
|
-
are registered once
|
|
82
|
+
Runtime settings (limits, context builder) and Tool execution settings
|
|
83
|
+
(approval policy, audit sink, retries) are registered once; the Tool settings
|
|
84
|
+
are shared by agents and `ToolRunner`:
|
|
84
85
|
|
|
85
86
|
```ts
|
|
86
87
|
@Module({
|
|
87
|
-
providers: [
|
|
88
|
-
maxSteps: 6,
|
|
89
|
-
|
|
90
|
-
approvalPolicy: (request) => request.tool.sideEffect === "read",
|
|
88
|
+
providers: [
|
|
89
|
+
DI.singleton(DI.valueProvider(AGENT_RUNTIME_OPTIONS, { maxSteps: 6 })),
|
|
90
|
+
DI.singleton(DI.valueProvider(TOOL_EXECUTION_OPTIONS, {
|
|
91
|
+
approvalPolicy: (request) => request.tool.metadata.sideEffect === "read",
|
|
91
92
|
auditSink: (entry) => auditStore.write(entry),
|
|
92
|
-
},
|
|
93
|
-
|
|
93
|
+
})),
|
|
94
|
+
],
|
|
94
95
|
})
|
|
95
96
|
export class AgentSettingsModule {}
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
Tool input and output are validated by the model validator
|
|
99
|
-
(`modelValidatorAdapter`) unless `
|
|
100
|
+
(`modelValidatorAdapter`) unless `TOOL_EXECUTION_OPTIONS.schemaValidator` says
|
|
100
101
|
otherwise. Audit entries redact `input`, `result` and execution `metadata`
|
|
101
102
|
by default using `bazis/library/redaction`.
|
|
102
103
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DI, DiError, Global, HOSTED_SERVICE, Module, registerModuleMetadataExpander, SERVICE_PROVIDER, type BazisModuleMetadata, type ServiceProvider } from "../di";
|
|
2
|
+
import { LOGGER } from "../kernel/logging/Logger";
|
|
3
|
+
import { modelValidatorAdapter } from "../../library/validation";
|
|
4
|
+
import { AgentRegistry, type ToolDefinition } from "./AgentRegistry";
|
|
5
|
+
import { AgentToolExecutor, type AgentToolExecutionOptions } from "./AgentToolExecutor";
|
|
6
|
+
import { AgentSetupError } from "./errors";
|
|
7
|
+
import { callerId, type ToolActor } from "./metadata";
|
|
8
|
+
import { describeTool } from "./Tool.contract";
|
|
9
|
+
import { agentToolCall, agentToolResult, type AgentToolContract, type AgentToolResult } from "./semantic";
|
|
10
|
+
import { TOOL_EXECUTION_OPTIONS, type ToolExecutionOptions } from "./toolExecutionOptions";
|
|
11
|
+
|
|
12
|
+
/** One call of {@link BoundToolRunner.run}. */
|
|
13
|
+
export interface ToolRunOptions extends Omit<AgentToolExecutionOptions, "agentName" | "caller" | "actor"> {
|
|
14
|
+
/** Id of the call in audit and hooks; a random uuid by default. */
|
|
15
|
+
readonly callId?: string;
|
|
16
|
+
/** On whose behalf the Tool runs. Required for `write` and `external` Tools. */
|
|
17
|
+
readonly actor?: ToolActor;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs the application's Tools without a model (TOOLS-001 §4). The framework
|
|
22
|
+
* registers it whenever a module declares `tools`; code binds it to its caller
|
|
23
|
+
* id and sees only the Tools whose `callers` list that id:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* constructor(runner: ToolRunner) { this.tools = runner.as("workflow"); }
|
|
27
|
+
* const result = await this.tools.run(step.toolName, input, { actor, idempotencyKey: stepId });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The call goes through the same executor as an agent's call: input and output
|
|
31
|
+
* validation, DI scope, approval, audit, hooks, retries and deadline, configured
|
|
32
|
+
* once with `TOOL_EXECUTION_OPTIONS`.
|
|
33
|
+
*/
|
|
34
|
+
export class ToolRunner {
|
|
35
|
+
private readonly catalog: AgentRegistry;
|
|
36
|
+
private readonly executor: AgentToolExecutor;
|
|
37
|
+
|
|
38
|
+
constructor(services: ServiceProvider, options: ToolExecutionOptions = {}) {
|
|
39
|
+
const { logger, ...executorOptions } = options;
|
|
40
|
+
this.catalog = AgentRegistry.fromContainer(services);
|
|
41
|
+
// Invalid options and hook setup fail here, when the runner is built at start.
|
|
42
|
+
this.executor = new AgentToolExecutor(services, this.catalog, { schemaValidator: modelValidatorAdapter, ...executorOptions }, logger);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A view of the Tools this caller may run. `"agent"` is refused: agents run through `AgentRuntime`. */
|
|
46
|
+
as(caller: string): BoundToolRunner {
|
|
47
|
+
let id: string;
|
|
48
|
+
try {
|
|
49
|
+
id = callerId(caller);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new AgentSetupError(`ToolRunner.as(${JSON.stringify(caller)}): a caller id is lower-case, such as "workflow".`);
|
|
52
|
+
}
|
|
53
|
+
if (id === "agent") throw new AgentSetupError('ToolRunner.as("agent"): agents run Tools through AgentRuntime.');
|
|
54
|
+
return new BoundToolRunner(id, this.catalog, this.executor);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** {@link ToolRunner} bound to one caller id: it sees and runs only that caller's Tools. */
|
|
59
|
+
export class BoundToolRunner {
|
|
60
|
+
constructor(
|
|
61
|
+
readonly caller: string,
|
|
62
|
+
private readonly catalog: AgentRegistry,
|
|
63
|
+
private readonly executor: AgentToolExecutor,
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
/** The Tools this caller may run, optionally only those that carry every one of `tags`. */
|
|
67
|
+
listTools(tags: readonly string[] = []): readonly ToolDefinition[] {
|
|
68
|
+
return this.catalog.listTools().filter((tool) => tool.metadata.callers.includes(this.caller) && tags.every((tag) => tool.metadata.tags.includes(tag)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The Tool registered under `name` if this caller may run it, otherwise `undefined`. */
|
|
72
|
+
getTool(name: string): ToolDefinition | undefined {
|
|
73
|
+
const tool = this.catalog.getTool(name);
|
|
74
|
+
return tool?.metadata.callers.includes(this.caller) ? tool : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The contract with the generated input and output JSON schemas, or `undefined`. */
|
|
78
|
+
describe(name: string): AgentToolContract | undefined {
|
|
79
|
+
const tool = this.getTool(name);
|
|
80
|
+
return tool === undefined ? undefined : describeTool(tool);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runs the Tool `name` with `input`. Never throws for a Tool failure: an
|
|
85
|
+
* unknown name, a Tool this caller may not run (TOOL_CALLER_NOT_ALLOWED),
|
|
86
|
+
* invalid input, denial, timeout or an exception comes back as the result.
|
|
87
|
+
*/
|
|
88
|
+
run(name: string, input: unknown, options: ToolRunOptions = {}): Promise<AgentToolResult> {
|
|
89
|
+
const { callId, actor, ...execution } = options;
|
|
90
|
+
const call = agentToolCall({ id: callId ?? crypto.randomUUID(), name, input: input as never });
|
|
91
|
+
const tool = this.catalog.getTool(name);
|
|
92
|
+
if (tool !== undefined && actor === undefined && (tool.metadata.sideEffect === "write" || tool.metadata.sideEffect === "external") && tool.metadata.callers.includes(this.caller)) {
|
|
93
|
+
return Promise.resolve(agentToolResult({
|
|
94
|
+
callId: call.id,
|
|
95
|
+
name,
|
|
96
|
+
status: "error",
|
|
97
|
+
error: { code: "TOOL_ACTOR_REQUIRED", message: `Tool "${name}" changes data: pass actor, the user or system the call runs for.` },
|
|
98
|
+
durationMs: 0,
|
|
99
|
+
metadata: {},
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
return this.executor.execute(call, { ...execution, caller: this.caller, ...(actor === undefined ? {} : { actor }) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Provides ToolRunner once per application and builds it at start, so wrong options stop the start. */
|
|
107
|
+
@Global()
|
|
108
|
+
@Module({
|
|
109
|
+
providers: [
|
|
110
|
+
DI.singleton(DI.factoryProviderWithResolver(ToolRunner, [], (resolver) => {
|
|
111
|
+
const services = resolver.resolve(SERVICE_PROVIDER);
|
|
112
|
+
const options = services.has(TOOL_EXECUTION_OPTIONS) ? services.resolve(TOOL_EXECUTION_OPTIONS) : {};
|
|
113
|
+
return new ToolRunner(services, { ...options, logger: options.logger ?? services.tryResolve(LOGGER) });
|
|
114
|
+
})),
|
|
115
|
+
DI.singleton(DI.factoryProviderWithResolver(HOSTED_SERVICE, [], (resolver) => ({
|
|
116
|
+
start() {
|
|
117
|
+
try {
|
|
118
|
+
resolver.resolve(ToolRunner);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof DiError) throw error;
|
|
121
|
+
throw new DiError(`ToolRunner: ${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
stop() {},
|
|
125
|
+
}))),
|
|
126
|
+
],
|
|
127
|
+
exports: [ToolRunner],
|
|
128
|
+
})
|
|
129
|
+
class ToolRunnerModule {}
|
|
130
|
+
|
|
131
|
+
// A module that declares Tools brings the runner; importing it twice is one module.
|
|
132
|
+
registerModuleMetadataExpander((metadata: BazisModuleMetadata) => {
|
|
133
|
+
const tools = (metadata as { readonly tools?: readonly unknown[] }).tools;
|
|
134
|
+
return Array.isArray(tools) && tools.length > 0 ? [ToolRunnerModule] : [];
|
|
135
|
+
});
|
package/core/agent/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ export {
|
|
|
36
36
|
type AgentRuntimeToolCallContext,
|
|
37
37
|
type AgentRuntimeToolExecutionOptions,
|
|
38
38
|
} from "./AgentRuntime";
|
|
39
|
+
export { BoundToolRunner, ToolRunner, type ToolRunOptions } from "./ToolRunner";
|
|
40
|
+
export { TOOL_EXECUTION_OPTIONS, type ToolExecutionOptions } from "./toolExecutionOptions";
|
|
39
41
|
export {
|
|
40
42
|
ApproximateAgentContextTokenEstimator,
|
|
41
43
|
DefaultAgentContextBuilder,
|
|
@@ -80,7 +80,6 @@ export interface AgentRuntimeOptions {
|
|
|
80
80
|
readonly timeoutMs?: number;
|
|
81
81
|
readonly contextBuilder?: AgentContextBuilder;
|
|
82
82
|
readonly contextLimits?: AgentContextLimits;
|
|
83
|
-
readonly toolExecutorOptions?: AgentToolExecutorOptions;
|
|
84
83
|
/**
|
|
85
84
|
* Where failures of the model and unexpected tool failures go, with their
|
|
86
85
|
* stack. The LLM connectors pass the application logger.
|
|
@@ -88,6 +87,14 @@ export interface AgentRuntimeOptions {
|
|
|
88
87
|
readonly logger?: Logger;
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @internal The driver also takes the Tool execution settings; `AgentRuntime`
|
|
92
|
+
* reads them from `TOOL_EXECUTION_OPTIONS`, shared with `ToolRunner`.
|
|
93
|
+
*/
|
|
94
|
+
export interface AgentExecutionDriverOptions extends AgentRuntimeOptions {
|
|
95
|
+
readonly toolExecutorOptions?: AgentToolExecutorOptions;
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
export interface AgentRuntimeInvokeOptions {
|
|
92
99
|
readonly id?: string;
|
|
93
100
|
readonly input?: unknown;
|
|
@@ -515,7 +522,7 @@ export class AgentExecutionDriver {
|
|
|
515
522
|
services: ServiceProvider,
|
|
516
523
|
private readonly registry: AgentRegistry,
|
|
517
524
|
private readonly modelProvider: AgentModelProvider,
|
|
518
|
-
options:
|
|
525
|
+
options: AgentExecutionDriverOptions = {},
|
|
519
526
|
) {
|
|
520
527
|
assertPositiveInteger(options.maxSteps, "AgentRuntime.maxSteps");
|
|
521
528
|
assertPositiveInteger(options.maxToolCallsPerStep, "AgentRuntime.maxToolCallsPerStep");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SensitiveRedactionOptions } from "../../../library/redaction";
|
|
2
|
+
import type { ToolActor } from "../metadata";
|
|
2
3
|
import { redactSensitive } from "../../../library/redaction";
|
|
3
4
|
import type { ToolDefinition } from "../AgentRegistry";
|
|
4
5
|
import type { AgentToolAuditHookProjectionV1 } from "../AgentToolHooks";
|
|
@@ -23,6 +24,10 @@ export interface AgentToolAuditEntry {
|
|
|
23
24
|
readonly phase: AgentToolAuditPhase;
|
|
24
25
|
readonly callId: string;
|
|
25
26
|
readonly toolName: string;
|
|
27
|
+
/** `"agent"` for an agent call, otherwise the runner's caller id. */
|
|
28
|
+
readonly caller?: string;
|
|
29
|
+
/** On whose behalf the call ran. */
|
|
30
|
+
readonly actor?: ToolActor;
|
|
26
31
|
readonly agentName?: string;
|
|
27
32
|
readonly invocationId?: string;
|
|
28
33
|
readonly attempt?: number;
|
|
@@ -39,6 +44,8 @@ export interface AgentToolAuditEntry {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
interface ToolAuditOptions {
|
|
47
|
+
readonly caller?: string;
|
|
48
|
+
readonly actor?: ToolActor;
|
|
42
49
|
readonly agentName?: string;
|
|
43
50
|
readonly invocationId?: string;
|
|
44
51
|
readonly auditRedaction?: SensitiveRedactionOptions | false;
|
|
@@ -70,6 +77,8 @@ export class ToolAuditProjector {
|
|
|
70
77
|
phase: AgentToolAuditPhase;
|
|
71
78
|
callId: string;
|
|
72
79
|
toolName: string;
|
|
80
|
+
caller?: string;
|
|
81
|
+
actor?: ToolActor;
|
|
73
82
|
agentName?: string;
|
|
74
83
|
invocationId?: string;
|
|
75
84
|
attempt?: number;
|
|
@@ -92,6 +101,13 @@ export class ToolAuditProjector {
|
|
|
92
101
|
metadata: this.redactAuditValue(metadata, options) as JsonObject,
|
|
93
102
|
};
|
|
94
103
|
|
|
104
|
+
const caller = options.agentName !== undefined || agentName !== undefined ? "agent" : options.caller;
|
|
105
|
+
if (caller !== undefined) {
|
|
106
|
+
entry.caller = caller;
|
|
107
|
+
}
|
|
108
|
+
if (options.actor !== undefined) {
|
|
109
|
+
entry.actor = options.actor;
|
|
110
|
+
}
|
|
95
111
|
if (agentName !== undefined || options.agentName !== undefined) {
|
|
96
112
|
entry.agentName = agentName ?? options.agentName;
|
|
97
113
|
}
|
package/core/agent/metadata.ts
CHANGED
|
@@ -8,6 +8,12 @@ import { AgentSetupError } from "./errors";
|
|
|
8
8
|
const TOOL_META = Symbol.for("bazis:agent:tool");
|
|
9
9
|
|
|
10
10
|
export type ToolSideEffect = "none" | "read" | "write" | "external";
|
|
11
|
+
|
|
12
|
+
/** On whose behalf a Tool runs: a user, or the system (a process, a job). */
|
|
13
|
+
export interface ToolActor {
|
|
14
|
+
readonly kind: "user" | "system";
|
|
15
|
+
readonly id: string;
|
|
16
|
+
}
|
|
11
17
|
export type ToolApproval = "never" | "policy" | "required";
|
|
12
18
|
|
|
13
19
|
export interface ToolOptions {
|
|
@@ -18,6 +24,13 @@ export interface ToolOptions {
|
|
|
18
24
|
readonly sideEffect?: ToolSideEffect;
|
|
19
25
|
readonly approval?: ToolApproval;
|
|
20
26
|
readonly timeoutMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Who may call the Tool: `"agent"` for LLM agents, any other id for code that
|
|
29
|
+
* runs Tools itself (`ToolRunner.as("workflow")`). Required and non-empty;
|
|
30
|
+
* a caller not listed is refused with TOOL_CALLER_NOT_ALLOWED.
|
|
31
|
+
*/
|
|
32
|
+
readonly callers: readonly string[];
|
|
33
|
+
/** Grouping and search only; tags never decide who may call a Tool. */
|
|
21
34
|
readonly tags?: readonly string[];
|
|
22
35
|
}
|
|
23
36
|
|
|
@@ -29,6 +42,7 @@ export interface ToolMetadata {
|
|
|
29
42
|
readonly sideEffect: ToolSideEffect;
|
|
30
43
|
readonly approval: ToolApproval;
|
|
31
44
|
readonly timeoutMs?: number;
|
|
45
|
+
readonly callers: readonly string[];
|
|
32
46
|
readonly tags: readonly string[];
|
|
33
47
|
}
|
|
34
48
|
|
|
@@ -51,6 +65,14 @@ function readonlyTextList(values: readonly string[] | undefined, field: string):
|
|
|
51
65
|
return Object.freeze(values.map((value) => text(value, field)));
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
/** A caller id: `agent` or an application id such as `workflow`. */
|
|
69
|
+
export function callerId(value: unknown): string {
|
|
70
|
+
if (typeof value !== "string" || !/^[a-z][a-z0-9_.-]{0,63}$/u.test(value)) {
|
|
71
|
+
throw new AgentSetupError(`@Tool.callers: ${JSON.stringify(value)} is not a caller id such as "agent" or "workflow".`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
function positiveInteger(value: number | undefined, field: string): number | undefined {
|
|
55
77
|
if (value === undefined) {
|
|
56
78
|
return undefined;
|
|
@@ -89,6 +111,9 @@ export function defineToolMetadata(_target: object, metadata: object, options: T
|
|
|
89
111
|
sideEffect,
|
|
90
112
|
approval: approvalFor(sideEffect, options.approval),
|
|
91
113
|
timeoutMs: positiveInteger(options.timeoutMs, "@Tool.timeoutMs"),
|
|
114
|
+
// Checked when the module registers the Tool, so a missing list prints as
|
|
115
|
+
// one configuration-error line at start instead of failing the import.
|
|
116
|
+
callers: Array.isArray(options.callers) ? Object.freeze(options.callers.map((caller) => callerId(caller))) : Object.freeze([]),
|
|
92
117
|
tags: readonlyTextList(options.tags, "@Tool.tags"),
|
|
93
118
|
});
|
|
94
119
|
(metadata as MetadataCarrier)[TOOL_META] = normalized;
|
|
@@ -60,7 +60,11 @@ registerModuleOwnedProviderContributor((metadata: BazisModuleMetadata, context)
|
|
|
60
60
|
const owner = metadata as BazisModuleRef;
|
|
61
61
|
const fields = metadata as BazisModuleMetadata & AgentModuleFields;
|
|
62
62
|
for (const target of classValues(fields.tools, "tools")) {
|
|
63
|
-
|
|
63
|
+
const tool = toolMetadataOf(target);
|
|
64
|
+
if (!tool) throw new AgentSetupError(`${target.name || "<anonymous class>"} is listed as a tool but is not decorated with @Tool(...).`);
|
|
65
|
+
if (tool.callers.length === 0) {
|
|
66
|
+
throw new AgentSetupError(`Tool "${tool.name}" (${target.name || "<anonymous class>"}) declares no callers: list who may call it, for example callers: ["agent"] for LLM agents or ["workflow"] for a process engine.`);
|
|
67
|
+
}
|
|
64
68
|
try {
|
|
65
69
|
context.registerScoped(TOOL_CHANNEL, scoped(target), Object.freeze({ target, owner }));
|
|
66
70
|
} catch (error) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createToken } from "../di";
|
|
2
|
+
import type { Logger } from "../kernel/logging/Logger";
|
|
3
|
+
import type { AgentToolExecutorOptions } from "./AgentToolExecutor";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How Tools run for every caller: approval policy, audit sink, retries,
|
|
7
|
+
* deadlines and hooks. Register one value in any module; `AgentRuntime` and
|
|
8
|
+
* `ToolRunner` read it from the root container (TOOLS-001 §6).
|
|
9
|
+
*/
|
|
10
|
+
export interface ToolExecutionOptions extends AgentToolExecutorOptions {
|
|
11
|
+
/** Where unexpected Tool failures go with their stack. Defaults to the application logger. */
|
|
12
|
+
readonly logger?: Logger;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TOOL_EXECUTION_OPTIONS = createToken<ToolExecutionOptions>("ToolExecutionOptions");
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.123.0" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.123.0",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|