@spine-event-engine/server 2.0.0-snapshot.22 → 2.0.0-snapshot.23
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 +104 -7
- package/REFERENCE.md +194 -19
- package/dist/agent/agent-admission.d.ts +80 -0
- package/dist/agent/agent-admission.d.ts.map +1 -0
- package/dist/agent/agent-admission.js +127 -0
- package/dist/agent/agent-admission.js.map +1 -0
- package/dist/agent/agent-ai-binding.d.ts +24 -0
- package/dist/agent/agent-ai-binding.d.ts.map +1 -0
- package/dist/agent/agent-ai-binding.js +48 -0
- package/dist/agent/agent-ai-binding.js.map +1 -0
- package/dist/agent/agent-ai-runtime.d.ts +76 -0
- package/dist/agent/agent-ai-runtime.d.ts.map +1 -0
- package/dist/agent/agent-ai-runtime.js +1269 -0
- package/dist/agent/agent-ai-runtime.js.map +1 -0
- package/dist/agent/agent-execution-capacity.d.ts +43 -0
- package/dist/agent/agent-execution-capacity.d.ts.map +1 -0
- package/dist/agent/agent-execution-capacity.js +147 -0
- package/dist/agent/agent-execution-capacity.js.map +1 -0
- package/dist/agent/agent-execution-fault.d.ts +18 -0
- package/dist/agent/agent-execution-fault.d.ts.map +1 -0
- package/dist/agent/agent-execution-fault.js +31 -0
- package/dist/agent/agent-execution-fault.js.map +1 -0
- package/dist/agent/agent-execution-session.d.ts +103 -0
- package/dist/agent/agent-execution-session.d.ts.map +1 -0
- package/dist/agent/agent-execution-session.js +238 -0
- package/dist/agent/agent-execution-session.js.map +1 -0
- package/dist/agent/agent-history.d.ts +148 -0
- package/dist/agent/agent-history.d.ts.map +1 -0
- package/dist/agent/agent-history.js +340 -0
- package/dist/agent/agent-history.js.map +1 -0
- package/dist/agent/agent-interaction-audit.d.ts +58 -0
- package/dist/agent/agent-interaction-audit.d.ts.map +1 -0
- package/dist/agent/agent-interaction-audit.js +114 -0
- package/dist/agent/agent-interaction-audit.js.map +1 -0
- package/dist/agent/agent-interaction-events.d.ts +40 -0
- package/dist/agent/agent-interaction-events.d.ts.map +1 -0
- package/dist/agent/agent-interaction-events.js +129 -0
- package/dist/agent/agent-interaction-events.js.map +1 -0
- package/dist/agent/agent-mcp-budget.d.ts +125 -0
- package/dist/agent/agent-mcp-budget.d.ts.map +1 -0
- package/dist/agent/agent-mcp-budget.js +566 -0
- package/dist/agent/agent-mcp-budget.js.map +1 -0
- package/dist/agent/agent-mcp-host.d.ts +125 -0
- package/dist/agent/agent-mcp-host.d.ts.map +1 -0
- package/dist/agent/agent-mcp-host.js +15 -0
- package/dist/agent/agent-mcp-host.js.map +1 -0
- package/dist/agent/agent-mcp-runtime.d.ts +199 -0
- package/dist/agent/agent-mcp-runtime.d.ts.map +1 -0
- package/dist/agent/agent-mcp-runtime.js +621 -0
- package/dist/agent/agent-mcp-runtime.js.map +1 -0
- package/dist/agent/agent-model-selection.d.ts +129 -0
- package/dist/agent/agent-model-selection.d.ts.map +1 -0
- package/dist/agent/agent-model-selection.js +342 -0
- package/dist/agent/agent-model-selection.js.map +1 -0
- package/dist/agent/agent-read-runtime.d.ts +70 -0
- package/dist/agent/agent-read-runtime.d.ts.map +1 -0
- package/dist/agent/agent-read-runtime.js +320 -0
- package/dist/agent/agent-read-runtime.js.map +1 -0
- package/dist/agent/agent-revisions.d.ts +35 -0
- package/dist/agent/agent-revisions.d.ts.map +1 -0
- package/dist/agent/agent-revisions.js +133 -0
- package/dist/agent/agent-revisions.js.map +1 -0
- package/dist/agent/agent-scheduler.d.ts +99 -0
- package/dist/agent/agent-scheduler.d.ts.map +1 -0
- package/dist/agent/agent-scheduler.js +398 -0
- package/dist/agent/agent-scheduler.js.map +1 -0
- package/dist/bus/command-bus.d.ts +62 -0
- package/dist/bus/command-bus.d.ts.map +1 -1
- package/dist/bus/command-bus.js +51 -2
- package/dist/bus/command-bus.js.map +1 -1
- package/dist/bus/event-bus.d.ts +137 -3
- package/dist/bus/event-bus.d.ts.map +1 -1
- package/dist/bus/event-bus.js +133 -2
- package/dist/bus/event-bus.js.map +1 -1
- package/dist/bus/saved-dispatcher-binding.d.ts +96 -0
- package/dist/bus/saved-dispatcher-binding.d.ts.map +1 -0
- package/dist/bus/saved-dispatcher-binding.js +35 -0
- package/dist/bus/saved-dispatcher-binding.js.map +1 -0
- package/dist/context/bounded-context.d.ts +36 -2
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +253 -54
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/context/tenant-index.d.ts +25 -0
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +45 -1
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/entity/entity.d.ts +82 -3
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +121 -22
- package/dist/entity/entity.js.map +1 -1
- package/dist/handler/generated-handler-registry.js +8 -3
- package/dist/handler/generated-handler-registry.js.map +1 -1
- package/dist/handler/handler-metadata.d.ts +17 -1
- package/dist/handler/handler-metadata.d.ts.map +1 -1
- package/dist/handler/handler-metadata.js +10 -4
- package/dist/handler/handler-metadata.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/repository/commit-fence.d.ts +11 -0
- package/dist/repository/commit-fence.d.ts.map +1 -1
- package/dist/repository/commit-fence.js +13 -0
- package/dist/repository/commit-fence.js.map +1 -1
- package/dist/repository/repository.d.ts +222 -13
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +2323 -123
- package/dist/repository/repository.js.map +1 -1
- package/dist/runtime/signal-publisher.d.ts +6 -0
- package/dist/runtime/signal-publisher.d.ts.map +1 -1
- package/dist/runtime/signal-publisher.js +13 -0
- package/dist/runtime/signal-publisher.js.map +1 -1
- package/dist/runtime/standalone-handler-runtime.d.ts +7 -0
- package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
- package/dist/runtime/standalone-handler-runtime.js +148 -11
- package/dist/runtime/standalone-handler-runtime.js.map +1 -1
- package/dist/server/server.d.ts +9 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +13 -1
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts +57 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +87 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -12
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { clone, create, toBinary } from "@bufbuild/protobuf";
|
|
15
|
+
import { AnySchema, TimestampSchema } from "@bufbuild/protobuf/wkt";
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
+
import { AgentExecutionFault } from "./agent-execution-fault.js";
|
|
18
|
+
import { AnyMessages, Time, TypeUrls, Validate, } from "@spine-event-engine/core";
|
|
19
|
+
import { admitDecision, backendDefinition, isAiModel, parseCandidate, selectDeployment, } from "@spine-event-engine/ai/spi/runtime";
|
|
20
|
+
import { AiMcpSetupFailure } from "@spine-event-engine/ai/spi/adapter";
|
|
21
|
+
import { AiAttemptIdSchema, AiCapabilityNameSchema, AiCapabilityRevisionSchema, AiContentDigestSchema, AiDeadlineMillisSchema, AiDiagnosticIdSchema, AiFailureCode, AiInputByteLimitSchema as InputBytesSchema, AiModelKind, AiExecutionMode, AgentAiOperationStartedSchema as OperationStartedSchema, AgentModelAttemptStartedSchema as AttemptStartedSchema, AgentModelAttemptFinishedSchema as AttemptFinishedSchema, AgentAiResultAdmittedSchema as ResultAdmittedSchema, AgentAiOperationFailedSchema as OperationFailedSchema, AiValidationRevisionSchema, ModelRefSchema, AiModelRequestLimitSchema as RequestLimitSchema, AiOperationIdSchema, AiOperationLimitsSchema, AiOutputByteLimitSchema as OutputBytesSchema, AiOutputTokenLimitSchema as OutputTokensSchema, AiOutcome, AiToolCallLimitSchema as ToolLimitSchema, ConversationIdSchema, DecisionRequestSchema, DecisionAnswerSchema, DecisionQuestionIdSchema, DecisionQuestionKind, DecisionResponseSchema, GenerationRequestSchema, GenerationResponseSchema, AgentHistoryEntrySchema, ConversationRecordIdSchema, ConversationRecordSchema, } from "@spine-event-engine/proto/agent";
|
|
22
|
+
import { AgentAttemptEvidenceSchema, AgentExecutionJournalEntrySchema as JournalEntrySchema, AgentNamedOperationSchema, AgentExecutionHeadSchema, AgentExecutionRecordSchema, AgentSavedFailureSchema, AgentValidationIssueSchema, } from "@spine-event-engine/proto/generated/spine/server/agent/execution_record_pb.js";
|
|
23
|
+
import { AgentExecutionSizes } from "@spine-event-engine/storage/provider";
|
|
24
|
+
import { AgentExecutionSession } from "./agent-execution-session.js";
|
|
25
|
+
import { AgentMcpBudget } from "./agent-mcp-budget.js";
|
|
26
|
+
import { AgentMcpRuntime } from "./agent-mcp-runtime.js";
|
|
27
|
+
import { AgentModelSelection } from "./agent-model-selection.js";
|
|
28
|
+
import { AgentInteractionAudit } from "./agent-interaction-audit.js";
|
|
29
|
+
const failureCodes = Object.freeze({
|
|
30
|
+
INVALID_INPUT: AiFailureCode.INVALID_INPUT,
|
|
31
|
+
UNSUPPORTED_CAPABILITY: AiFailureCode.UNSUPPORTED_CAPABILITY,
|
|
32
|
+
AUTHENTICATION_REQUIRED: AiFailureCode.AUTHENTICATION_REQUIRED,
|
|
33
|
+
RATE_LIMITED: AiFailureCode.RATE_LIMITED,
|
|
34
|
+
UNAVAILABLE: AiFailureCode.UNAVAILABLE,
|
|
35
|
+
REFUSED: AiFailureCode.REFUSED,
|
|
36
|
+
INVALID_OUTPUT: AiFailureCode.INVALID_OUTPUT,
|
|
37
|
+
BUDGET_EXCEEDED: AiFailureCode.BUDGET_EXCEEDED,
|
|
38
|
+
TOOL_FAILED: AiFailureCode.TOOL_FAILED,
|
|
39
|
+
TOOL_OUTCOME_UNKNOWN: AiFailureCode.TOOL_OUTCOME_UNKNOWN,
|
|
40
|
+
CANCELLED: AiFailureCode.CANCELLED,
|
|
41
|
+
DEADLINE_EXCEEDED: AiFailureCode.DEADLINE_EXCEEDED,
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Indicates a saved physical request with no recorded response after recovery.
|
|
45
|
+
*/
|
|
46
|
+
export class AgentUncertainAttemptError extends Error {
|
|
47
|
+
/**
|
|
48
|
+
* Creates an error that prevents replaying an unresolved physical request.
|
|
49
|
+
*/
|
|
50
|
+
constructor() {
|
|
51
|
+
super("Agent physical request outcome is uncertain; it cannot be resent.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Provides one handler's named AI calls against a fenced execution journal.
|
|
56
|
+
*/
|
|
57
|
+
export class AgentAiRuntime {
|
|
58
|
+
registry;
|
|
59
|
+
repository;
|
|
60
|
+
scope;
|
|
61
|
+
session;
|
|
62
|
+
handlerOrdinal;
|
|
63
|
+
audit;
|
|
64
|
+
#names = new Set();
|
|
65
|
+
#controller = new AbortController();
|
|
66
|
+
#signal;
|
|
67
|
+
#cursor = 0;
|
|
68
|
+
#busy = false;
|
|
69
|
+
#closed = false;
|
|
70
|
+
/**
|
|
71
|
+
* Binds named calls to the selected repository and fenced invocation.
|
|
72
|
+
*
|
|
73
|
+
* @param registry Configured model and MCP registrations.
|
|
74
|
+
* @param repository Repository model policy.
|
|
75
|
+
* @param scope Accepted actor, source, Bounded Context, and tenant.
|
|
76
|
+
* @param session Fenced durable execution session.
|
|
77
|
+
* @param handlerOrdinal Saved generated handler order.
|
|
78
|
+
* @param audit Paired Agent history and System Event writer.
|
|
79
|
+
*/
|
|
80
|
+
constructor(registry, repository, scope, session, handlerOrdinal, audit) {
|
|
81
|
+
this.registry = registry;
|
|
82
|
+
this.repository = repository;
|
|
83
|
+
this.scope = scope;
|
|
84
|
+
this.session = session;
|
|
85
|
+
this.handlerOrdinal = handlerOrdinal;
|
|
86
|
+
this.audit = audit;
|
|
87
|
+
this.#signal = AbortSignal.any([session.signal, this.#controller.signal]);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns one saved or newly admitted typed model result.
|
|
91
|
+
*
|
|
92
|
+
* @typeParam I Generated input descriptor.
|
|
93
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
94
|
+
* @param model Registered capability to invoke.
|
|
95
|
+
* @param request Named call, conversation, and typed input.
|
|
96
|
+
* @returns Saved or newly admitted result with its operation identity.
|
|
97
|
+
*/
|
|
98
|
+
async invoke(model, request) {
|
|
99
|
+
this.#requireOpen();
|
|
100
|
+
if (this.#busy)
|
|
101
|
+
throw new Error("Agent AI calls must be sequential within one handler.");
|
|
102
|
+
this.#busy = true;
|
|
103
|
+
try {
|
|
104
|
+
if (!isAiModel(model) || !this.repository.models.includes(model))
|
|
105
|
+
throw new TypeError("Agent model is not registered for this repository.");
|
|
106
|
+
if (request.call.trim().length === 0 || this.#names.has(request.call))
|
|
107
|
+
throw new TypeError("Agent AI call names must be nonblank and unique per handler.");
|
|
108
|
+
this.#names.add(request.call);
|
|
109
|
+
Validate.check(ConversationIdSchema, request.conversation);
|
|
110
|
+
Validate.check(model.definition.input, request.input);
|
|
111
|
+
const operation = await this.#operation(model, request);
|
|
112
|
+
this.#requireOpen();
|
|
113
|
+
return (this.#savedResult(model, operation) ?? (await this.#dispatch(model, request, operation)));
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
this.#busy = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Restores a terminal named result without contacting the backend.
|
|
121
|
+
*
|
|
122
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
123
|
+
*/
|
|
124
|
+
#savedResult(model, operation) {
|
|
125
|
+
const operationId = operation.operation;
|
|
126
|
+
if (operationId === undefined)
|
|
127
|
+
throw new Error("Agent named operation has no durable ID.");
|
|
128
|
+
if (operation.result.case === "admittedOutput") {
|
|
129
|
+
const value = AnyMessages.unpack(operation.result.value, model.definition.output);
|
|
130
|
+
if (value === undefined)
|
|
131
|
+
throw new Error("Saved Agent AI output type changed.");
|
|
132
|
+
return { ok: true, value, operationId };
|
|
133
|
+
}
|
|
134
|
+
if (operation.result.case === "failure") {
|
|
135
|
+
const saved = operation.result.value;
|
|
136
|
+
const code = Object.entries(failureCodes).find(([, value]) => value === saved.code)?.[0];
|
|
137
|
+
if (code === undefined || saved.diagnostic === undefined)
|
|
138
|
+
throw new Error("Saved Agent AI failure category is invalid.");
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
operationId,
|
|
142
|
+
failure: {
|
|
143
|
+
code: code,
|
|
144
|
+
retryableByNewSignal: saved.retryableByNewSignal,
|
|
145
|
+
diagnosticId: saved.diagnostic.value,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Sets a preference only for a later accepted signal.
|
|
153
|
+
*
|
|
154
|
+
* @param kind Generation or decision selection.
|
|
155
|
+
* @param model Deployment to prefer for later accepted work.
|
|
156
|
+
*/
|
|
157
|
+
select(kind, model) {
|
|
158
|
+
this.#requireOpen();
|
|
159
|
+
const selectedKind = kind === AiModelKind.GENERATION
|
|
160
|
+
? "generation"
|
|
161
|
+
: kind === AiModelKind.DECISION
|
|
162
|
+
? "decision"
|
|
163
|
+
: undefined;
|
|
164
|
+
if (selectedKind === undefined)
|
|
165
|
+
throw new TypeError("Agent model preference requires a supported kind.");
|
|
166
|
+
if (model !== undefined) {
|
|
167
|
+
Validate.check(ModelRefSchema, model);
|
|
168
|
+
selectDeployment(this.registry, {
|
|
169
|
+
kind: selectedKind,
|
|
170
|
+
models: this.repository.models,
|
|
171
|
+
instancePreference: model,
|
|
172
|
+
...(this.repository.allowedModels?.[selectedKind] === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { allowedModels: this.repository.allowedModels[selectedKind] }),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
this.session.stagePreference(kind, model);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Rejects recovery when the accepted handler no longer issues its saved calls.
|
|
181
|
+
*/
|
|
182
|
+
finish() {
|
|
183
|
+
this.#requireOpen();
|
|
184
|
+
if (this.#busy)
|
|
185
|
+
throw new Error("Agent handler returned with a pending AI call.");
|
|
186
|
+
const saved = this.#savedOperations(this.session.record());
|
|
187
|
+
if (this.#cursor !== saved.length)
|
|
188
|
+
throw new AgentExecutionFault("REPLAY_DIVERGENCE", "Agent named operation sequence changed on recovery.");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Rejects callbacks retained after the generated handler returns.
|
|
192
|
+
*/
|
|
193
|
+
close() {
|
|
194
|
+
this.#closed = true;
|
|
195
|
+
this.#controller.abort();
|
|
196
|
+
}
|
|
197
|
+
#requireOpen() {
|
|
198
|
+
if (this.#closed || this.#signal.aborted)
|
|
199
|
+
throw new Error("Agent AI handler capability is closed.");
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Loads or persists the next exact handler-scoped named operation.
|
|
203
|
+
*
|
|
204
|
+
* @typeParam I Generated input descriptor.
|
|
205
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
206
|
+
*/
|
|
207
|
+
async #operation(model, request) {
|
|
208
|
+
const saved = this.#savedOperations(this.session.record())[this.#cursor++];
|
|
209
|
+
if (saved !== undefined) {
|
|
210
|
+
this.#verifyOperation(saved, model, request);
|
|
211
|
+
return saved;
|
|
212
|
+
}
|
|
213
|
+
const created = this.#newOperation(model, request);
|
|
214
|
+
const change = (record) => {
|
|
215
|
+
record.journal.push(create(JournalEntrySchema, {
|
|
216
|
+
ordinal: BigInt(record.journal.length),
|
|
217
|
+
evidence: { case: "operation", value: created },
|
|
218
|
+
}));
|
|
219
|
+
return record;
|
|
220
|
+
};
|
|
221
|
+
if (this.audit === undefined)
|
|
222
|
+
await this.session.update(change);
|
|
223
|
+
else
|
|
224
|
+
await this.audit.save(OperationStartedSchema, this.#operationStarted(model, request, created), change);
|
|
225
|
+
return created;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Describes a newly journaled named operation using its original typed input.
|
|
229
|
+
*
|
|
230
|
+
* @typeParam I Generated input descriptor.
|
|
231
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
232
|
+
*/
|
|
233
|
+
#operationStarted(model, request, operation) {
|
|
234
|
+
const kind = model.definition.kind === "generation" ? AiModelKind.GENERATION : AiModelKind.DECISION;
|
|
235
|
+
const mode = model.definition.kind === "decision"
|
|
236
|
+
? AiExecutionMode.DECISION_DIRECT
|
|
237
|
+
: model.definition.outputMode === "native-schema"
|
|
238
|
+
? AiExecutionMode.GENERATION_NATIVE_SCHEMA
|
|
239
|
+
: AiExecutionMode.GENERATION_PROMPT_VALIDATE;
|
|
240
|
+
const digest = createHash("sha256")
|
|
241
|
+
.update(toBinary(model.definition.input, request.input))
|
|
242
|
+
.digest("hex");
|
|
243
|
+
return create(OperationStartedSchema, {
|
|
244
|
+
operation: this.audit?.reference(operation, kind),
|
|
245
|
+
capability: operation.capability,
|
|
246
|
+
capabilityRevision: operation.capabilityRevision,
|
|
247
|
+
input: create(AiContentDigestSchema, { value: digest }),
|
|
248
|
+
limits: operation.limits,
|
|
249
|
+
startedAt: Time.currentTime(),
|
|
250
|
+
mode,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Rejects changed call facts before any recovery side effect.
|
|
255
|
+
*
|
|
256
|
+
* @typeParam I Generated input descriptor.
|
|
257
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
258
|
+
*/
|
|
259
|
+
#verifyOperation(saved, model, request) {
|
|
260
|
+
if (saved.callName !== request.call ||
|
|
261
|
+
saved.conversation?.value !== request.conversation.value ||
|
|
262
|
+
saved.capability?.value !== model.definition.name ||
|
|
263
|
+
saved.capabilityRevision?.value !== model.definition.version ||
|
|
264
|
+
!this.#sameInput(saved, model.definition.input, request.input))
|
|
265
|
+
throw new AgentExecutionFault("REPLAY_DIVERGENCE", "Agent named operation changed on recovery.");
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Captures one typed call's immutable accepted facts.
|
|
269
|
+
*
|
|
270
|
+
* @typeParam I Generated input descriptor.
|
|
271
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
272
|
+
*/
|
|
273
|
+
#newOperation(model, request) {
|
|
274
|
+
const deadline = this.#operationDeadline(model.definition.limits.deadlineMs);
|
|
275
|
+
return create(AgentNamedOperationSchema, {
|
|
276
|
+
handlerOrdinal: this.handlerOrdinal,
|
|
277
|
+
callName: request.call,
|
|
278
|
+
conversation: clone(ConversationIdSchema, request.conversation),
|
|
279
|
+
operation: create(AiOperationIdSchema, { value: randomUUID() }),
|
|
280
|
+
limits: this.#limits(model),
|
|
281
|
+
input: AnyMessages.pack(model.definition.input, request.input),
|
|
282
|
+
capability: create(AiCapabilityNameSchema, { value: model.definition.name }),
|
|
283
|
+
capabilityRevision: create(AiCapabilityRevisionSchema, { value: model.definition.version }),
|
|
284
|
+
...(deadline === undefined ? {} : { deadline }),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Samples Time once and caps a capability deadline at the saved invocation deadline.
|
|
289
|
+
*/
|
|
290
|
+
#operationDeadline(durationMs) {
|
|
291
|
+
const invocationDeadline = this.#invocationDeadline();
|
|
292
|
+
if (durationMs === undefined && invocationDeadline === undefined)
|
|
293
|
+
return undefined;
|
|
294
|
+
const now = Time.currentTime();
|
|
295
|
+
const nowMs = Number(now.seconds) * 1_000 + Math.floor(now.nanos / 1_000_000);
|
|
296
|
+
const deadlineMs = durationMs === undefined
|
|
297
|
+
? invocationDeadline
|
|
298
|
+
: invocationDeadline === undefined
|
|
299
|
+
? nowMs + durationMs
|
|
300
|
+
: Math.min(nowMs + durationMs, invocationDeadline);
|
|
301
|
+
if (deadlineMs === undefined)
|
|
302
|
+
throw new Error("Agent deadline calculation unavailable.");
|
|
303
|
+
return create(TimestampSchema, {
|
|
304
|
+
seconds: BigInt(Math.floor(deadlineMs / 1_000)),
|
|
305
|
+
nanos: (deadlineMs % 1_000) * 1_000_000,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Calls the selected authenticated backend after recording the named operation.
|
|
310
|
+
*
|
|
311
|
+
* @typeParam I Generated input descriptor.
|
|
312
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
313
|
+
*/
|
|
314
|
+
async #dispatch(model, request, operation) {
|
|
315
|
+
const prepared = await this.#prepareExecution(model, request, operation);
|
|
316
|
+
if (prepared instanceof AiMcpSetupFailure)
|
|
317
|
+
return this.#setupFailure(model, operation, prepared);
|
|
318
|
+
const { backend, execution, mcp } = prepared;
|
|
319
|
+
try {
|
|
320
|
+
this.#requireOpen();
|
|
321
|
+
const outcome = await AgentModelSelection.awaitHook(backend.execute(execution), this.#signal, this.#deadline(operation));
|
|
322
|
+
this.#requireOpen();
|
|
323
|
+
if (outcome.ok)
|
|
324
|
+
Validate.check(model.definition.output, outcome.value);
|
|
325
|
+
await this.#saveOutcome(model, operation, outcome);
|
|
326
|
+
return outcome.ok
|
|
327
|
+
? {
|
|
328
|
+
ok: true,
|
|
329
|
+
value: outcome.value,
|
|
330
|
+
operationId: this.#operationId(operation),
|
|
331
|
+
}
|
|
332
|
+
: { ok: false, failure: outcome.failure, operationId: this.#operationId(operation) };
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await this.#closeMcp(mcp, operation);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Classifies only a typed MCP setup failure from execution preparation.
|
|
340
|
+
*
|
|
341
|
+
* @typeParam I Generated input descriptor.
|
|
342
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
343
|
+
*/
|
|
344
|
+
async #prepareExecution(model, request, operation) {
|
|
345
|
+
try {
|
|
346
|
+
return await this.#execution(model, request, operation);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (error instanceof AiMcpSetupFailure)
|
|
350
|
+
return error;
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Saves only a typed MCP preparation denial as the named operation result.
|
|
356
|
+
*
|
|
357
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
358
|
+
*/
|
|
359
|
+
async #setupFailure(model, operation, error) {
|
|
360
|
+
this.#requireOpen();
|
|
361
|
+
const failure = await this.#recordFailure(operation, error.code, false);
|
|
362
|
+
await this.#saveOutcome(model, operation, { ok: false, failure });
|
|
363
|
+
return { ok: false, failure, operationId: this.#operationId(operation) };
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Selects the saved deployment under current registry policy.
|
|
367
|
+
*/
|
|
368
|
+
#selectedBackend(model) {
|
|
369
|
+
const selected = this.session
|
|
370
|
+
.record()
|
|
371
|
+
.started?.models.find((item) => item.kind ===
|
|
372
|
+
(model.definition.kind === "generation" ? AiModelKind.GENERATION : AiModelKind.DECISION));
|
|
373
|
+
if (selected?.model === undefined || selected.connection === undefined)
|
|
374
|
+
throw new Error("Agent AI kind has no saved authenticated deployment.");
|
|
375
|
+
const registration = selectDeployment(this.registry, {
|
|
376
|
+
kind: model.definition.kind,
|
|
377
|
+
models: this.repository.models,
|
|
378
|
+
instancePreference: selected.model,
|
|
379
|
+
...(this.repository.allowedModels?.[model.definition.kind] === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: { allowedModels: this.repository.allowedModels[model.definition.kind] }),
|
|
382
|
+
});
|
|
383
|
+
const backend = backendDefinition(registration);
|
|
384
|
+
return { backend, connection: selected.connection };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Reauthorizes and binds the saved operation to the adapter execution.
|
|
388
|
+
*
|
|
389
|
+
* @typeParam I Generated input descriptor.
|
|
390
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
391
|
+
*/
|
|
392
|
+
async #execution(model, request, operation) {
|
|
393
|
+
const { backend, connection } = this.#selectedBackend(model);
|
|
394
|
+
const identity = await this.#connectIdentity(backend, connection, operation);
|
|
395
|
+
const tools = model.definition.kind === "generation" ? (model.definition.tools ?? []) : [];
|
|
396
|
+
const mcp = tools.length
|
|
397
|
+
? new AgentMcpRuntime(this.#mcpHost(backend, operation, model), tools)
|
|
398
|
+
: undefined;
|
|
399
|
+
try {
|
|
400
|
+
const execution = await this.#connectExecution(backend, model, request, operation, identity, mcp);
|
|
401
|
+
return { backend, execution, mcp };
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
await this.#closeMcp(mcp, operation);
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Records safe cleanup failure without replacing a saved result or setup error.
|
|
410
|
+
*/
|
|
411
|
+
async #closeMcp(mcp, operation) {
|
|
412
|
+
if (mcp === undefined)
|
|
413
|
+
return;
|
|
414
|
+
try {
|
|
415
|
+
await mcp.close();
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
try {
|
|
419
|
+
await this.#recordFailure(operation, "TOOL_FAILED", false);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// The original result or setup error remains authoritative after cleanup failure.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Prepares the exact catalog and reconnects the saved model identity.
|
|
428
|
+
*
|
|
429
|
+
* @typeParam I Generated input descriptor.
|
|
430
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
431
|
+
*/
|
|
432
|
+
async #connectExecution(backend, model, request, operation, identity, mcp) {
|
|
433
|
+
const advertisedTools = await mcp?.prepare();
|
|
434
|
+
const control = this.#control(operation, model, mcp);
|
|
435
|
+
const connected = await AgentModelSelection.awaitHook(backend.connect(this.scope, identity, control), this.#signal, control.deadlineEpochMs);
|
|
436
|
+
if (!this.#sameIdentity(connected.identity, identity))
|
|
437
|
+
throw new AgentExecutionFault("REVISION_CHANGED", "Agent backend connection identity changed.");
|
|
438
|
+
return this.#adapterExecution(model, request, operation, identity, connected.model, control, advertisedTools);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Materializes the checked adapter invocation after connection and catalog setup.
|
|
442
|
+
*
|
|
443
|
+
* @typeParam I Generated input descriptor.
|
|
444
|
+
* @typeParam O Generated admitted-output descriptor.
|
|
445
|
+
*/
|
|
446
|
+
#adapterExecution(model, request, operation, identity, connectedModel, control, advertisedTools) {
|
|
447
|
+
return {
|
|
448
|
+
call: request.call,
|
|
449
|
+
operationId: this.#operationId(operation),
|
|
450
|
+
scope: this.scope,
|
|
451
|
+
identity,
|
|
452
|
+
model: connectedModel,
|
|
453
|
+
// Adapter SPI erases the factory-validated descriptor pair at this boundary.
|
|
454
|
+
definition: model.definition,
|
|
455
|
+
input: request.input,
|
|
456
|
+
control,
|
|
457
|
+
...(advertisedTools === undefined ? {} : { advertisedTools }),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Binds the MCP module to this invocation's single fenced budget authority.
|
|
462
|
+
*/
|
|
463
|
+
#mcpHost(backend, operation, model) {
|
|
464
|
+
const deadlineEpochMs = this.#deadline(operation);
|
|
465
|
+
const budget = new AgentMcpBudget(this.session, operation, deadlineEpochMs, this.audit);
|
|
466
|
+
return {
|
|
467
|
+
scope: this.scope,
|
|
468
|
+
registry: this.registry,
|
|
469
|
+
backend,
|
|
470
|
+
operation,
|
|
471
|
+
session: this.session,
|
|
472
|
+
signal: this.#signal,
|
|
473
|
+
...(deadlineEpochMs === undefined ? {} : { deadlineEpochMs }),
|
|
474
|
+
reserveMessage: (server, message) => budget.reserveMessage(server, message),
|
|
475
|
+
onReceived: (id, bytes) => {
|
|
476
|
+
budget.onReceived(id, bytes);
|
|
477
|
+
},
|
|
478
|
+
finishMessage: (id, bytes) => budget.finishMessage(id, bytes),
|
|
479
|
+
journalToolIntent: (invocation, facts) => budget.journalToolIntent(invocation, facts, model.definition.limits.toolCalls),
|
|
480
|
+
markToolDispatched: (id) => budget.markToolDispatched(id),
|
|
481
|
+
finishTool: (id, response, history) => budget.finishTool(id, response, [history]),
|
|
482
|
+
recordFailure: (code, retryable) => this.#recordFailure(operation, code, retryable),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Retains the admitted result before the domain handler can observe it.
|
|
487
|
+
*/
|
|
488
|
+
async #saveOutcome(model, operation, outcome) {
|
|
489
|
+
const change = this.#outcomeChange(model, operation, outcome);
|
|
490
|
+
if (this.audit === undefined)
|
|
491
|
+
await this.session.update(change);
|
|
492
|
+
else if (outcome.ok)
|
|
493
|
+
await this.audit.save(ResultAdmittedSchema, this.#admittedEvent(model, operation, outcome), change);
|
|
494
|
+
else
|
|
495
|
+
await this.audit.save(OperationFailedSchema, this.#failedEvent(model, operation, outcome), change);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Applies the final typed result to its named operation.
|
|
499
|
+
*/
|
|
500
|
+
#outcomeChange(model, operation, outcome) {
|
|
501
|
+
return (record) => {
|
|
502
|
+
const saved = this.#findOperation(record, operation);
|
|
503
|
+
saved.result = outcome.ok
|
|
504
|
+
? {
|
|
505
|
+
case: "admittedOutput",
|
|
506
|
+
value: AnyMessages.pack(model.definition.output, outcome.value),
|
|
507
|
+
}
|
|
508
|
+
: {
|
|
509
|
+
case: "failure",
|
|
510
|
+
value: create(AgentSavedFailureSchema, {
|
|
511
|
+
code: failureCodes[outcome.failure.code],
|
|
512
|
+
retryableByNewSignal: outcome.failure.retryableByNewSignal,
|
|
513
|
+
diagnostic: create(AiDiagnosticIdSchema, { value: outcome.failure.diagnosticId }),
|
|
514
|
+
}),
|
|
515
|
+
};
|
|
516
|
+
const response = record.journal.findLast((entry) => entry.evidence.case === "attempt" &&
|
|
517
|
+
entry.evidence.value.operation?.value === operation.operation?.value);
|
|
518
|
+
saved.outcome =
|
|
519
|
+
response?.evidence.case === "attempt" ? response.evidence.value.outcome : AiOutcome.FAILED;
|
|
520
|
+
return record;
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Describes one locally admitted output without exposing its content.
|
|
525
|
+
*/
|
|
526
|
+
#admittedEvent(model, operation, outcome) {
|
|
527
|
+
return create(ResultAdmittedSchema, {
|
|
528
|
+
operation: this.audit?.reference(operation, this.#modelKind(model)),
|
|
529
|
+
result: create(AiContentDigestSchema, {
|
|
530
|
+
value: createHash("sha256")
|
|
531
|
+
.update(toBinary(model.definition.output, outcome.value))
|
|
532
|
+
.digest("hex"),
|
|
533
|
+
}),
|
|
534
|
+
validationRevision: create(AiValidationRevisionSchema, {
|
|
535
|
+
value: model.definition.validation?.version ?? model.definition.version,
|
|
536
|
+
}),
|
|
537
|
+
admittedAt: Time.currentTime(),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Describes one terminal logical failure with its original safe diagnostic.
|
|
542
|
+
*/
|
|
543
|
+
#failedEvent(model, operation, outcome) {
|
|
544
|
+
const last = this.#lastAttempt(operation);
|
|
545
|
+
return create(OperationFailedSchema, {
|
|
546
|
+
operation: this.audit?.reference(operation, this.#modelKind(model)),
|
|
547
|
+
outcome: last?.outcome === AiOutcome.TOOL_REQUESTED
|
|
548
|
+
? AiOutcome.FAILED
|
|
549
|
+
: (last?.outcome ?? AiOutcome.FAILED),
|
|
550
|
+
diagnosticId: create(AiDiagnosticIdSchema, { value: outcome.failure.diagnosticId }),
|
|
551
|
+
...(last?.attempt === undefined ? {} : { attempt: last.attempt }),
|
|
552
|
+
failedAt: Time.currentTime(),
|
|
553
|
+
failure: failureCodes[outcome.failure.code],
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Finds the last saved physical attempt of one named operation.
|
|
558
|
+
*/
|
|
559
|
+
#lastAttempt(operation) {
|
|
560
|
+
const entry = this.session
|
|
561
|
+
.record()
|
|
562
|
+
.journal.findLast((item) => item.evidence.case === "attempt" &&
|
|
563
|
+
item.evidence.value.operation?.value === operation.operation?.value);
|
|
564
|
+
return entry?.evidence.case === "attempt" ? entry.evidence.value : undefined;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Converts the factory-defined API kind to its persisted System-event enum.
|
|
568
|
+
*/
|
|
569
|
+
#modelKind(model) {
|
|
570
|
+
return model.definition.kind === "generation" ? AiModelKind.GENERATION : AiModelKind.DECISION;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Requires the durable identity of a named operation.
|
|
574
|
+
*/
|
|
575
|
+
#operationId(operation) {
|
|
576
|
+
if (operation.operation === undefined)
|
|
577
|
+
throw new Error("Agent named operation has no ID.");
|
|
578
|
+
return operation.operation;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Restores nonsecret connection identity and rejects drift before use.
|
|
582
|
+
*/
|
|
583
|
+
async #connectIdentity(backend, saved, operation) {
|
|
584
|
+
if (saved === undefined)
|
|
585
|
+
throw new Error("Saved Agent connection identity is absent.");
|
|
586
|
+
const deadlineEpochMs = this.#deadline(operation);
|
|
587
|
+
const control = {
|
|
588
|
+
signal: this.#signal,
|
|
589
|
+
...(deadlineEpochMs === undefined ? {} : { deadlineEpochMs }),
|
|
590
|
+
};
|
|
591
|
+
const identity = await AgentModelSelection.awaitHook(backend.resolveIdentity(this.scope, control), this.#signal, deadlineEpochMs);
|
|
592
|
+
const authorized = await AgentModelSelection.awaitHook(backend.authorizeUse(this.scope, identity, control), this.#signal, deadlineEpochMs);
|
|
593
|
+
if (!authorized || !this.#sameIdentity(identity, this.#savedIdentity(saved)))
|
|
594
|
+
throw new AgentExecutionFault("REVISION_CHANGED", "Agent model identity or authorization changed.");
|
|
595
|
+
return identity;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Copies credential-free identity fields from a saved model connection.
|
|
599
|
+
*
|
|
600
|
+
* @param saved Durable selected connection fields.
|
|
601
|
+
* @returns Identity comparable with the current model binding.
|
|
602
|
+
*/
|
|
603
|
+
#savedIdentity(saved) {
|
|
604
|
+
return {
|
|
605
|
+
provider: saved?.provider?.value ?? "",
|
|
606
|
+
account: saved?.account?.value ?? "",
|
|
607
|
+
endpoint: saved?.endpoint?.value ?? "",
|
|
608
|
+
model: saved?.model?.value ?? "",
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Compares all credential-free fields of a selected connection.
|
|
613
|
+
*/
|
|
614
|
+
#sameIdentity(left, right) {
|
|
615
|
+
return (left.provider === right.provider &&
|
|
616
|
+
left.account === right.account &&
|
|
617
|
+
left.endpoint === right.endpoint &&
|
|
618
|
+
left.model === right.model);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Reads the saved invocation deadline without extending it on restart.
|
|
622
|
+
*/
|
|
623
|
+
#invocationDeadline() {
|
|
624
|
+
const deadline = this.session.record().started?.deadline;
|
|
625
|
+
if (deadline === undefined) {
|
|
626
|
+
if (this.session.record().started?.bounds?.deadlineMillis !== undefined)
|
|
627
|
+
throw new Error("Agent invocation deadline was not saved.");
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
return Number(deadline.seconds) * 1_000 + Math.floor(deadline.nanos / 1_000_000);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Reads the persisted per-capability absolute deadline.
|
|
634
|
+
*/
|
|
635
|
+
#deadline(operation) {
|
|
636
|
+
const deadline = operation.deadline;
|
|
637
|
+
const invocationDeadline = this.#invocationDeadline();
|
|
638
|
+
if (deadline === undefined) {
|
|
639
|
+
if (operation.limits?.deadlineMs !== undefined)
|
|
640
|
+
throw new Error("Agent operation deadline was not saved.");
|
|
641
|
+
return invocationDeadline;
|
|
642
|
+
}
|
|
643
|
+
const operationDeadline = Number(deadline.seconds) * 1_000 + Math.floor(deadline.nanos / 1_000_000);
|
|
644
|
+
return invocationDeadline === undefined
|
|
645
|
+
? operationDeadline
|
|
646
|
+
: Math.min(invocationDeadline, operationDeadline);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Enumerates this handler's named calls in original order.
|
|
650
|
+
*/
|
|
651
|
+
#savedOperations(record) {
|
|
652
|
+
return record.journal.flatMap((entry) => entry.evidence.case === "operation" &&
|
|
653
|
+
entry.evidence.value.handlerOrdinal === this.handlerOrdinal
|
|
654
|
+
? [entry.evidence.value]
|
|
655
|
+
: []);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Compares exact canonical typed application input.
|
|
659
|
+
*
|
|
660
|
+
* @typeParam I Generated input descriptor.
|
|
661
|
+
*/
|
|
662
|
+
#sameInput(saved, schema, input) {
|
|
663
|
+
const packed = saved.input;
|
|
664
|
+
if (packed === undefined)
|
|
665
|
+
return false;
|
|
666
|
+
const current = AnyMessages.pack(schema, input);
|
|
667
|
+
return (packed.typeUrl === current.typeUrl &&
|
|
668
|
+
Buffer.compare(Buffer.from(packed.value), Buffer.from(current.value)) === 0);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Materializes per-capability immutable operation limits.
|
|
672
|
+
*/
|
|
673
|
+
#limits(model) {
|
|
674
|
+
const limits = model.definition.limits;
|
|
675
|
+
return create(AiOperationLimitsSchema, {
|
|
676
|
+
...(limits.modelRequests === undefined
|
|
677
|
+
? {}
|
|
678
|
+
: { modelRequests: create(RequestLimitSchema, { value: limits.modelRequests }) }),
|
|
679
|
+
...(limits.toolCalls === undefined
|
|
680
|
+
? {}
|
|
681
|
+
: { toolCalls: create(ToolLimitSchema, { value: limits.toolCalls }) }),
|
|
682
|
+
...(limits.deadlineMs === undefined
|
|
683
|
+
? {}
|
|
684
|
+
: { deadlineMs: create(AiDeadlineMillisSchema, { value: BigInt(limits.deadlineMs) }) }),
|
|
685
|
+
...(limits.maxInputBytes === undefined
|
|
686
|
+
? {}
|
|
687
|
+
: { maxInputBytes: create(InputBytesSchema, { value: BigInt(limits.maxInputBytes) }) }),
|
|
688
|
+
...(limits.maxOutputBytes === undefined
|
|
689
|
+
? {}
|
|
690
|
+
: { maxOutputBytes: create(OutputBytesSchema, { value: BigInt(limits.maxOutputBytes) }) }),
|
|
691
|
+
...(limits.maxOutputTokens === undefined
|
|
692
|
+
? {}
|
|
693
|
+
: {
|
|
694
|
+
maxOutputTokens: create(OutputTokensSchema, {
|
|
695
|
+
value: BigInt(limits.maxOutputTokens),
|
|
696
|
+
}),
|
|
697
|
+
}),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Supplies fenced physical-attempt controls for the selected backend.
|
|
702
|
+
*/
|
|
703
|
+
#control(operation, model, mcp) {
|
|
704
|
+
const deadlineEpochMs = this.#deadline(operation);
|
|
705
|
+
const received = new Map();
|
|
706
|
+
let attemptNumber = 0;
|
|
707
|
+
return {
|
|
708
|
+
signal: this.#signal,
|
|
709
|
+
...(deadlineEpochMs === undefined ? {} : { deadlineEpochMs }),
|
|
710
|
+
nowEpochMs: () => Time.currentTimeMillis(),
|
|
711
|
+
hasAuthority: () => !this.#signal.aborted &&
|
|
712
|
+
(deadlineEpochMs === undefined || Time.currentTimeMillis() < deadlineEpochMs),
|
|
713
|
+
beginAttempt: (request) => this.#beginAttempt(operation, model, request, ++attemptNumber),
|
|
714
|
+
reserveTransport: (id, bytes, credit) => this.#reserve(id, bytes, credit, model),
|
|
715
|
+
onReceived: (id, bytes) => {
|
|
716
|
+
this.#providerBytes(received, id, bytes);
|
|
717
|
+
},
|
|
718
|
+
finishAttempt: (completion) => this.#finishAttempt(operation, completion, received),
|
|
719
|
+
recordFailure: (code, retryableByNewSignal) => this.#recordFailure(operation, code, retryableByNewSignal),
|
|
720
|
+
admitGeneration: (candidate, definition, input) => parseCandidate(definition.output, candidate, (value) => definition.validation?.check(value, input) ?? []),
|
|
721
|
+
admitDecision: (result, definition, input, rounding) => admitDecision(model, result, input, rounding ?? {}),
|
|
722
|
+
callTool: (invocation) => this.#callTool(mcp, invocation),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Delegates only configured proposals to the selected MCP runtime.
|
|
727
|
+
*/
|
|
728
|
+
#callTool(mcp, invocation) {
|
|
729
|
+
if (mcp === undefined)
|
|
730
|
+
return Promise.reject(new Error("Agent capability has no configured MCP tools."));
|
|
731
|
+
return mcp.call(invocation);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Counts decoded provider bytes before returning control to its transport.
|
|
735
|
+
*/
|
|
736
|
+
#providerBytes(received, id, bytes) {
|
|
737
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0)
|
|
738
|
+
throw new Error("Agent provider receipt byte count is invalid.");
|
|
739
|
+
received.set(id, (received.get(id) ?? 0) + bytes);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Persists a safe diagnostic before exposing its identity to the adapter.
|
|
743
|
+
*/
|
|
744
|
+
async #recordFailure(operation, code, retryableByNewSignal) {
|
|
745
|
+
const failure = { code, retryableByNewSignal, diagnosticId: randomUUID() };
|
|
746
|
+
await this.session.update((record) => {
|
|
747
|
+
const saved = this.#findOperation(record, operation);
|
|
748
|
+
saved.diagnostics.push(create(AgentSavedFailureSchema, {
|
|
749
|
+
code: failureCodes[code],
|
|
750
|
+
retryableByNewSignal,
|
|
751
|
+
diagnostic: create(AiDiagnosticIdSchema, { value: failure.diagnosticId }),
|
|
752
|
+
}));
|
|
753
|
+
const limit = record.started?.bounds?.maxRecoveryBytes;
|
|
754
|
+
if (limit !== undefined && BigInt(AgentExecutionSizes.record(record)) > limit)
|
|
755
|
+
throw new Error("Agent diagnostic exceeds the invocation recovery byte bound.");
|
|
756
|
+
return record;
|
|
757
|
+
});
|
|
758
|
+
return failure;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Restores only the exact diagnostic referenced by a saved response.
|
|
762
|
+
*/
|
|
763
|
+
#savedDiagnostic(operation, id) {
|
|
764
|
+
const saved = this.#findOperation(this.session.record(), operation).diagnostics.find((item) => item.diagnostic?.value === id);
|
|
765
|
+
const code = Object.entries(failureCodes).find(([, value]) => value === saved?.code)?.[0];
|
|
766
|
+
if (saved === undefined || code === undefined)
|
|
767
|
+
throw new Error("Saved Agent model response references an unknown diagnostic.");
|
|
768
|
+
return {
|
|
769
|
+
code: code,
|
|
770
|
+
diagnosticId: id,
|
|
771
|
+
retryableByNewSignal: saved.retryableByNewSignal,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Finds the current journal image of a previously admitted operation.
|
|
776
|
+
*/
|
|
777
|
+
#findOperation(record, operation) {
|
|
778
|
+
const entry = record.journal.find((item) => item.evidence.case === "operation" &&
|
|
779
|
+
item.evidence.value.operation?.value === operation.operation?.value);
|
|
780
|
+
if (entry?.evidence.case !== "operation")
|
|
781
|
+
throw new Error("Agent named operation journal disappeared.");
|
|
782
|
+
return entry.evidence.value;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Persists one physical request before an adapter receives a new ticket.
|
|
786
|
+
*/
|
|
787
|
+
async #beginAttempt(operation, model, request, number) {
|
|
788
|
+
const prior = this.#priorAttempt(operation, number);
|
|
789
|
+
if (prior !== undefined)
|
|
790
|
+
return this.#replayAttempt(operation, request, prior);
|
|
791
|
+
const deadlineEpochMs = this.#deadline(operation);
|
|
792
|
+
this.#checkAttemptBounds(model, number, deadlineEpochMs);
|
|
793
|
+
const id = randomUUID();
|
|
794
|
+
const requestContent = request.kind === "generation"
|
|
795
|
+
? AnyMessages.pack(GenerationRequestSchema, request.content)
|
|
796
|
+
: AnyMessages.pack(DecisionRequestSchema, request.content);
|
|
797
|
+
const history = this.#conversationHistory(operation, id, requestContent);
|
|
798
|
+
await this.#saveAttempt(operation, request, number, id, history);
|
|
799
|
+
return {
|
|
800
|
+
id,
|
|
801
|
+
...(model.definition.limits.maxInputBytes === undefined
|
|
802
|
+
? {}
|
|
803
|
+
: { maxInputBytes: model.definition.limits.maxInputBytes }),
|
|
804
|
+
...(model.definition.limits.maxOutputBytes === undefined
|
|
805
|
+
? {}
|
|
806
|
+
: { maxOutputBytes: model.definition.limits.maxOutputBytes }),
|
|
807
|
+
...(deadlineEpochMs === undefined ? {} : { deadlineEpochMs }),
|
|
808
|
+
signal: this.#signal,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Checks the selected model's explicit request and deadline bounds before journaling.
|
|
813
|
+
*/
|
|
814
|
+
#checkAttemptBounds(model, number, deadlineEpochMs) {
|
|
815
|
+
if (this.#signal.aborted ||
|
|
816
|
+
(deadlineEpochMs !== undefined && Time.currentTimeMillis() >= deadlineEpochMs))
|
|
817
|
+
throw new Error("Agent model attempt deadline expired.");
|
|
818
|
+
const operationLimit = model.definition.limits.modelRequests;
|
|
819
|
+
const invocationLimit = this.session.record().started?.bounds?.modelRequests;
|
|
820
|
+
if ((operationLimit !== undefined && number > operationLimit) ||
|
|
821
|
+
(invocationLimit !== undefined && this.#attemptCount() >= Number(invocationLimit)))
|
|
822
|
+
throw new AgentExecutionFault("MODEL_BUDGET_EXCEEDED", "Agent physical model request budget is exhausted.");
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Finds the exact physical ordinal already saved for recovery.
|
|
826
|
+
*/
|
|
827
|
+
#priorAttempt(operation, number) {
|
|
828
|
+
const entry = this.session
|
|
829
|
+
.record()
|
|
830
|
+
.journal.find((item) => item.evidence.case === "attempt" &&
|
|
831
|
+
item.evidence.value.operation?.value === operation.operation?.value &&
|
|
832
|
+
item.evidence.value.physicalAttempt === BigInt(number));
|
|
833
|
+
return entry?.evidence.case === "attempt" ? entry.evidence.value : undefined;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Restores saved content and its exact diagnostic without a new ticket.
|
|
837
|
+
*/
|
|
838
|
+
#replayAttempt(operation, request, prior) {
|
|
839
|
+
if (!this.#sameRequest(prior, request))
|
|
840
|
+
throw new AgentExecutionFault("REPLAY_DIVERGENCE", "Saved Agent provider request changed on recovery.");
|
|
841
|
+
if (prior.response.case !== "generationResponse" && prior.response.case !== "decisionResponse")
|
|
842
|
+
throw new AgentUncertainAttemptError();
|
|
843
|
+
const diagnosticId = prior.response.value.diagnosticId?.value;
|
|
844
|
+
const failure = diagnosticId === undefined ? undefined : this.#savedDiagnostic(operation, diagnosticId);
|
|
845
|
+
return {
|
|
846
|
+
kind: "replay",
|
|
847
|
+
id: prior.attempt?.value ?? "",
|
|
848
|
+
response: prior.response.value,
|
|
849
|
+
issues: prior.validationIssues.map((issue) => ({
|
|
850
|
+
code: issue.code,
|
|
851
|
+
path: issue.path,
|
|
852
|
+
message: issue.message,
|
|
853
|
+
})),
|
|
854
|
+
...(failure === undefined ? {} : { failure }),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Saves an original request with its conversation row in one provider mutation.
|
|
859
|
+
*/
|
|
860
|
+
async #saveAttempt(operation, request, number, id, history) {
|
|
861
|
+
const change = this.#attemptChange(operation, request, number, id);
|
|
862
|
+
if (this.audit === undefined)
|
|
863
|
+
await this.session.update(change, [history]);
|
|
864
|
+
else
|
|
865
|
+
await this.audit.save(AttemptStartedSchema, this.#attemptStarted(operation, request, id), change, [history]);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Appends a physical request without consuming a second ticket.
|
|
869
|
+
*/
|
|
870
|
+
#attemptChange(operation, request, number, id) {
|
|
871
|
+
return (record) => {
|
|
872
|
+
record.journal.push(create(JournalEntrySchema, {
|
|
873
|
+
ordinal: BigInt(record.journal.length),
|
|
874
|
+
evidence: {
|
|
875
|
+
case: "attempt",
|
|
876
|
+
value: create(AgentAttemptEvidenceSchema, {
|
|
877
|
+
operation: operation.operation,
|
|
878
|
+
attempt: create(AiAttemptIdSchema, { value: id }),
|
|
879
|
+
physicalAttempt: BigInt(number),
|
|
880
|
+
preparedRequest: request.kind === "generation"
|
|
881
|
+
? { case: "generationRequest", value: request.content }
|
|
882
|
+
: { case: "decisionRequest", value: request.content },
|
|
883
|
+
}),
|
|
884
|
+
},
|
|
885
|
+
}));
|
|
886
|
+
return record;
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Describes the prepared physical request using its saved digest.
|
|
891
|
+
*/
|
|
892
|
+
#attemptStarted(operation, request, id) {
|
|
893
|
+
return create(AttemptStartedSchema, {
|
|
894
|
+
operation: this.audit?.reference(operation, request.kind === "generation" ? AiModelKind.GENERATION : AiModelKind.DECISION),
|
|
895
|
+
attempt: create(AiAttemptIdSchema, { value: id }),
|
|
896
|
+
startedAt: Time.currentTime(),
|
|
897
|
+
request: request.content.digest,
|
|
898
|
+
...(request.kind === "generation" && request.content.corrects !== undefined
|
|
899
|
+
? { previousAttempt: request.content.corrects }
|
|
900
|
+
: {}),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Reserves transport bytes and provider persistence capacity before dispatch.
|
|
905
|
+
*/
|
|
906
|
+
async #reserve(id, bytes, credit, model) {
|
|
907
|
+
const inputLimit = model.definition.limits.maxInputBytes;
|
|
908
|
+
const outputLimit = model.definition.limits.maxOutputBytes;
|
|
909
|
+
const configuredCredit = credit ?? outputLimit;
|
|
910
|
+
if (!Number.isSafeInteger(bytes) ||
|
|
911
|
+
bytes < 0 ||
|
|
912
|
+
(inputLimit !== undefined && bytes > inputLimit) ||
|
|
913
|
+
(credit !== undefined && (!Number.isSafeInteger(credit) || credit < 1)) ||
|
|
914
|
+
(outputLimit !== undefined && credit !== undefined && credit > outputLimit))
|
|
915
|
+
throw new Error("Agent provider request exceeds its operation byte bounds.");
|
|
916
|
+
const record = this.session.record();
|
|
917
|
+
const previous = this.#findAttempt(record, id);
|
|
918
|
+
if (previous.transportReserved)
|
|
919
|
+
throw new Error("Agent transport ticket was already reserved.");
|
|
920
|
+
const proposedCredit = this.#outputCredit(record, configuredCredit, bytes);
|
|
921
|
+
AgentMcpBudget.checkSharedBytes(record, bytes, proposedCredit ?? 0);
|
|
922
|
+
if (proposedCredit !== undefined)
|
|
923
|
+
this.#checkCapacity(record, proposedCredit, model);
|
|
924
|
+
let effectiveCredit;
|
|
925
|
+
await this.session.update((current) => {
|
|
926
|
+
effectiveCredit = this.#applyReservation(current, id, bytes, configuredCredit, model);
|
|
927
|
+
return current;
|
|
928
|
+
});
|
|
929
|
+
return effectiveCredit;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Narrows explicitly configured provider credit by remaining invocation credit.
|
|
933
|
+
*/
|
|
934
|
+
#outputCredit(record, configured, bytes) {
|
|
935
|
+
const remaining = AgentMcpBudget.remainingOutputCredit(record);
|
|
936
|
+
if (remaining !== undefined && remaining < 1n)
|
|
937
|
+
AgentMcpBudget.checkSharedBytes(record, bytes, 1);
|
|
938
|
+
const available = remaining === undefined ? undefined : Number(remaining);
|
|
939
|
+
if (available !== undefined && !Number.isSafeInteger(available))
|
|
940
|
+
throw new Error("Agent invocation output credit is not a safe byte count.");
|
|
941
|
+
return configured === undefined
|
|
942
|
+
? available
|
|
943
|
+
: available === undefined
|
|
944
|
+
? configured
|
|
945
|
+
: Math.min(configured, available);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Applies one atomic transport reservation to the fenced execution record.
|
|
949
|
+
*/
|
|
950
|
+
#applyReservation(record, id, bytes, configured, model) {
|
|
951
|
+
const evidence = this.#findAttempt(record, id);
|
|
952
|
+
if (evidence.transportReserved)
|
|
953
|
+
throw new Error("Agent transport ticket was already reserved.");
|
|
954
|
+
const credit = this.#outputCredit(record, configured, bytes);
|
|
955
|
+
AgentMcpBudget.checkSharedBytes(record, bytes, credit ?? 0);
|
|
956
|
+
evidence.inputBytes = BigInt(bytes);
|
|
957
|
+
if (credit !== undefined)
|
|
958
|
+
this.#checkCapacity(record, credit, model);
|
|
959
|
+
evidence.reservedResponseBytes = BigInt(credit ?? 0);
|
|
960
|
+
evidence.transportReserved = true;
|
|
961
|
+
return credit;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Persists a complete response before an adapter can return it to the handler.
|
|
965
|
+
*/
|
|
966
|
+
async #finishAttempt(operation, completion, received) {
|
|
967
|
+
const response = completion.response;
|
|
968
|
+
const content = response.$typeName === GenerationResponseSchema.typeName
|
|
969
|
+
? AnyMessages.pack(GenerationResponseSchema, response)
|
|
970
|
+
: AnyMessages.pack(DecisionResponseSchema, response);
|
|
971
|
+
const responseDigest = response.$typeName === GenerationResponseSchema.typeName ? response.digest : undefined;
|
|
972
|
+
const history = this.#conversationHistory(operation, completion.ticketId, content);
|
|
973
|
+
const change = (record) => {
|
|
974
|
+
this.#storeCompletion(this.#findAttempt(record, completion.ticketId), completion, received.get(completion.ticketId));
|
|
975
|
+
return record;
|
|
976
|
+
};
|
|
977
|
+
if (this.audit === undefined)
|
|
978
|
+
await this.session.update(change, [history]);
|
|
979
|
+
else
|
|
980
|
+
await this.audit.save(AttemptFinishedSchema, this.#attemptFinished(operation, completion, responseDigest), change, [history]);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Describes the persisted response and its original physical ticket.
|
|
984
|
+
*/
|
|
985
|
+
#attemptFinished(operation, completion, responseDigest) {
|
|
986
|
+
const response = completion.response;
|
|
987
|
+
return create(AttemptFinishedSchema, {
|
|
988
|
+
operation: this.audit?.reference(operation, response.$typeName === GenerationResponseSchema.typeName
|
|
989
|
+
? AiModelKind.GENERATION
|
|
990
|
+
: AiModelKind.DECISION),
|
|
991
|
+
attempt: create(AiAttemptIdSchema, { value: completion.ticketId }),
|
|
992
|
+
outcome: response.outcome,
|
|
993
|
+
...(responseDigest === undefined ? {} : { response: responseDigest }),
|
|
994
|
+
...(response.usage === undefined ? {} : { usage: response.usage }),
|
|
995
|
+
...(response.diagnosticId === undefined ? {} : { diagnosticId: response.diagnosticId }),
|
|
996
|
+
finishedAt: Time.currentTime(),
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Applies one bounded observed completion to its reserved attempt.
|
|
1001
|
+
*/
|
|
1002
|
+
#storeCompletion(evidence, completion, observedBytes) {
|
|
1003
|
+
if (!evidence.transportReserved || evidence.response.case !== undefined)
|
|
1004
|
+
throw new Error("Agent model attempt has no live reservation.");
|
|
1005
|
+
if (observedBytes !== undefined &&
|
|
1006
|
+
completion.receivedBytes !== undefined &&
|
|
1007
|
+
observedBytes !== completion.receivedBytes)
|
|
1008
|
+
throw new Error("Agent model receipt byte counts disagree.");
|
|
1009
|
+
const bytes = completion.receivedBytes;
|
|
1010
|
+
if (bytes !== undefined) {
|
|
1011
|
+
if (evidence.reservedResponseBytes !== 0n && bytes > Number(evidence.reservedResponseBytes))
|
|
1012
|
+
throw new Error("Agent model response exceeded reserved bytes.");
|
|
1013
|
+
evidence.receivedBytes = BigInt(bytes);
|
|
1014
|
+
}
|
|
1015
|
+
const response = completion.response;
|
|
1016
|
+
evidence.response =
|
|
1017
|
+
response.$typeName === GenerationResponseSchema.typeName
|
|
1018
|
+
? { case: "generationResponse", value: response }
|
|
1019
|
+
: { case: "decisionResponse", value: response };
|
|
1020
|
+
evidence.outcome = response.outcome;
|
|
1021
|
+
evidence.validationIssues = (completion.issues ?? []).map((issue) => create(AgentValidationIssueSchema, {
|
|
1022
|
+
code: issue.code,
|
|
1023
|
+
path: issue.path,
|
|
1024
|
+
message: issue.message,
|
|
1025
|
+
}));
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Links one exact typed exchange to the named operation and physical attempt.
|
|
1029
|
+
*/
|
|
1030
|
+
#conversationHistory(operation, attemptId, content) {
|
|
1031
|
+
const occurredAt = Time.currentTime();
|
|
1032
|
+
return create(AgentHistoryEntrySchema, {
|
|
1033
|
+
occurredAt,
|
|
1034
|
+
item: {
|
|
1035
|
+
case: "conversationRecord",
|
|
1036
|
+
value: create(ConversationRecordSchema, {
|
|
1037
|
+
id: create(ConversationRecordIdSchema, { value: randomUUID() }),
|
|
1038
|
+
conversation: operation.conversation,
|
|
1039
|
+
operation: operation.operation,
|
|
1040
|
+
attempt: create(AiAttemptIdSchema, { value: attemptId }),
|
|
1041
|
+
occurredAt,
|
|
1042
|
+
content,
|
|
1043
|
+
}),
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Finds one journaled attempt under its original ticket.
|
|
1049
|
+
*/
|
|
1050
|
+
#findAttempt(record, id) {
|
|
1051
|
+
const entry = record.journal.find((item) => item.evidence.case === "attempt" && item.evidence.value.attempt?.value === id);
|
|
1052
|
+
if (entry?.evidence.case !== "attempt")
|
|
1053
|
+
throw new Error("Agent model ticket is unknown.");
|
|
1054
|
+
return entry.evidence.value;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Counts physical attempts across all handlers in this invocation.
|
|
1058
|
+
*/
|
|
1059
|
+
#attemptCount() {
|
|
1060
|
+
return this.session.record().journal.filter((entry) => entry.evidence.case === "attempt")
|
|
1061
|
+
.length;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Checks complete provider wrappers with a maximally credited response.
|
|
1065
|
+
*/
|
|
1066
|
+
#checkCapacity(record, credit, model) {
|
|
1067
|
+
const recoveryLimit = record.started?.bounds?.maxRecoveryBytes;
|
|
1068
|
+
if (recoveryLimit !== undefined && BigInt(credit) > recoveryLimit)
|
|
1069
|
+
throw new Error("Agent model credit exceeds invocation recovery bytes.");
|
|
1070
|
+
const projected = this.#projectedRecord(record, 1, model);
|
|
1071
|
+
const creditBytes = BigInt(credit);
|
|
1072
|
+
// The generation envelope can add one ordered Anthropic content copy.
|
|
1073
|
+
const orderedContent = model.definition.kind === "generation" ? creditBytes + 64n : 0n;
|
|
1074
|
+
// Three response copies: raw/decoded content, typed attempt output, named result.
|
|
1075
|
+
// One more credit covers bounded usage, issues, model identity, and proposal metadata.
|
|
1076
|
+
const recordBytes = BigInt(AgentExecutionSizes.record(projected)) +
|
|
1077
|
+
3n * (creditBytes - 1n) +
|
|
1078
|
+
creditBytes +
|
|
1079
|
+
orderedContent +
|
|
1080
|
+
240n;
|
|
1081
|
+
const headBytes = this.#projectedHead(record);
|
|
1082
|
+
const history = this.#historyBytes(record, 1, model);
|
|
1083
|
+
// The response row retains raw/decoded content and typed output, plus metadata credit.
|
|
1084
|
+
const responseBytes = BigInt(history.response) + 2n * (creditBytes - 1n) + creditBytes + orderedContent + 120n;
|
|
1085
|
+
const auditBytes = this.#capacityAuditBytes(record, model);
|
|
1086
|
+
this.#checkProjectedBounds(recoveryLimit, recordBytes, headBytes, [
|
|
1087
|
+
BigInt(history.request),
|
|
1088
|
+
responseBytes,
|
|
1089
|
+
...auditBytes,
|
|
1090
|
+
]);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Applies invocation and provider byte limits to projected persistent rows.
|
|
1094
|
+
*/
|
|
1095
|
+
#checkProjectedBounds(recoveryLimit, recordBytes, headBytes, historyBytes) {
|
|
1096
|
+
const capacity = this.session.capacity();
|
|
1097
|
+
if (recoveryLimit !== undefined && recordBytes > recoveryLimit)
|
|
1098
|
+
throw new Error("Agent model evidence exceeds invocation recovery bytes.");
|
|
1099
|
+
if (capacity.executionRecordBytes !== undefined &&
|
|
1100
|
+
recordBytes > BigInt(capacity.executionRecordBytes))
|
|
1101
|
+
throw new Error("Agent execution record cannot retain the bounded model response.");
|
|
1102
|
+
if (capacity.executionHeadBytes !== undefined && headBytes > capacity.executionHeadBytes)
|
|
1103
|
+
throw new Error("Agent execution head exceeds its provider payload bound.");
|
|
1104
|
+
if (capacity.historyRecordBytes !== undefined &&
|
|
1105
|
+
historyBytes.some((bytes) => bytes > BigInt(capacity.historyRecordBytes ?? 0)))
|
|
1106
|
+
throw new Error("Agent conversation history cannot retain the bounded model response.");
|
|
1107
|
+
if (capacity.transactionPayloadBytes !== undefined &&
|
|
1108
|
+
recordBytes + BigInt(headBytes) + historyBytes.reduce((sum, bytes) => sum + bytes, 0n) >
|
|
1109
|
+
BigInt(capacity.transactionPayloadBytes))
|
|
1110
|
+
throw new Error("Agent model evidence exceeds provider transaction payload capacity.");
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Measures both original outcome audit rows with the accepted scope and actor.
|
|
1114
|
+
*/
|
|
1115
|
+
#capacityAuditBytes(record, model) {
|
|
1116
|
+
if (this.audit === undefined)
|
|
1117
|
+
return [];
|
|
1118
|
+
const operation = record.journal.findLast((entry) => entry.evidence.case === "operation");
|
|
1119
|
+
const attempt = record.journal.findLast((entry) => entry.evidence.case === "attempt");
|
|
1120
|
+
if (operation?.evidence.case !== "operation" || attempt?.evidence.case !== "attempt")
|
|
1121
|
+
throw new Error("Agent audit capacity requires saved operation and attempt.");
|
|
1122
|
+
const ref = this.audit.reference(operation.evidence.value, this.#modelKind(model));
|
|
1123
|
+
const digest = create(AiContentDigestSchema, { value: "a".repeat(64) });
|
|
1124
|
+
const finish = create(AttemptFinishedSchema, {
|
|
1125
|
+
operation: ref,
|
|
1126
|
+
attempt: attempt.evidence.value.attempt,
|
|
1127
|
+
outcome: AiOutcome.ADMITTED,
|
|
1128
|
+
response: digest,
|
|
1129
|
+
finishedAt: Time.currentTime(),
|
|
1130
|
+
});
|
|
1131
|
+
const admitted = create(ResultAdmittedSchema, {
|
|
1132
|
+
operation: ref,
|
|
1133
|
+
result: digest,
|
|
1134
|
+
validationRevision: create(AiValidationRevisionSchema, {
|
|
1135
|
+
value: model.definition.validation?.version ?? model.definition.version,
|
|
1136
|
+
}),
|
|
1137
|
+
admittedAt: Time.currentTime(),
|
|
1138
|
+
});
|
|
1139
|
+
return [
|
|
1140
|
+
BigInt(this.audit.historyBytes(AttemptFinishedSchema, finish)),
|
|
1141
|
+
BigInt(this.audit.historyBytes(ResultAdmittedSchema, admitted)),
|
|
1142
|
+
];
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Measures the full record with raw, response-Any and admitted-output copies.
|
|
1146
|
+
*/
|
|
1147
|
+
#projectedRecord(record, credit, model) {
|
|
1148
|
+
const projected = clone(AgentExecutionRecordSchema, record);
|
|
1149
|
+
const attempt = projected.journal.findLast((entry) => entry.evidence.case === "attempt");
|
|
1150
|
+
const operation = projected.journal.findLast((entry) => entry.evidence.case === "operation");
|
|
1151
|
+
if (attempt?.evidence.case !== "attempt" || operation?.evidence.case !== "operation")
|
|
1152
|
+
throw new Error("Agent capacity check requires a saved attempt and operation.");
|
|
1153
|
+
const admitted = create(AnySchema, {
|
|
1154
|
+
typeUrl: TypeUrls.derive(model.definition.output),
|
|
1155
|
+
value: new Uint8Array(credit),
|
|
1156
|
+
});
|
|
1157
|
+
attempt.evidence.value.response =
|
|
1158
|
+
model.definition.kind === "generation"
|
|
1159
|
+
? { case: "generationResponse", value: this.#creditedGeneration(credit, admitted) }
|
|
1160
|
+
: { case: "decisionResponse", value: this.#creditedDecision(credit, admitted) };
|
|
1161
|
+
operation.evidence.value.result = { case: "admittedOutput", value: admitted };
|
|
1162
|
+
return projected;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Sizes assistant text and typed output as separate credited copies.
|
|
1166
|
+
*/
|
|
1167
|
+
#creditedGeneration(credit, admitted) {
|
|
1168
|
+
return create(GenerationResponseSchema, {
|
|
1169
|
+
rawOutput: "x".repeat(credit),
|
|
1170
|
+
outcome: AiOutcome.ADMITTED,
|
|
1171
|
+
admittedOutput: admitted,
|
|
1172
|
+
digest: create(AiContentDigestSchema, { value: "a".repeat(64) }),
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Sizes decoded decision answers and typed output as distinct copies.
|
|
1177
|
+
*/
|
|
1178
|
+
#creditedDecision(credit, admitted) {
|
|
1179
|
+
return create(DecisionResponseSchema, {
|
|
1180
|
+
outcome: AiOutcome.ADMITTED,
|
|
1181
|
+
admittedOutput: admitted,
|
|
1182
|
+
answers: [
|
|
1183
|
+
create(DecisionAnswerSchema, {
|
|
1184
|
+
id: create(DecisionQuestionIdSchema, { value: "x".repeat(credit) }),
|
|
1185
|
+
kind: DecisionQuestionKind.BOOLEAN,
|
|
1186
|
+
value: { case: "booleanProbability", value: 0.5 },
|
|
1187
|
+
}),
|
|
1188
|
+
],
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Measures a conservative complete claim head including pending Inbox order.
|
|
1193
|
+
*/
|
|
1194
|
+
#projectedHead(record) {
|
|
1195
|
+
const accepted = record.accepted;
|
|
1196
|
+
if (accepted?.key?.scope === undefined)
|
|
1197
|
+
throw new Error("Agent capacity check requires the accepted instance scope.");
|
|
1198
|
+
return AgentExecutionSizes.head(create(AgentExecutionHeadSchema, {
|
|
1199
|
+
scope: accepted.key.scope,
|
|
1200
|
+
active: accepted.key,
|
|
1201
|
+
pending: accepted.key,
|
|
1202
|
+
pendingOrder: accepted.order,
|
|
1203
|
+
lastResolved: accepted.order,
|
|
1204
|
+
claimToken: record.claimToken,
|
|
1205
|
+
claimExpiresAt: record.claimExpiresAt,
|
|
1206
|
+
preferences: [...this.session.preferences()],
|
|
1207
|
+
eligibleAt: Time.currentTime(),
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Measures both complete typed conversation rows staged around transport.
|
|
1212
|
+
*/
|
|
1213
|
+
#historyBytes(record, credit, model) {
|
|
1214
|
+
const scope = record.accepted?.key?.scope;
|
|
1215
|
+
const operation = record.journal.findLast((entry) => entry.evidence.case === "operation");
|
|
1216
|
+
const attempt = record.journal.findLast((entry) => entry.evidence.case === "attempt");
|
|
1217
|
+
if (scope === undefined ||
|
|
1218
|
+
operation?.evidence.case !== "operation" ||
|
|
1219
|
+
attempt?.evidence.case !== "attempt")
|
|
1220
|
+
throw new Error("Agent conversation capacity requires saved operation and attempt.");
|
|
1221
|
+
const named = operation.evidence.value;
|
|
1222
|
+
const physical = attempt.evidence.value;
|
|
1223
|
+
const id = physical.attempt?.value;
|
|
1224
|
+
if (id === undefined)
|
|
1225
|
+
throw new Error("Agent capacity check requires an attempt ID.");
|
|
1226
|
+
return {
|
|
1227
|
+
request: AgentExecutionSizes.history(scope, this.#conversationHistory(named, id, this.#preparedContent(physical))),
|
|
1228
|
+
response: AgentExecutionSizes.history(scope, this.#conversationHistory(named, id, this.#creditedContent(model, credit))),
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Restores the exact packed prepared request for history sizing.
|
|
1233
|
+
*/
|
|
1234
|
+
#preparedContent(attempt) {
|
|
1235
|
+
const prepared = attempt.preparedRequest;
|
|
1236
|
+
if (prepared.case !== "generationRequest" && prepared.case !== "decisionRequest")
|
|
1237
|
+
throw new Error("Agent prepared request is absent from the attempt journal.");
|
|
1238
|
+
const schema = prepared.case === "generationRequest" ? GenerationRequestSchema : DecisionRequestSchema;
|
|
1239
|
+
return create(AnySchema, {
|
|
1240
|
+
typeUrl: TypeUrls.derive(schema),
|
|
1241
|
+
value: prepared.case === "generationRequest"
|
|
1242
|
+
? toBinary(GenerationRequestSchema, prepared.value)
|
|
1243
|
+
: toBinary(DecisionRequestSchema, prepared.value),
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Models the full credited response Any without dispatching externally.
|
|
1248
|
+
*/
|
|
1249
|
+
#creditedContent(model, credit) {
|
|
1250
|
+
const admitted = create(AnySchema, {
|
|
1251
|
+
typeUrl: TypeUrls.derive(model.definition.output),
|
|
1252
|
+
value: new Uint8Array(credit),
|
|
1253
|
+
});
|
|
1254
|
+
return model.definition.kind === "generation"
|
|
1255
|
+
? AnyMessages.pack(GenerationResponseSchema, this.#creditedGeneration(credit, admitted))
|
|
1256
|
+
: AnyMessages.pack(DecisionResponseSchema, this.#creditedDecision(credit, admitted));
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Compares exact prepared request content before a saved response is reused.
|
|
1260
|
+
*/
|
|
1261
|
+
#sameRequest(saved, request) {
|
|
1262
|
+
if (request.kind === "generation" && saved.preparedRequest.case === "generationRequest")
|
|
1263
|
+
return (Buffer.compare(Buffer.from(toBinary(GenerationRequestSchema, request.content)), Buffer.from(toBinary(GenerationRequestSchema, saved.preparedRequest.value))) === 0);
|
|
1264
|
+
if (request.kind === "decision" && saved.preparedRequest.case === "decisionRequest")
|
|
1265
|
+
return (Buffer.compare(Buffer.from(toBinary(DecisionRequestSchema, request.content)), Buffer.from(toBinary(DecisionRequestSchema, saved.preparedRequest.value))) === 0);
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
//# sourceMappingURL=agent-ai-runtime.js.map
|