loadout-ai 0.1.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/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { buildBenchmarkSchedule, parseBenchmarkCampaign, previewBenchmarkBudget, } from "./benchmark-campaign.js";
|
|
5
|
+
import { createBenchmarkEvidenceEvent, reduceBenchmarkEvidence, } from "./benchmark-evidence.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
function safeId(value, context) {
|
|
8
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(value) ||
|
|
9
|
+
value.includes("..") ||
|
|
10
|
+
/(?:sk-|bearer\s|api[_-]?key|token=|password=|https?:\/\/)/i.test(value))
|
|
11
|
+
throw new Error(`${context} is invalid or contains sensitive material`);
|
|
12
|
+
}
|
|
13
|
+
/** Select Docker first, then Podman. There is intentionally no host-process fallback. */
|
|
14
|
+
export async function selectLocalBenchmarkSandbox(options = {}) {
|
|
15
|
+
const probe = options.probe ??
|
|
16
|
+
(async (binary, args) => {
|
|
17
|
+
try {
|
|
18
|
+
await execFileAsync(binary, args, {
|
|
19
|
+
timeout: 2_000,
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
env: { PATH: process.env.PATH ?? "" },
|
|
22
|
+
maxBuffer: 64 * 1024,
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
for (const binary of ["docker", "podman"]) {
|
|
31
|
+
if (await probe(binary, ["version", "--format", "{{.Server.Version}}"]))
|
|
32
|
+
return { backend: binary, binary, reasonCode: "available" };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
backend: "unavailable",
|
|
36
|
+
reasonCode: "no-supported-container-runtime",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function timeoutSignal(parent, timeoutMs) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const abort = () => controller.abort(parent?.reason);
|
|
42
|
+
parent?.addEventListener("abort", abort, { once: true });
|
|
43
|
+
const timer = setTimeout(() => controller.abort(new Error("benchmark-timeout")), timeoutMs);
|
|
44
|
+
timer.unref();
|
|
45
|
+
return {
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
dispose() {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
parent?.removeEventListener("abort", abort);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function withinTimeout(operation, parent, timeoutMs) {
|
|
54
|
+
const bounded = timeoutSignal(parent, timeoutMs);
|
|
55
|
+
let listener;
|
|
56
|
+
try {
|
|
57
|
+
return await Promise.race([
|
|
58
|
+
operation(bounded.signal),
|
|
59
|
+
new Promise((_resolve, reject) => {
|
|
60
|
+
listener = () => reject(new Error("benchmark-timeout-or-abort"));
|
|
61
|
+
bounded.signal.addEventListener("abort", listener, { once: true });
|
|
62
|
+
}),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (listener)
|
|
67
|
+
bounded.signal.removeEventListener("abort", listener);
|
|
68
|
+
bounded.dispose();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function validateProviderResult(result, campaign) {
|
|
72
|
+
if (result.outcome !== "succeeded" && result.outcome !== "failed")
|
|
73
|
+
throw new Error("provider-invalid-result");
|
|
74
|
+
if (result.outcome === "succeeded") {
|
|
75
|
+
if (typeof result.output !== "string" ||
|
|
76
|
+
Buffer.byteLength(result.output, "utf8") >
|
|
77
|
+
Math.max(1_024, campaign.sampling.maxOutputTokensPerRequest * 16))
|
|
78
|
+
throw new Error("provider-invalid-result");
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
safeId(result.failureCode, "Benchmark provider failure code");
|
|
82
|
+
const values = [
|
|
83
|
+
[result.inputTokens, campaign.sampling.maxInputTokensPerRequest],
|
|
84
|
+
[result.outputTokens, campaign.sampling.maxOutputTokensPerRequest],
|
|
85
|
+
];
|
|
86
|
+
if (values.some(([value, cap]) => !Number.isInteger(value) || value < 0 || value > cap) ||
|
|
87
|
+
!Number.isFinite(result.reportedCostUsd) ||
|
|
88
|
+
result.reportedCostUsd < 0 ||
|
|
89
|
+
result.reportedCostUsd > campaign.budget.maxCostUsd)
|
|
90
|
+
throw new Error("provider-usage-out-of-bounds");
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function remainingCanReserveAttempt(state, campaign) {
|
|
94
|
+
const worstCost = (campaign.sampling.maxInputTokensPerRequest / 1_000_000) *
|
|
95
|
+
campaign.budget.inputUsdPerMillionTokens +
|
|
96
|
+
(campaign.sampling.maxOutputTokensPerRequest / 1_000_000) *
|
|
97
|
+
campaign.budget.outputUsdPerMillionTokens;
|
|
98
|
+
return (state.attempts + 1 <= campaign.budget.maxRequests &&
|
|
99
|
+
state.inputTokens + campaign.sampling.maxInputTokensPerRequest <=
|
|
100
|
+
campaign.budget.maxInputTokens &&
|
|
101
|
+
state.outputTokens + campaign.sampling.maxOutputTokensPerRequest <=
|
|
102
|
+
campaign.budget.maxOutputTokens &&
|
|
103
|
+
state.reportedCostUsd + worstCost <=
|
|
104
|
+
campaign.budget.maxCostUsd + Number.EPSILON);
|
|
105
|
+
}
|
|
106
|
+
function completionForFailure(requestId, attempts, usage, failureCode) {
|
|
107
|
+
return {
|
|
108
|
+
requestId,
|
|
109
|
+
outcome: "exhausted",
|
|
110
|
+
attempts,
|
|
111
|
+
...usage,
|
|
112
|
+
failureCode,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Execute a deterministic paired campaign through injected boundaries.
|
|
117
|
+
* This module contains no SDK, endpoint, credential, or default provider and
|
|
118
|
+
* therefore cannot make a real provider call unless the caller supplies one.
|
|
119
|
+
*/
|
|
120
|
+
export async function runPairedBenchmark(options) {
|
|
121
|
+
const campaign = parseBenchmarkCampaign(options.campaign);
|
|
122
|
+
safeId(options.runId, "Benchmark run id");
|
|
123
|
+
if (!options.approveSpend)
|
|
124
|
+
throw new Error("Benchmark execution requires explicit spend approval");
|
|
125
|
+
if (!options.provider)
|
|
126
|
+
throw new Error("Benchmark execution requires an injected provider adapter");
|
|
127
|
+
if (!options.executor)
|
|
128
|
+
throw new Error("Benchmark execution requires an injected isolation executor");
|
|
129
|
+
safeId(options.provider.id, "Benchmark provider id");
|
|
130
|
+
if (options.provider.id !== campaign.model.provider)
|
|
131
|
+
throw new Error("Benchmark provider does not match the immutable campaign");
|
|
132
|
+
const preview = previewBenchmarkBudget(campaign);
|
|
133
|
+
if (!preview.withinBudget)
|
|
134
|
+
throw new Error(`Benchmark campaign exceeds declared budget: ${preview.blockers.join("; ")}`);
|
|
135
|
+
const maximumPossibleDuration = campaign.trials.pairs *
|
|
136
|
+
2 *
|
|
137
|
+
(campaign.trials.maxRetriesPerRequest + 1) *
|
|
138
|
+
campaign.trials.timeoutMsPerRequest;
|
|
139
|
+
const maxTotalDurationMs = options.maxTotalDurationMs ?? maximumPossibleDuration;
|
|
140
|
+
if (!Number.isInteger(maxTotalDurationMs) ||
|
|
141
|
+
maxTotalDurationMs < campaign.trials.timeoutMsPerRequest ||
|
|
142
|
+
maxTotalDurationMs > maximumPossibleDuration)
|
|
143
|
+
throw new Error("Benchmark total-time ceiling is invalid");
|
|
144
|
+
let events = [...(options.existingEvents ?? [])];
|
|
145
|
+
let state = reduceBenchmarkEvidence(events, campaign);
|
|
146
|
+
if (state.inFlight)
|
|
147
|
+
throw new Error("Benchmark has an interrupted in-flight request; append an explicit recovery before resuming");
|
|
148
|
+
if (state.status === "completed" || state.status === "cancelled")
|
|
149
|
+
return { state, emitted: [] };
|
|
150
|
+
const emitted = [];
|
|
151
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
152
|
+
const emit = async (payload) => {
|
|
153
|
+
const event = createBenchmarkEvidenceEvent(campaign, options.runId, events, payload, now());
|
|
154
|
+
await options.onEvent?.(event);
|
|
155
|
+
events = [...events, event];
|
|
156
|
+
emitted.push(event);
|
|
157
|
+
state = reduceBenchmarkEvidence(events, campaign);
|
|
158
|
+
};
|
|
159
|
+
if (state.status === "empty")
|
|
160
|
+
await emit({
|
|
161
|
+
type: "run-started",
|
|
162
|
+
providerId: options.provider.id,
|
|
163
|
+
sandboxBackend: options.executor.backend,
|
|
164
|
+
spendApproved: true,
|
|
165
|
+
});
|
|
166
|
+
const byRole = new Map(campaign.candidates.map((candidate) => [candidate.role, candidate]));
|
|
167
|
+
const schedule = buildBenchmarkSchedule(campaign);
|
|
168
|
+
const startedAt = Date.now();
|
|
169
|
+
for (const request of schedule.filter((item) => state.pending.some((pending) => pending.requestId === item.requestId))) {
|
|
170
|
+
if (options.signal?.aborted) {
|
|
171
|
+
await emit({ type: "run-paused", reasonCode: "caller-aborted" });
|
|
172
|
+
return { state, emitted };
|
|
173
|
+
}
|
|
174
|
+
if (state.durationMs + Date.now() - startedAt >= maxTotalDurationMs) {
|
|
175
|
+
await emit({ type: "run-paused", reasonCode: "overall-time-ceiling" });
|
|
176
|
+
return { state, emitted };
|
|
177
|
+
}
|
|
178
|
+
let durationMs = state.currentRequestUsage.durationMs;
|
|
179
|
+
let inputTokens = state.currentRequestUsage.inputTokens;
|
|
180
|
+
let outputTokens = state.currentRequestUsage.outputTokens;
|
|
181
|
+
let reportedCostUsd = state.currentRequestUsage.reportedCostUsd;
|
|
182
|
+
const priorAttempts = events.filter((event) => event.payload.type === "request-started" &&
|
|
183
|
+
event.payload.requestId === request.requestId).length;
|
|
184
|
+
for (let attempt = priorAttempts + 1; attempt <= campaign.trials.maxRetriesPerRequest + 1; attempt++) {
|
|
185
|
+
if (!remainingCanReserveAttempt(state, campaign)) {
|
|
186
|
+
await emit({ type: "run-paused", reasonCode: "budget-ceiling" });
|
|
187
|
+
return { state, emitted };
|
|
188
|
+
}
|
|
189
|
+
await emit({
|
|
190
|
+
type: "request-started",
|
|
191
|
+
requestId: request.requestId,
|
|
192
|
+
pairIndex: request.pairIndex,
|
|
193
|
+
position: request.position,
|
|
194
|
+
attempt,
|
|
195
|
+
});
|
|
196
|
+
const attemptStarted = Date.now();
|
|
197
|
+
let prepared;
|
|
198
|
+
let teardownFailed = false;
|
|
199
|
+
let providerInvoked = false;
|
|
200
|
+
let providerResult;
|
|
201
|
+
let knownFailureCode = "executor-failed-or-timed-out";
|
|
202
|
+
try {
|
|
203
|
+
const candidate = byRole.get(request.role);
|
|
204
|
+
prepared = await withinTimeout((signal) => options.executor.prepare(request, candidate, signal), options.signal, campaign.trials.timeoutMsPerRequest);
|
|
205
|
+
if (!prepared ||
|
|
206
|
+
typeof prepared.input !== "string" ||
|
|
207
|
+
typeof prepared.teardown !== "function")
|
|
208
|
+
throw new Error("executor-invalid-result");
|
|
209
|
+
if (Buffer.byteLength(prepared.input, "utf8") >
|
|
210
|
+
Math.max(1_024, campaign.sampling.maxInputTokensPerRequest * 16))
|
|
211
|
+
throw new Error("executor-input-out-of-bounds");
|
|
212
|
+
providerInvoked = true;
|
|
213
|
+
providerResult = validateProviderResult(await withinTimeout((signal) => options.provider.invoke({
|
|
214
|
+
provider: campaign.model.provider,
|
|
215
|
+
model: campaign.model.model,
|
|
216
|
+
modelVersion: campaign.model.version,
|
|
217
|
+
input: prepared.input,
|
|
218
|
+
temperature: campaign.sampling.temperature,
|
|
219
|
+
topP: campaign.sampling.topP,
|
|
220
|
+
maxInputTokens: campaign.sampling.maxInputTokensPerRequest,
|
|
221
|
+
maxOutputTokens: campaign.sampling.maxOutputTokensPerRequest,
|
|
222
|
+
signal,
|
|
223
|
+
}), options.signal, campaign.trials.timeoutMsPerRequest), campaign);
|
|
224
|
+
inputTokens += providerResult.inputTokens;
|
|
225
|
+
outputTokens += providerResult.outputTokens;
|
|
226
|
+
reportedCostUsd += providerResult.reportedCostUsd;
|
|
227
|
+
if (providerResult.outcome === "failed")
|
|
228
|
+
knownFailureCode = providerResult.failureCode;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Once a provider invocation starts, an exception or timeout can hide
|
|
232
|
+
// billable usage. Never retry that request automatically.
|
|
233
|
+
if (providerInvoked)
|
|
234
|
+
knownFailureCode = "provider-state-unknown";
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
durationMs += Math.min(Date.now() - attemptStarted, campaign.trials.timeoutMsPerRequest);
|
|
238
|
+
if (prepared) {
|
|
239
|
+
try {
|
|
240
|
+
await withinTimeout(() => prepared.teardown(), undefined, Math.min(5_000, campaign.trials.timeoutMsPerRequest));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
teardownFailed = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (teardownFailed || (providerInvoked && !providerResult)) {
|
|
248
|
+
await emit({
|
|
249
|
+
type: "run-paused",
|
|
250
|
+
reasonCode: teardownFailed
|
|
251
|
+
? "teardown-failed"
|
|
252
|
+
: "interrupted-attempt",
|
|
253
|
+
});
|
|
254
|
+
return { state, emitted };
|
|
255
|
+
}
|
|
256
|
+
if (options.signal?.aborted) {
|
|
257
|
+
await emit({ type: "run-paused", reasonCode: "caller-aborted" });
|
|
258
|
+
return { state, emitted };
|
|
259
|
+
}
|
|
260
|
+
if (providerResult?.outcome === "succeeded") {
|
|
261
|
+
await emit({
|
|
262
|
+
type: "request-completed",
|
|
263
|
+
completion: {
|
|
264
|
+
requestId: request.requestId,
|
|
265
|
+
outcome: "succeeded",
|
|
266
|
+
attempts: attempt,
|
|
267
|
+
inputTokens,
|
|
268
|
+
outputTokens,
|
|
269
|
+
durationMs,
|
|
270
|
+
reportedCostUsd,
|
|
271
|
+
outputSha256: createHash("sha256")
|
|
272
|
+
.update(providerResult.output)
|
|
273
|
+
.digest("hex"),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (attempt === campaign.trials.maxRetriesPerRequest + 1) {
|
|
279
|
+
await emit({
|
|
280
|
+
type: "request-completed",
|
|
281
|
+
completion: completionForFailure(request.requestId, attempt, { inputTokens, outputTokens, durationMs, reportedCostUsd }, knownFailureCode),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const prior = state.currentRequestUsage;
|
|
286
|
+
await emit({
|
|
287
|
+
type: "request-attempt-failed",
|
|
288
|
+
requestId: request.requestId,
|
|
289
|
+
attempt,
|
|
290
|
+
inputTokens: inputTokens - prior.inputTokens,
|
|
291
|
+
outputTokens: outputTokens - prior.outputTokens,
|
|
292
|
+
durationMs: durationMs - prior.durationMs,
|
|
293
|
+
reportedCostUsd: reportedCostUsd - prior.reportedCostUsd,
|
|
294
|
+
failureCode: knownFailureCode,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
await emit({ type: "run-completed" });
|
|
300
|
+
return { state, emitted };
|
|
301
|
+
}
|