stageflow 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/agents/authorChat.d.ts +4 -0
- package/dist/agents/authorChat.js +113 -12
- package/dist/agents/authorSessionStore.d.ts +35 -0
- package/dist/agents/authorSessionStore.js +152 -0
- package/dist/agents/chatTurn.d.ts +2 -0
- package/dist/agents/chatTurn.js +17 -1
- package/dist/agents/errors.d.ts +1 -1
- package/dist/agents/errors.js +1 -0
- package/dist/agents/evalGate.d.ts +61 -0
- package/dist/agents/evalGate.js +103 -0
- package/dist/agents/evalSession.d.ts +26 -0
- package/dist/agents/evalSession.js +29 -0
- package/dist/agents/hostContext.d.ts +3 -0
- package/dist/agents/hostContext.js +4 -1
- package/dist/agents/runtime/piRuntime.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.js +4 -4
- package/dist/agents/runtime/types.d.ts +3 -1
- package/dist/agents/service.d.ts +27 -2
- package/dist/agents/service.js +88 -13
- package/dist/agents/sessions.d.ts +8 -0
- package/dist/agents/sessions.js +50 -10
- package/dist/agents/store.d.ts +14 -3
- package/dist/agents/store.js +45 -7
- package/dist/agents/toolRisk.d.ts +21 -0
- package/dist/agents/toolRisk.js +50 -0
- package/dist/cli/doctorCommand.js +26 -0
- package/dist/cli/evalCommand.d.ts +20 -0
- package/dist/cli/evalCommand.js +720 -0
- package/dist/cli/exportAllCommand.js +1 -1
- package/dist/cli/hostClient.d.ts +60 -0
- package/dist/cli/hostClient.js +71 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.js +10 -0
- package/dist/config/atomicWrite.d.ts +15 -0
- package/dist/config/atomicWrite.js +43 -0
- package/dist/config/browseCatalog.d.ts +2 -0
- package/dist/config/browseCatalog.js +4 -2
- package/dist/config/csv.d.ts +9 -0
- package/dist/config/csv.js +81 -0
- package/dist/config/datasetAgentCheck.d.ts +18 -0
- package/dist/config/datasetAgentCheck.js +109 -0
- package/dist/config/datasetCandidates.d.ts +22 -0
- package/dist/config/datasetCandidates.js +86 -0
- package/dist/config/datasetFromSessions.d.ts +34 -0
- package/dist/config/datasetFromSessions.js +105 -0
- package/dist/config/datasetHash.d.ts +1 -0
- package/dist/config/datasetHash.js +4 -0
- package/dist/config/datasetImport.d.ts +21 -0
- package/dist/config/datasetImport.js +120 -0
- package/dist/config/datasetStageCheck.d.ts +9 -0
- package/dist/config/datasetStageCheck.js +17 -0
- package/dist/config/datasetTypes.d.ts +27 -0
- package/dist/config/datasetTypes.js +1 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
- package/dist/config/ensureCatalogScanRoot.js +2 -6
- package/dist/config/evalCatalog.d.ts +22 -0
- package/dist/config/evalCatalog.js +67 -0
- package/dist/config/evalWriteRoots.d.ts +17 -0
- package/dist/config/evalWriteRoots.js +109 -0
- package/dist/config/hostConfig.js +1 -0
- package/dist/config/installEvalSample.d.ts +34 -0
- package/dist/config/installEvalSample.js +182 -0
- package/dist/config/loadDataset.d.ts +16 -0
- package/dist/config/loadDataset.js +318 -0
- package/dist/config/loadEval.d.ts +32 -0
- package/dist/config/loadEval.js +1085 -0
- package/dist/config/loadOutcome.d.ts +5 -0
- package/dist/config/loadStageflowManifest.js +64 -0
- package/dist/config/resolveAgentTarget.d.ts +32 -0
- package/dist/config/resolveAgentTarget.js +118 -0
- package/dist/config/resolveEvalTarget.d.ts +21 -0
- package/dist/config/resolveEvalTarget.js +89 -0
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +17 -5
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +9 -1
- package/dist/config/validateEval.d.ts +36 -0
- package/dist/config/validateEval.js +405 -0
- package/dist/config/writeDataset.d.ts +54 -0
- package/dist/config/writeDataset.js +186 -0
- package/dist/config/writeEval.d.ts +25 -0
- package/dist/config/writeEval.js +398 -0
- package/dist/diagnostics/checks.js +1 -1
- package/dist/evals/agentDiff.d.ts +13 -0
- package/dist/evals/agentDiff.js +11 -0
- package/dist/evals/canonical.d.ts +3 -0
- package/dist/evals/canonical.js +24 -0
- package/dist/evals/caseTask.d.ts +3 -0
- package/dist/evals/caseTask.js +9 -0
- package/dist/evals/compare.d.ts +94 -0
- package/dist/evals/compare.js +201 -0
- package/dist/evals/constants.d.ts +60 -0
- package/dist/evals/constants.js +95 -0
- package/dist/evals/engine/FakeEngine.d.ts +33 -0
- package/dist/evals/engine/FakeEngine.js +176 -0
- package/dist/evals/engine/index.d.ts +9 -0
- package/dist/evals/engine/index.js +19 -0
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +52 -0
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +311 -0
- package/dist/evals/engine/promptfoo/errors.d.ts +5 -0
- package/dist/evals/engine/promptfoo/errors.js +8 -0
- package/dist/evals/engine/promptfoo/generate.d.ts +30 -0
- package/dist/evals/engine/promptfoo/generate.js +342 -0
- package/dist/evals/engine/promptfoo/guard.d.ts +10 -0
- package/dist/evals/engine/promptfoo/guard.js +116 -0
- package/dist/evals/engine/promptfoo/install.d.ts +17 -0
- package/dist/evals/engine/promptfoo/install.js +73 -0
- package/dist/evals/engine/promptfoo/judgeProviders.d.ts +2 -0
- package/dist/evals/engine/promptfoo/judgeProviders.js +9 -0
- package/dist/evals/engine/promptfoo/judgeServices.d.ts +6 -0
- package/dist/evals/engine/promptfoo/judgeServices.js +41 -0
- package/dist/evals/engine/promptfoo/locate.d.ts +21 -0
- package/dist/evals/engine/promptfoo/locate.js +68 -0
- package/dist/evals/engine/promptfoo/nodeVersion.d.ts +7 -0
- package/dist/evals/engine/promptfoo/nodeVersion.js +14 -0
- package/dist/evals/engine/promptfoo/normalize.d.ts +66 -0
- package/dist/evals/engine/promptfoo/normalize.js +261 -0
- package/dist/evals/engine/promptfoo/pinned.d.ts +5 -0
- package/dist/evals/engine/promptfoo/pinned.js +5 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +14 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.js +103 -0
- package/dist/evals/engine/promptfoo/runTokens.d.ts +18 -0
- package/dist/evals/engine/promptfoo/runTokens.js +31 -0
- package/dist/evals/engine/promptfoo/status.d.ts +13 -0
- package/dist/evals/engine/promptfoo/status.js +16 -0
- package/dist/evals/estimate.d.ts +56 -0
- package/dist/evals/estimate.js +66 -0
- package/dist/evals/graders/codeGrader.d.ts +6 -0
- package/dist/evals/graders/codeGrader.js +85 -0
- package/dist/evals/graders/conversationCheckGrader.d.ts +6 -0
- package/dist/evals/graders/conversationCheckGrader.js +62 -0
- package/dist/evals/graders/fieldGrader.d.ts +11 -0
- package/dist/evals/graders/fieldGrader.js +117 -0
- package/dist/evals/graders/goal.d.ts +3 -0
- package/dist/evals/graders/goal.js +14 -0
- package/dist/evals/graders/index.d.ts +18 -0
- package/dist/evals/graders/index.js +34 -0
- package/dist/evals/graders/notApplicable.d.ts +5 -0
- package/dist/evals/graders/notApplicable.js +9 -0
- package/dist/evals/graders/path.d.ts +26 -0
- package/dist/evals/graders/path.js +75 -0
- package/dist/evals/graders/referenceGrader.d.ts +5 -0
- package/dist/evals/graders/referenceGrader.js +59 -0
- package/dist/evals/graders/schemaGrader.d.ts +5 -0
- package/dist/evals/graders/schemaGrader.js +29 -0
- package/dist/evals/graders/subject.d.ts +6 -0
- package/dist/evals/graders/subject.js +33 -0
- package/dist/evals/graders/toolCallsGrader.d.ts +6 -0
- package/dist/evals/graders/toolCallsGrader.js +100 -0
- package/dist/evals/graders/transcript.d.ts +12 -0
- package/dist/evals/graders/transcript.js +57 -0
- package/dist/evals/graders/types.d.ts +20 -0
- package/dist/evals/graders/types.js +1 -0
- package/dist/evals/judgeProviders.d.ts +16 -0
- package/dist/evals/judgeProviders.js +27 -0
- package/dist/evals/lineDiff.d.ts +5 -0
- package/dist/evals/lineDiff.js +34 -0
- package/dist/evals/listStatus.d.ts +31 -0
- package/dist/evals/listStatus.js +48 -0
- package/dist/evals/manager.d.ts +174 -0
- package/dist/evals/manager.js +787 -0
- package/dist/evals/plan.d.ts +58 -0
- package/dist/evals/plan.js +602 -0
- package/dist/evals/providerKeys.d.ts +1 -0
- package/dist/evals/providerKeys.js +21 -0
- package/dist/evals/read/agentTargets.d.ts +37 -0
- package/dist/evals/read/agentTargets.js +101 -0
- package/dist/evals/read/api.d.ts +196 -0
- package/dist/evals/read/api.js +358 -0
- package/dist/evals/read/compare.d.ts +4 -0
- package/dist/evals/read/compare.js +39 -0
- package/dist/evals/read/datasets.d.ts +105 -0
- package/dist/evals/read/datasets.js +318 -0
- package/dist/evals/read/list.d.ts +66 -0
- package/dist/evals/read/list.js +173 -0
- package/dist/evals/read/results.d.ts +46 -0
- package/dist/evals/read/results.js +107 -0
- package/dist/evals/read/runs.d.ts +149 -0
- package/dist/evals/read/runs.js +289 -0
- package/dist/evals/read/suggestions.d.ts +43 -0
- package/dist/evals/read/suggestions.js +73 -0
- package/dist/evals/runner/FakeAgentBackend.d.ts +40 -0
- package/dist/evals/runner/FakeAgentBackend.js +82 -0
- package/dist/evals/runner/agentCaseRunner.d.ts +24 -0
- package/dist/evals/runner/agentCaseRunner.js +388 -0
- package/dist/evals/runner/caseRunner.d.ts +7 -0
- package/dist/evals/runner/caseRunner.js +17 -0
- package/dist/evals/runner/conversationSubject.d.ts +18 -0
- package/dist/evals/runner/conversationSubject.js +123 -0
- package/dist/evals/runner/simulatorModel.d.ts +51 -0
- package/dist/evals/runner/simulatorModel.js +58 -0
- package/dist/evals/runner/simulatorPrompt.d.ts +29 -0
- package/dist/evals/runner/simulatorPrompt.js +59 -0
- package/dist/evals/runner/stageCaseRunner.d.ts +30 -0
- package/dist/evals/runner/stageCaseRunner.js +279 -0
- package/dist/evals/runner/userDriver.d.ts +71 -0
- package/dist/evals/runner/userDriver.js +104 -0
- package/dist/evals/scoring.d.ts +34 -0
- package/dist/evals/scoring.js +67 -0
- package/dist/evals/snapshot.d.ts +183 -0
- package/dist/evals/snapshot.js +65 -0
- package/dist/evals/stageDiff.d.ts +8 -0
- package/dist/evals/stageDiff.js +48 -0
- package/dist/evals/stats.d.ts +95 -0
- package/dist/evals/stats.js +220 -0
- package/dist/evals/statsAdapter.d.ts +4 -0
- package/dist/evals/statsAdapter.js +30 -0
- package/dist/evals/statsTypes.d.ts +51 -0
- package/dist/evals/statsTypes.js +18 -0
- package/dist/evals/storeError.d.ts +5 -0
- package/dist/evals/storeError.js +8 -0
- package/dist/evals/storeTypes.d.ts +207 -0
- package/dist/evals/storeTypes.js +1 -0
- package/dist/evals/tTable.d.ts +3 -0
- package/dist/evals/tTable.js +15 -0
- package/dist/evals/triage/constants.d.ts +26 -0
- package/dist/evals/triage/constants.js +33 -0
- package/dist/evals/triage/credentials.d.ts +13 -0
- package/dist/evals/triage/credentials.js +20 -0
- package/dist/evals/triage/jevClassifier.d.ts +14 -0
- package/dist/evals/triage/jevClassifier.js +124 -0
- package/dist/evals/triage/judgeClassifier.d.ts +10 -0
- package/dist/evals/triage/judgeClassifier.js +76 -0
- package/dist/evals/triage/prompts.d.ts +16 -0
- package/dist/evals/triage/prompts.js +64 -0
- package/dist/evals/triage/service.d.ts +79 -0
- package/dist/evals/triage/service.js +217 -0
- package/dist/evals/triage/stateBuilder.d.ts +19 -0
- package/dist/evals/triage/stateBuilder.js +125 -0
- package/dist/evals/triage/types.d.ts +60 -0
- package/dist/evals/triage/types.js +8 -0
- package/dist/evals/types.d.ts +347 -0
- package/dist/evals/types.js +1 -0
- package/dist/mcp/deps.d.ts +7 -0
- package/dist/mcp/evalTools.d.ts +3 -0
- package/dist/mcp/evalTools.js +280 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/models/response.d.ts +7 -2
- package/dist/models/response.js +6 -1
- package/dist/operatorAgent/agentAuthorContext.d.ts +1 -1
- package/dist/operatorAgent/agentAuthorContext.js +1 -1
- package/dist/operatorAgent/piModel.js +17 -4
- package/dist/operatorAgent/profiles/agentAuthor.d.ts +3 -1
- package/dist/operatorAgent/profiles/agentAuthor.js +145 -20
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +48 -15
- package/dist/operatorAgent/profiles/authorReadable.d.ts +2 -0
- package/dist/operatorAgent/profiles/authorReadable.js +26 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.d.ts +43 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +99 -5
- package/dist/runstore/sqlite/evalStore.d.ts +20 -0
- package/dist/runstore/sqlite/evalStore.js +541 -0
- package/dist/runstore/sqlite/migrations/015-evals.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/015-evals.js +157 -0
- package/dist/runstore/sqlite/migrations/016-eval-subject-ref.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/016-eval-subject-ref.js +20 -0
- package/dist/runstore/sqlite/migrations/017-eval-result-suggestions.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/017-eval-result-suggestions.js +36 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runstore/sqlite/suggestionStore.d.ts +14 -0
- package/dist/runstore/sqlite/suggestionStore.js +132 -0
- package/dist/runtime/pipelineRunner.d.ts +2 -1
- package/dist/runtime/pipelineRunner.js +2 -0
- package/dist/runtime/runManager.d.ts +2 -1
- package/dist/runtime/runManager.js +8 -6
- package/dist/runtime/runRetentionSweep.js +2 -2
- package/dist/server/agentAuthorRoutes.js +50 -1
- package/dist/server/bootstrap.d.ts +8 -0
- package/dist/server/bootstrap.js +56 -0
- package/dist/server/controlToken.js +3 -0
- package/dist/server/evalInternalRoutes.d.ts +14 -0
- package/dist/server/evalInternalRoutes.js +87 -0
- package/dist/server/evalRouteContext.d.ts +46 -0
- package/dist/server/evalRouteContext.js +43 -0
- package/dist/server/evalRoutes.d.ts +5 -0
- package/dist/server/evalRoutes.js +1427 -0
- package/dist/server/evalTriageRoutes.d.ts +2 -0
- package/dist/server/evalTriageRoutes.js +95 -0
- package/dist/server/http.d.ts +5 -0
- package/dist/server/http.js +32 -2
- package/dist/server/mcpHost.js +3 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +15 -0
- package/dist/toolkit/profiles.js +1 -1
- package/dist/types/stageflowManifest.d.ts +6 -0
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +9 -0
- package/dist/ui/assets/{MarkdownText-D7tYaIFO.js → MarkdownText-BTrOqu5J.js} +1 -1
- package/dist/ui/assets/chat-Df_wCpak.js +5 -0
- package/dist/ui/assets/chat-P7KQznHm.css +2 -0
- package/dist/ui/assets/{main-BFo79krC.css → main-CHlmTNJe.css} +1 -1
- package/dist/ui/assets/main-CvVfxmMd.js +271 -0
- package/dist/ui/chat.html +3 -3
- package/dist/ui/index.html +3 -3
- package/examples/README.md +2 -0
- package/examples/document-policy-review/README.md +35 -0
- package/examples/document-policy-review/agents/compliance.agent.yaml +42 -0
- package/examples/document-policy-review/documents/acme-vendor-sow.md +15 -0
- package/examples/document-policy-review/documents/support-runbook.md +13 -0
- package/examples/document-policy-review/policies/acceptable-use.md +13 -0
- package/examples/document-policy-review/policies/data-handling.md +17 -0
- package/examples/document-policy-review/policies/vendor-security.md +17 -0
- package/examples/evals/README.md +123 -0
- package/examples/evals/critique-sample.dataset.jsonl +8 -0
- package/examples/evals/critique-sample.eval.yaml +34 -0
- package/examples/evals/critique-sample.task.yaml +4 -0
- package/examples/evals/eval-sample.pipeline.yaml +35 -0
- package/examples/evals/support-sample.agent.yaml +20 -0
- package/examples/evals/support-sample.dataset.jsonl +6 -0
- package/examples/evals/support-sample.eval.yaml +38 -0
- package/examples/stageflow.yaml +6 -0
- package/package.json +1 -1
- package/skills/stageflow/SKILL.md +5 -2
- package/skills/stageflow/references/control-surface.md +4 -2
- package/skills/stageflow/references/evals.md +68 -0
- package/skills/stageflow-author/SKILL.md +4 -0
- package/skills/stageflow-run/SKILL.md +4 -0
- package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +0 -9
- package/dist/ui/assets/chat-BbLukq9W.js +0 -5
- package/dist/ui/assets/chat-zIZU_bP1.css +0 -2
- package/dist/ui/assets/main-CijYMeM0.js +0 -259
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { isAgentError } from "../../agents/errors.js";
|
|
2
|
+
import { createEvalGate } from "../../agents/evalGate.js";
|
|
3
|
+
import { EVAL_MAX_CONSECUTIVE_PROVIDER_FAILURES } from "../constants.js";
|
|
4
|
+
import { buildConversationSubject } from "./conversationSubject.js";
|
|
5
|
+
import { createBackendSimulatorModel, SimulatorError, } from "./simulatorModel.js";
|
|
6
|
+
import { createDriver, DriverScenarioError, } from "./userDriver.js";
|
|
7
|
+
export const EVAL_CASE_WALL_CLOCK_GRACE_MS = 60_000;
|
|
8
|
+
export const EVAL_TURN_STOP_GRACE_MS = 10_000;
|
|
9
|
+
const SESSION_OVERHEAD_MINUTES = 5;
|
|
10
|
+
function failure(code, message, extra = {}) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
code,
|
|
14
|
+
message,
|
|
15
|
+
...(extra.subjectRef !== undefined ? { subjectRef: extra.subjectRef } : {}),
|
|
16
|
+
...(extra.output !== undefined ? { output: extra.output } : {}),
|
|
17
|
+
...(extra.latencyMs !== undefined ? { latencyMs: extra.latencyMs } : {}),
|
|
18
|
+
...(extra.costUsd !== undefined ? { costUsd: extra.costUsd } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parseMocks(value) {
|
|
22
|
+
const out = {};
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
24
|
+
return out;
|
|
25
|
+
for (const [tool, mock] of Object.entries(value)) {
|
|
26
|
+
if (typeof mock !== "object" || mock === null)
|
|
27
|
+
continue;
|
|
28
|
+
const { result, error } = mock;
|
|
29
|
+
if (typeof error === "string")
|
|
30
|
+
out[tool] = { error };
|
|
31
|
+
else if ("result" in mock)
|
|
32
|
+
out[tool] = { result };
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function sumTurns(detail) {
|
|
37
|
+
let costUsd = 0;
|
|
38
|
+
let tokensIn = 0;
|
|
39
|
+
let tokensOut = 0;
|
|
40
|
+
for (const turn of detail.turns) {
|
|
41
|
+
costUsd += turn.costUsd ?? 0;
|
|
42
|
+
tokensIn += turn.inputTokens ?? 0;
|
|
43
|
+
tokensOut += turn.outputTokens ?? 0;
|
|
44
|
+
}
|
|
45
|
+
const modelSpans = detail.spans.filter((span) => span.kind === "model").length;
|
|
46
|
+
return { costUsd, tokensIn, tokensOut, modelCalls: modelSpans > 0 ? modelSpans : detail.turns.length };
|
|
47
|
+
}
|
|
48
|
+
function isEmptyReply(detail, turn) {
|
|
49
|
+
if ((turn.outputTokens ?? 0) > 0)
|
|
50
|
+
return false;
|
|
51
|
+
return !detail.messages.some((m) => m.turnId === turn.id && m.role === "assistant" && m.text.trim() !== "");
|
|
52
|
+
}
|
|
53
|
+
function errorText(error) {
|
|
54
|
+
if (isAgentError(error))
|
|
55
|
+
return error.message === error.code ? error.code : `${error.code}: ${error.message}`;
|
|
56
|
+
return "agent.backend_failed";
|
|
57
|
+
}
|
|
58
|
+
export function createAgentCaseRunner(deps) {
|
|
59
|
+
const clock = deps.now ?? Date.now;
|
|
60
|
+
const wallGraceMs = deps.wallClockGraceMs ?? EVAL_CASE_WALL_CLOCK_GRACE_MS;
|
|
61
|
+
const stopGraceMs = deps.stopGraceMs ?? EVAL_TURN_STOP_GRACE_MS;
|
|
62
|
+
const states = new Map();
|
|
63
|
+
const stateFor = (evalRunId) => {
|
|
64
|
+
let state = states.get(evalRunId);
|
|
65
|
+
if (state === undefined) {
|
|
66
|
+
state = { active: new Map(), spend: 0, cancelled: false, abort: new AbortController(), providerFailures: 0 };
|
|
67
|
+
states.set(evalRunId, state);
|
|
68
|
+
}
|
|
69
|
+
return state;
|
|
70
|
+
};
|
|
71
|
+
async function simulatorFor(plan, host, definition) {
|
|
72
|
+
const modelId = plan.userModel;
|
|
73
|
+
if (deps.simulatorModel)
|
|
74
|
+
return deps.simulatorModel({ projectRoot: plan.projectRoot, definition, modelId, host });
|
|
75
|
+
const prices = (await deps.loadPrices?.([modelId], plan.projectRoot).catch(() => undefined)) ?? new Map();
|
|
76
|
+
return createBackendSimulatorModel({
|
|
77
|
+
backend: host.backend,
|
|
78
|
+
projectRoot: plan.projectRoot,
|
|
79
|
+
definition,
|
|
80
|
+
modelId,
|
|
81
|
+
priceOf: (id) => prices.get(id),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function runCase(plan, req) {
|
|
85
|
+
const state = stateFor(req.evalRunId);
|
|
86
|
+
if (state.stopReason !== undefined)
|
|
87
|
+
return failure("EVAL_CANCELLED", state.stopReason);
|
|
88
|
+
const result = await runCaseInner(plan, req);
|
|
89
|
+
if (result.ok)
|
|
90
|
+
state.providerFailures = 0;
|
|
91
|
+
else if (result.code === "EVAL_AGENT_EMPTY_REPLY" || result.code === "EVAL_AGENT_FAILED") {
|
|
92
|
+
state.providerFailures += 1;
|
|
93
|
+
if (state.providerFailures >= EVAL_MAX_CONSECUTIVE_PROVIDER_FAILURES && state.stopReason === undefined) {
|
|
94
|
+
state.stopReason = `Stopped after ${EVAL_MAX_CONSECUTIVE_PROVIDER_FAILURES} consecutive provider failures; check your provider key or rate limit`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
async function runCaseInner(plan, req) {
|
|
100
|
+
const state = stateFor(req.evalRunId);
|
|
101
|
+
if (state.cancelled)
|
|
102
|
+
return failure("EVAL_CANCELLED", "Eval run was cancelled");
|
|
103
|
+
if (state.spend >= plan.maxCostUsd) {
|
|
104
|
+
return failure("EVAL_BUDGET_EXCEEDED", `Eval spend reached the $${plan.maxCostUsd} limit`);
|
|
105
|
+
}
|
|
106
|
+
const { agent, conversation, target } = plan;
|
|
107
|
+
if (target.kind !== "agent" || !agent || !conversation) {
|
|
108
|
+
return failure("EVAL_ENGINE_ERROR", "Not an agent plan");
|
|
109
|
+
}
|
|
110
|
+
const host = deps.host();
|
|
111
|
+
if (!host)
|
|
112
|
+
return failure("EVAL_ENGINE_ERROR", "Agents are unavailable on this host");
|
|
113
|
+
const planCase = plan.cases.find((c) => c.id === req.caseId);
|
|
114
|
+
if (planCase === undefined)
|
|
115
|
+
return failure("EVAL_ENGINE_ERROR", `Case "${req.caseId}" is not in the plan`);
|
|
116
|
+
const definition = agent.definition;
|
|
117
|
+
const scenario = planCase.input;
|
|
118
|
+
const mode = scenario.mode === "script" || scenario.mode === "simulated" ? scenario.mode : conversation.mode;
|
|
119
|
+
let driver;
|
|
120
|
+
try {
|
|
121
|
+
let simulatorModel;
|
|
122
|
+
if (mode === "simulated") {
|
|
123
|
+
if (!plan.userModel)
|
|
124
|
+
return failure("EVAL_ENGINE_ERROR", "A simulated case needs a user model");
|
|
125
|
+
simulatorModel = await simulatorFor(plan, host, definition);
|
|
126
|
+
}
|
|
127
|
+
driver = createDriver(mode, scenario, simulatorModel ? { simulatorModel } : {});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return failure("EVAL_ENGINE_ERROR", err instanceof DriverScenarioError ? err.message : "Scenario could not be prepared");
|
|
131
|
+
}
|
|
132
|
+
const { service, chat, evalGates } = host;
|
|
133
|
+
const startedAt = clock();
|
|
134
|
+
const sessionMinutes = Math.ceil((conversation.maxTurns * conversation.turnTimeoutMs) / 60_000) + SESSION_OVERHEAD_MINUTES;
|
|
135
|
+
let sessionId;
|
|
136
|
+
try {
|
|
137
|
+
const session = await service.openSession(plan.projectRoot, target.agentId, {
|
|
138
|
+
source: "eval",
|
|
139
|
+
sourceRef: plan.evalRunId,
|
|
140
|
+
title: `eval ${plan.evalId} ${plan.evalRunId} ${req.caseId}.${req.repeatIndex}`,
|
|
141
|
+
eval: {
|
|
142
|
+
...(target.revision !== undefined ? { revision: target.revision } : {}),
|
|
143
|
+
...(plan.modelOverride !== undefined ? { definition: { model: { id: plan.modelOverride } } } : {}),
|
|
144
|
+
limits: { max_turns: conversation.maxTurns + 1, session_minutes: sessionMinutes, cost_usd: null },
|
|
145
|
+
capture: "full",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
sessionId = session.id;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const code = isAgentError(err) && (err.code === "agent.archived" || err.code === "agent.revision_not_found") ? "EVAL_AGENT_FAILED" : "EVAL_ENGINE_ERROR";
|
|
152
|
+
return failure(code, isAgentError(err) ? errorText(err) : "The eval session could not be opened");
|
|
153
|
+
}
|
|
154
|
+
let resolveDone;
|
|
155
|
+
state.active.set(sessionId, { done: new Promise((resolve) => (resolveDone = resolve)) });
|
|
156
|
+
let currentTurn = 0;
|
|
157
|
+
const gate = createEvalGate({
|
|
158
|
+
policy: {
|
|
159
|
+
tools: conversation.tools,
|
|
160
|
+
approvals: conversation.approvals,
|
|
161
|
+
readOnlyTools: conversation.readOnlyTools,
|
|
162
|
+
mocks: parseMocks(scenario.tool_mocks),
|
|
163
|
+
},
|
|
164
|
+
definition,
|
|
165
|
+
projectRoot: plan.projectRoot,
|
|
166
|
+
turn: () => currentTurn,
|
|
167
|
+
});
|
|
168
|
+
evalGates.set(sessionId, gate);
|
|
169
|
+
const usage = { simCost: 0, simIn: 0, simOut: 0, simCalls: 0, simEstimated: false };
|
|
170
|
+
const addSimulator = (u) => {
|
|
171
|
+
if (u === undefined)
|
|
172
|
+
return;
|
|
173
|
+
usage.simCost += u.costUsd;
|
|
174
|
+
usage.simIn += u.tokensIn;
|
|
175
|
+
usage.simOut += u.tokensOut;
|
|
176
|
+
usage.simCalls += 1;
|
|
177
|
+
usage.simEstimated ||= u.estimated === true;
|
|
178
|
+
state.spend += u.costUsd;
|
|
179
|
+
};
|
|
180
|
+
let countedRootCost = 0;
|
|
181
|
+
const syncSpend = (detail) => {
|
|
182
|
+
const cost = sumTurns(detail).costUsd;
|
|
183
|
+
state.spend += cost - countedRootCost;
|
|
184
|
+
countedRootCost = cost;
|
|
185
|
+
};
|
|
186
|
+
const caseCtl = new AbortController();
|
|
187
|
+
let wallExpired = false;
|
|
188
|
+
let stuckTimer;
|
|
189
|
+
let releaseStuck = () => undefined;
|
|
190
|
+
const stuck = new Promise((resolve) => (releaseStuck = () => resolve("stuck")));
|
|
191
|
+
const stopTurn = () => {
|
|
192
|
+
chat.stop(sessionId);
|
|
193
|
+
stuckTimer ??= setTimeout(releaseStuck, stopGraceMs);
|
|
194
|
+
};
|
|
195
|
+
const onCancel = () => {
|
|
196
|
+
caseCtl.abort();
|
|
197
|
+
stopTurn();
|
|
198
|
+
};
|
|
199
|
+
state.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
200
|
+
const wallMs = conversation.maxTurns * conversation.turnTimeoutMs + wallGraceMs;
|
|
201
|
+
const wallTimer = setTimeout(() => {
|
|
202
|
+
wallExpired = true;
|
|
203
|
+
caseCtl.abort();
|
|
204
|
+
stopTurn();
|
|
205
|
+
}, wallMs);
|
|
206
|
+
let turnsDone = 0;
|
|
207
|
+
const converse = async () => {
|
|
208
|
+
const transcript = [];
|
|
209
|
+
for (let turn = 1; turn <= conversation.maxTurns; turn++) {
|
|
210
|
+
if (state.cancelled)
|
|
211
|
+
return { kind: "cancelled" };
|
|
212
|
+
if (wallExpired)
|
|
213
|
+
return { kind: "failed", code: "EVAL_AGENT_TIMEOUT", message: "Conversation exceeded its time limit", stopReason: "timeout" };
|
|
214
|
+
let step;
|
|
215
|
+
try {
|
|
216
|
+
step = await driver.next({ turn, transcript, signal: caseCtl.signal });
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (state.cancelled)
|
|
220
|
+
return { kind: "cancelled" };
|
|
221
|
+
if (wallExpired)
|
|
222
|
+
return { kind: "failed", code: "EVAL_AGENT_TIMEOUT", message: "Conversation exceeded its time limit", stopReason: "timeout" };
|
|
223
|
+
const message = err instanceof SimulatorError ? err.message : "The simulated user failed";
|
|
224
|
+
return { kind: "failed", code: "EVAL_SIMULATOR_ERROR", message, stopReason: "error" };
|
|
225
|
+
}
|
|
226
|
+
addSimulator(step.usage);
|
|
227
|
+
if (step.kind === "end")
|
|
228
|
+
return { kind: "done", stopReason: step.reason };
|
|
229
|
+
currentTurn = turn;
|
|
230
|
+
let turnTimedOut = false;
|
|
231
|
+
const turnTimer = setTimeout(() => {
|
|
232
|
+
turnTimedOut = true;
|
|
233
|
+
stopTurn();
|
|
234
|
+
}, conversation.turnTimeoutMs);
|
|
235
|
+
const sent = chat
|
|
236
|
+
.send({ projectRoot: plan.projectRoot, agentId: target.agentId, sessionId, text: step.text, source: "eval" })
|
|
237
|
+
.then((detail) => ({ ok: true, detail }), (err) => ({ ok: false, err }));
|
|
238
|
+
const outcome = await Promise.race([sent, stuck]);
|
|
239
|
+
clearTimeout(turnTimer);
|
|
240
|
+
turnsDone = turn;
|
|
241
|
+
if (state.cancelled)
|
|
242
|
+
return { kind: "cancelled" };
|
|
243
|
+
if (outcome === "stuck" || turnTimedOut || wallExpired) {
|
|
244
|
+
const message = turnTimedOut ? `Turn ${turn} exceeded ${conversation.turnTimeoutMs} ms` : "Conversation exceeded its time limit";
|
|
245
|
+
return { kind: "failed", code: "EVAL_AGENT_TIMEOUT", message, stopReason: "timeout" };
|
|
246
|
+
}
|
|
247
|
+
if (!outcome.ok) {
|
|
248
|
+
return { kind: "failed", code: "EVAL_AGENT_FAILED", message: errorText(outcome.err), stopReason: "error" };
|
|
249
|
+
}
|
|
250
|
+
const { detail } = outcome;
|
|
251
|
+
syncSpend(detail);
|
|
252
|
+
const latest = detail.turns[detail.turns.length - 1];
|
|
253
|
+
if (latest?.status === "waiting") {
|
|
254
|
+
return { kind: "failed", code: "EVAL_ENGINE_ERROR", message: "A turn is waiting for approval, which an eval cannot give", stopReason: "error" };
|
|
255
|
+
}
|
|
256
|
+
if (latest !== undefined && latest.status !== "succeeded") {
|
|
257
|
+
return { kind: "failed", code: "EVAL_AGENT_FAILED", message: latest.errorCode ?? "agent.backend_failed", stopReason: "error" };
|
|
258
|
+
}
|
|
259
|
+
if (latest !== undefined && isEmptyReply(detail, latest)) {
|
|
260
|
+
return {
|
|
261
|
+
kind: "failed",
|
|
262
|
+
code: "EVAL_AGENT_EMPTY_REPLY",
|
|
263
|
+
message: "The agent returned an empty reply with no output tokens. The model provider may be rate limiting or failing; check your provider key and limits",
|
|
264
|
+
stopReason: "error",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
transcript.splice(0, transcript.length, ...detail.messages.map((m) => ({ role: m.role, text: m.text })));
|
|
268
|
+
if (step.endAfter !== undefined)
|
|
269
|
+
return { kind: "done", stopReason: step.endAfter };
|
|
270
|
+
}
|
|
271
|
+
return { kind: "done", stopReason: "max_turns" };
|
|
272
|
+
};
|
|
273
|
+
let end;
|
|
274
|
+
try {
|
|
275
|
+
end = await converse();
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
end = { kind: "failed", code: "EVAL_ENGINE_ERROR", message: err instanceof Error ? err.message : "Conversation failed", stopReason: "error" };
|
|
279
|
+
}
|
|
280
|
+
clearTimeout(wallTimer);
|
|
281
|
+
if (stuckTimer)
|
|
282
|
+
clearTimeout(stuckTimer);
|
|
283
|
+
state.abort.signal.removeEventListener("abort", onCancel);
|
|
284
|
+
let result;
|
|
285
|
+
try {
|
|
286
|
+
const detail = await service.getSession(plan.projectRoot, sessionId);
|
|
287
|
+
syncSpend(detail);
|
|
288
|
+
const root = sumTurns(detail);
|
|
289
|
+
let childCost = 0;
|
|
290
|
+
let childIn = 0;
|
|
291
|
+
let childOut = 0;
|
|
292
|
+
let childCalls = 0;
|
|
293
|
+
for (const child of service.listChildSessions(sessionId)) {
|
|
294
|
+
const childDetail = await service.getSession(plan.projectRoot, child.id);
|
|
295
|
+
const sums = sumTurns(childDetail);
|
|
296
|
+
childCost += sums.costUsd;
|
|
297
|
+
childIn += sums.tokensIn;
|
|
298
|
+
childOut += sums.tokensOut;
|
|
299
|
+
childCalls += sums.modelCalls;
|
|
300
|
+
}
|
|
301
|
+
state.spend += childCost;
|
|
302
|
+
const agentCost = root.costUsd + childCost;
|
|
303
|
+
const conversationUsage = {
|
|
304
|
+
cost_usd: agentCost + usage.simCost,
|
|
305
|
+
agent_cost_usd: agentCost,
|
|
306
|
+
simulator_cost_usd: usage.simCost,
|
|
307
|
+
tokens_in: root.tokensIn + childIn,
|
|
308
|
+
tokens_out: root.tokensOut + childOut,
|
|
309
|
+
simulator_tokens_in: usage.simIn,
|
|
310
|
+
simulator_tokens_out: usage.simOut,
|
|
311
|
+
simulator_estimated: usage.simEstimated,
|
|
312
|
+
agent_calls: root.modelCalls + childCalls,
|
|
313
|
+
simulator_calls: usage.simCalls,
|
|
314
|
+
};
|
|
315
|
+
const latencyMs = Math.max(0, clock() - startedAt);
|
|
316
|
+
const costUsd = conversationUsage.cost_usd;
|
|
317
|
+
const subject = (stopReason, failed) => buildConversationSubject({
|
|
318
|
+
detail,
|
|
319
|
+
calls: gate.records(),
|
|
320
|
+
stopReason,
|
|
321
|
+
usage: conversationUsage,
|
|
322
|
+
turns: turnsDone,
|
|
323
|
+
...(failed !== undefined ? { failure: failed } : {}),
|
|
324
|
+
});
|
|
325
|
+
if (end.kind === "done") {
|
|
326
|
+
result = {
|
|
327
|
+
ok: true,
|
|
328
|
+
subjectRef: sessionId,
|
|
329
|
+
output: subject(end.stopReason),
|
|
330
|
+
latencyMs,
|
|
331
|
+
costUsd,
|
|
332
|
+
tokensIn: conversationUsage.tokens_in,
|
|
333
|
+
tokensOut: conversationUsage.tokens_out,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
else if (end.kind === "cancelled") {
|
|
337
|
+
result = failure("EVAL_CANCELLED", "Eval run was cancelled", { subjectRef: sessionId, latencyMs, costUsd });
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
result = failure(end.code, end.message, {
|
|
341
|
+
subjectRef: sessionId,
|
|
342
|
+
output: subject(end.stopReason, end.message),
|
|
343
|
+
latencyMs,
|
|
344
|
+
costUsd,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
result = failure("EVAL_ENGINE_ERROR", err instanceof Error ? err.message : "Conversation could not be read", { subjectRef: sessionId });
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
try {
|
|
353
|
+
const children = service.listChildSessions(sessionId);
|
|
354
|
+
await chat.closeEvalSession(plan.projectRoot, sessionId);
|
|
355
|
+
for (const child of children) {
|
|
356
|
+
await chat.release(child.id);
|
|
357
|
+
service.forgetEvalSession(child.id);
|
|
358
|
+
evalGates.delete(child.id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
/* the runtime session is dropped with the Host at the latest */
|
|
363
|
+
}
|
|
364
|
+
evalGates.delete(sessionId);
|
|
365
|
+
state.active.delete(sessionId);
|
|
366
|
+
resolveDone();
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
runCase,
|
|
372
|
+
async cancelAll(evalRunId) {
|
|
373
|
+
const state = stateFor(evalRunId);
|
|
374
|
+
state.cancelled = true;
|
|
375
|
+
state.abort.abort();
|
|
376
|
+
const host = deps.host();
|
|
377
|
+
await Promise.all([...state.active.entries()].map(async ([sessionId, entry]) => {
|
|
378
|
+
host?.chat.stop(sessionId);
|
|
379
|
+
await entry.done;
|
|
380
|
+
}));
|
|
381
|
+
},
|
|
382
|
+
spend: (evalRunId) => states.get(evalRunId)?.spend ?? 0,
|
|
383
|
+
stopReason: (evalRunId) => states.get(evalRunId)?.stopReason,
|
|
384
|
+
addSpend(evalRunId, usd) {
|
|
385
|
+
stateFor(evalRunId).spend += usd;
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StageCaseRunner } from "./stageCaseRunner.js";
|
|
2
|
+
export type EvalCaseRunner = StageCaseRunner;
|
|
3
|
+
/** Routes each case to the stage or agent runner by the plan's target kind. */
|
|
4
|
+
export declare function createEvalCaseRunner(deps: {
|
|
5
|
+
stage: StageCaseRunner;
|
|
6
|
+
agent: StageCaseRunner;
|
|
7
|
+
}): EvalCaseRunner;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Routes each case to the stage or agent runner by the plan's target kind. */
|
|
2
|
+
export function createEvalCaseRunner(deps) {
|
|
3
|
+
const kinds = new Map();
|
|
4
|
+
const runnerOf = (evalRunId) => (kinds.get(evalRunId) === "agent" ? deps.agent : deps.stage);
|
|
5
|
+
return {
|
|
6
|
+
runCase(plan, req) {
|
|
7
|
+
kinds.set(req.evalRunId, plan.target.kind === "agent" ? "agent" : "stage");
|
|
8
|
+
return runnerOf(req.evalRunId).runCase(plan, req);
|
|
9
|
+
},
|
|
10
|
+
async cancelAll(evalRunId) {
|
|
11
|
+
await Promise.all([deps.stage.cancelAll(evalRunId), deps.agent.cancelAll(evalRunId)]);
|
|
12
|
+
},
|
|
13
|
+
spend: (evalRunId) => deps.stage.spend(evalRunId) + deps.agent.spend(evalRunId),
|
|
14
|
+
addSpend: (evalRunId, usd) => runnerOf(evalRunId).addSpend(evalRunId, usd),
|
|
15
|
+
stopReason: (evalRunId) => runnerOf(evalRunId).stopReason?.(evalRunId),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EvalToolCallRecord } from "../../agents/evalGate.js";
|
|
2
|
+
import type { AgentSessionDetail } from "../../agents/service.js";
|
|
3
|
+
import type { ConversationOutput, ConversationStopReason, ConversationUsage } from "../types.js";
|
|
4
|
+
export type ConversationSubjectInput = {
|
|
5
|
+
detail: AgentSessionDetail;
|
|
6
|
+
calls: readonly EvalToolCallRecord[];
|
|
7
|
+
stopReason: ConversationStopReason;
|
|
8
|
+
usage: ConversationUsage;
|
|
9
|
+
/** User turns completed (a turn counts once the agent replied or failed). */
|
|
10
|
+
turns: number;
|
|
11
|
+
/** Set for the partial subject of a failed case. */
|
|
12
|
+
failure?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The normalized subject of an agent result. Every text is redacted before it is capped, because stored agent
|
|
16
|
+
* messages are not redacted.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildConversationSubject(input: ConversationSubjectInput): ConversationOutput;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { redactSecrets } from "../../agent/streamLogRedact.js";
|
|
2
|
+
import { EVAL_CONVERSATION_LIMITS } from "../constants.js";
|
|
3
|
+
import { renderTranscript } from "../graders/transcript.js";
|
|
4
|
+
const L = EVAL_CONVERSATION_LIMITS;
|
|
5
|
+
function cut(text, max, count) {
|
|
6
|
+
if (text.length <= max)
|
|
7
|
+
return text;
|
|
8
|
+
count();
|
|
9
|
+
return `${text.slice(0, max)}…[truncated ${text.length - max} chars]`;
|
|
10
|
+
}
|
|
11
|
+
function redactedJson(value) {
|
|
12
|
+
try {
|
|
13
|
+
return redactSecrets(JSON.stringify(value ?? {}));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "{}";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function subAgentSessions(detail) {
|
|
20
|
+
const out = new Map();
|
|
21
|
+
for (const span of detail.spans) {
|
|
22
|
+
const callId = span.attributes["gen_ai.tool.call.id"];
|
|
23
|
+
const child = span.attributes["stageflow.sub_agent.session_id"];
|
|
24
|
+
if (typeof callId === "string" && typeof child === "string")
|
|
25
|
+
out.set(callId, child);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function messagesOf(detail) {
|
|
30
|
+
let turn = 0;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const message of detail.messages) {
|
|
33
|
+
if (message.role === "user")
|
|
34
|
+
turn += 1;
|
|
35
|
+
out.push({
|
|
36
|
+
role: message.role,
|
|
37
|
+
text: cut(redactSecrets(message.text), L.maxMessageChars, () => undefined),
|
|
38
|
+
turn: Math.max(turn, 1),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function toolCallsOf(detail, calls, truncation) {
|
|
44
|
+
const children = subAgentSessions(detail);
|
|
45
|
+
const kept = calls.slice(0, L.maxToolCalls);
|
|
46
|
+
truncation.tool_calls_dropped = calls.length - kept.length;
|
|
47
|
+
return kept.map((call) => {
|
|
48
|
+
let args;
|
|
49
|
+
const json = redactedJson(call.args);
|
|
50
|
+
if (json.length > L.maxArgsChars) {
|
|
51
|
+
truncation.tool_args += 1;
|
|
52
|
+
args = { _truncated: json.slice(0, L.maxArgsChars) };
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
args = JSON.parse(json);
|
|
56
|
+
}
|
|
57
|
+
const result = cut(redactSecrets(call.result ?? ""), L.maxResultSummaryChars, () => (truncation.tool_results += 1));
|
|
58
|
+
const sessionId = children.get(call.id);
|
|
59
|
+
return {
|
|
60
|
+
id: call.id,
|
|
61
|
+
turn: call.turn,
|
|
62
|
+
name: call.name,
|
|
63
|
+
args,
|
|
64
|
+
result_summary: result,
|
|
65
|
+
status: call.status,
|
|
66
|
+
policy_outcome: call.policyOutcome,
|
|
67
|
+
...(sessionId !== undefined ? { session_id: sessionId } : {}),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function sizeOf(output) {
|
|
72
|
+
return Buffer.byteLength(JSON.stringify(output));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The normalized subject of an agent result. Every text is redacted before it is capped, because stored agent
|
|
76
|
+
* messages are not redacted.
|
|
77
|
+
*/
|
|
78
|
+
export function buildConversationSubject(input) {
|
|
79
|
+
const truncation = { messages: 0, tool_results: 0, tool_args: 0, tool_calls_dropped: 0 };
|
|
80
|
+
let messages = messagesOf(input.detail);
|
|
81
|
+
const finalReply = [...messages].reverse().find((m) => m.role === "assistant")?.text ?? "";
|
|
82
|
+
const tool_calls = toolCallsOf(input.detail, input.calls, truncation);
|
|
83
|
+
let droppedMessages = 0;
|
|
84
|
+
if (messages.length > L.maxMessages) {
|
|
85
|
+
const tail = L.maxMessages - L.keepFirstMessages;
|
|
86
|
+
droppedMessages = messages.length - L.maxMessages;
|
|
87
|
+
messages = [...messages.slice(0, L.keepFirstMessages), ...messages.slice(messages.length - tail)];
|
|
88
|
+
}
|
|
89
|
+
const summary = input.failure !== undefined
|
|
90
|
+
? `${input.turns} turns, ${tool_calls.length} tool calls, ended ${input.stopReason}: ${input.failure}`
|
|
91
|
+
: `${input.turns} turns, ${tool_calls.length} tool calls, ended ${input.stopReason}`;
|
|
92
|
+
const output = {
|
|
93
|
+
kind: "conversation",
|
|
94
|
+
status: input.failure !== undefined ? "failure" : "success",
|
|
95
|
+
summary,
|
|
96
|
+
messages,
|
|
97
|
+
tool_calls,
|
|
98
|
+
final_reply: finalReply,
|
|
99
|
+
turns: input.turns,
|
|
100
|
+
stop_reason: input.stopReason,
|
|
101
|
+
usage: input.usage,
|
|
102
|
+
};
|
|
103
|
+
while (sizeOf(output) > L.maxSubjectBytes && output.messages.length > 2) {
|
|
104
|
+
const middle = Math.max(1, Math.floor(output.messages.length / 2));
|
|
105
|
+
output.messages.splice(middle, 1);
|
|
106
|
+
droppedMessages += 1;
|
|
107
|
+
}
|
|
108
|
+
truncation.messages = droppedMessages + countTruncatedText(output.messages);
|
|
109
|
+
const counts = Object.entries(truncation).filter(([, value]) => value > 0);
|
|
110
|
+
if (counts.length > 0) {
|
|
111
|
+
output.truncated = {
|
|
112
|
+
messages: truncation.messages,
|
|
113
|
+
tool_results: truncation.tool_results,
|
|
114
|
+
tool_args: truncation.tool_args,
|
|
115
|
+
tool_calls_dropped: truncation.tool_calls_dropped,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
output.transcript = renderTranscript(output);
|
|
119
|
+
return output;
|
|
120
|
+
}
|
|
121
|
+
function countTruncatedText(messages) {
|
|
122
|
+
return messages.filter((m) => /…\[truncated \d+ chars\]$/.test(m.text)).length;
|
|
123
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../config/loadAgent.js";
|
|
2
|
+
import type { ConversationBackend } from "../../agents/runtime/types.js";
|
|
3
|
+
export type DriverUsage = {
|
|
4
|
+
costUsd: number;
|
|
5
|
+
tokensIn: number;
|
|
6
|
+
tokensOut: number;
|
|
7
|
+
model: string;
|
|
8
|
+
estimated?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type SimulatorModelInput = {
|
|
11
|
+
system: string;
|
|
12
|
+
user: string;
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
};
|
|
15
|
+
export type SimulatorModelReply = {
|
|
16
|
+
text: string;
|
|
17
|
+
usage?: DriverUsage;
|
|
18
|
+
};
|
|
19
|
+
export interface SimulatorModel {
|
|
20
|
+
complete(input: SimulatorModelInput): Promise<SimulatorModelReply>;
|
|
21
|
+
}
|
|
22
|
+
export declare class SimulatorError extends Error {
|
|
23
|
+
readonly code = "EVAL_SIMULATOR_ERROR";
|
|
24
|
+
constructor(message?: string);
|
|
25
|
+
}
|
|
26
|
+
export declare const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
27
|
+
/** Price in USD per one million tokens. */
|
|
28
|
+
export type ModelPrice = {
|
|
29
|
+
inputPerMillion: number;
|
|
30
|
+
outputPerMillion: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function estimateTokens(text: string): number;
|
|
33
|
+
export declare function createBackendSimulatorModel(options: {
|
|
34
|
+
backend: ConversationBackend;
|
|
35
|
+
projectRoot: string;
|
|
36
|
+
definition: AgentDefinition;
|
|
37
|
+
modelId: string;
|
|
38
|
+
priceOf?: (modelId: string) => ModelPrice | undefined;
|
|
39
|
+
}): SimulatorModel;
|
|
40
|
+
export type FakeSimulatorReply = string | {
|
|
41
|
+
text: string;
|
|
42
|
+
usage?: DriverUsage;
|
|
43
|
+
} | Error;
|
|
44
|
+
export declare class FakeSimulatorModel implements SimulatorModel {
|
|
45
|
+
private readonly replies;
|
|
46
|
+
private readonly defaultUsage;
|
|
47
|
+
readonly calls: SimulatorModelInput[];
|
|
48
|
+
private index;
|
|
49
|
+
constructor(replies: readonly FakeSimulatorReply[] | ((call: SimulatorModelInput, index: number) => FakeSimulatorReply), defaultUsage?: DriverUsage);
|
|
50
|
+
complete(input: SimulatorModelInput): Promise<SimulatorModelReply>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class SimulatorError extends Error {
|
|
2
|
+
code = "EVAL_SIMULATOR_ERROR";
|
|
3
|
+
constructor(message = "The simulated user model failed to reply.") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "SimulatorError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
9
|
+
export function estimateTokens(text) {
|
|
10
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
|
|
11
|
+
}
|
|
12
|
+
export function createBackendSimulatorModel(options) {
|
|
13
|
+
const { backend, projectRoot, modelId } = options;
|
|
14
|
+
const definition = { ...options.definition, model: { ...options.definition.model, id: modelId } };
|
|
15
|
+
return {
|
|
16
|
+
async complete(input) {
|
|
17
|
+
if (backend.judge === undefined)
|
|
18
|
+
throw new SimulatorError("The agent backend cannot run a one-shot model call.");
|
|
19
|
+
const prompt = `${input.system}\n\n${input.user}`;
|
|
20
|
+
let text;
|
|
21
|
+
try {
|
|
22
|
+
text = await backend.judge({ projectRoot, definition, prompt: input.user, systemPrompt: input.system, signal: input.signal });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (input.signal.aborted)
|
|
26
|
+
throw error;
|
|
27
|
+
throw new SimulatorError();
|
|
28
|
+
}
|
|
29
|
+
const tokensIn = estimateTokens(prompt);
|
|
30
|
+
const tokensOut = estimateTokens(text);
|
|
31
|
+
const price = options.priceOf?.(modelId);
|
|
32
|
+
const costUsd = price === undefined ? 0 : (tokensIn * price.inputPerMillion + tokensOut * price.outputPerMillion) / 1_000_000;
|
|
33
|
+
return { text, usage: { costUsd, tokensIn, tokensOut, model: modelId, estimated: true } };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export class FakeSimulatorModel {
|
|
38
|
+
replies;
|
|
39
|
+
defaultUsage;
|
|
40
|
+
calls = [];
|
|
41
|
+
index = 0;
|
|
42
|
+
constructor(replies, defaultUsage = { costUsd: 0.001, tokensIn: 100, tokensOut: 10, model: "fake/simulator" }) {
|
|
43
|
+
this.replies = replies;
|
|
44
|
+
this.defaultUsage = defaultUsage;
|
|
45
|
+
}
|
|
46
|
+
async complete(input) {
|
|
47
|
+
const index = this.index++;
|
|
48
|
+
this.calls.push(input);
|
|
49
|
+
const reply = typeof this.replies === "function" ? this.replies(input, index) : this.replies[index];
|
|
50
|
+
if (reply === undefined)
|
|
51
|
+
throw new Error("FakeSimulatorModel has no scripted reply left");
|
|
52
|
+
if (reply instanceof Error)
|
|
53
|
+
throw reply;
|
|
54
|
+
if (typeof reply === "string")
|
|
55
|
+
return { text: reply, usage: this.defaultUsage };
|
|
56
|
+
return { text: reply.text, usage: reply.usage ?? this.defaultUsage };
|
|
57
|
+
}
|
|
58
|
+
}
|