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
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ The same pipeline runs three ways without rewriting anything:
|
|
|
35
35
|
- **Verified stage execution** — after-phase `verify` checks with durable evidence, plus `on_verify_fail` repair or manual operator recovery ([guide](docs/verified-stage-execution.md))
|
|
36
36
|
- **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
|
|
37
37
|
- **Agents** — project-owned, revisioned agents (`agents/*.agent.yaml`) with granted tools, ask-first approvals, traces, channels, schedules and a public web chat page ([guide](docs/agents.md))
|
|
38
|
+
- **Evals** — check a stage or an agent against a dataset: graders, repeats, honest statistics, compare two runs, and sort failures into what to fix ([guide](docs/evals.md))
|
|
38
39
|
- **Operator console** — an Inbox for gates and agent approvals, pipeline runs, agents and their sessions, connections, MCP servers and settings at `http://127.0.0.1:3847` ([guide](docs/operator-console.md))
|
|
39
40
|
- **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` or `sf mcp` is running
|
|
40
41
|
- **MCP servers for agents and stages** — add servers, secrets and OAuth sign-in from Settings → Project MCP, or in the project `.mcp.json` ([guide](docs/mcp-servers.md))
|
|
@@ -288,7 +289,7 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
|
|
|
288
289
|
| [docs/hosted.md](docs/hosted.md) | Embed Stageflow from another application and receive ordered run events |
|
|
289
290
|
| [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
|
|
290
291
|
| [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
|
|
291
|
-
| [docs/cli-reference.md](docs/cli-reference.md) | `sf init`, `sf run`, `sf run-stage`, `sf graph`, `sf validate`, `sf ui`, `sf mcp`, `sf envelope`, `sf export-run`, `sf artifact`, `sf skills`, `sf providers` |
|
|
292
|
+
| [docs/cli-reference.md](docs/cli-reference.md) | `sf init`, `sf run`, `sf run-stage`, `sf graph`, `sf validate`, `sf ui`, `sf mcp`, `sf envelope`, `sf export-run`, `sf artifact`, `sf skills`, `sf providers`, `sf eval` |
|
|
292
293
|
| [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
|
|
293
294
|
| [docs/verified-stage-execution.md](docs/verified-stage-execution.md) | After-phase `verify`, evidence, and `on_verify_fail` repair / manual recovery |
|
|
294
295
|
| [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
|
|
@@ -298,6 +299,7 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
|
|
|
298
299
|
| [docs/providers.md](docs/providers.md) | Pi providers, `sf providers` |
|
|
299
300
|
| [docs/docker.md](docs/docker.md) | Docker Compose local try, image pull/run, backup, self-hosting |
|
|
300
301
|
| [docs/operator-console.md](docs/operator-console.md) | Console navigation, command palette, shortcuts and routes; one page per area ([Inbox](docs/console-inbox.md), [Runs](docs/console-runs.md), [Agents](docs/console-agents.md), [Connections](docs/console-connections.md), [Settings](docs/console-settings.md)) |
|
|
302
|
+
| [docs/evals.md](docs/evals.md) | Evals for stages and agents: datasets, graders, runs, compare, failure sorting, the engine |
|
|
301
303
|
| [docs/agents.md](docs/agents.md) | Agents: definition, runtime, approvals, channels, schedules, web chat |
|
|
302
304
|
| [docs/mcp-servers.md](docs/mcp-servers.md) | Add MCP servers, secrets and OAuth; use them from agents and stages |
|
|
303
305
|
| [docs/channels.md](docs/channels.md) | Connections and communication channels |
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AuthorSessionRecord } from "./authorSessionStore.js";
|
|
1
2
|
import { type SummaryLine } from "./definitionSummary.js";
|
|
2
3
|
import { type AgentAuthorDraft, type OperatorAgentHost } from "../operatorAgent/index.js";
|
|
3
4
|
export type AgentAuthorTurnResult = {
|
|
@@ -14,6 +15,9 @@ export declare function runAgentAuthorTurn(input: {
|
|
|
14
15
|
message: string;
|
|
15
16
|
sessionId?: string;
|
|
16
17
|
draft?: AgentAuthorDraft;
|
|
18
|
+
/** Composer override. Blank uses the same default as Workshop chat. */
|
|
19
|
+
model?: string | null;
|
|
17
20
|
}): Promise<AgentAuthorTurnResult>;
|
|
18
21
|
export declare function undoAgentAuthorTurn(sessionId: string): AgentAuthorDraft;
|
|
22
|
+
export declare function authorSessionForApi(sessionId: string): AuthorSessionRecord;
|
|
19
23
|
export declare function parseAgentAuthorDraft(body: unknown): AgentAuthorDraft;
|
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { resolveWorkshopModel } from "../workshop/modelSettings.js";
|
|
3
|
+
import { createAuthorSession, readAuthorSession, saveAuthorSession, } from "./authorSessionStore.js";
|
|
2
4
|
import { describeChanges } from "./definitionSummary.js";
|
|
3
5
|
import { AGENT_AUTHOR_PROFILE_ID, createAgentAuthorContext, isAgentAuthorContext, readAgentAuthorDraft, } from "../operatorAgent/index.js";
|
|
4
6
|
const sessions = new Map();
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function sameDraftValue(a, b) {
|
|
11
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
12
|
+
}
|
|
13
|
+
/** Objects walk key by key; arrays and scalars are wholes. Turn changes win, including removals. */
|
|
14
|
+
function mergeTurnDraft(start, end, disk) {
|
|
15
|
+
const keys = new Set([...Object.keys(start), ...Object.keys(end), ...Object.keys(disk)]);
|
|
16
|
+
const next = {};
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const startHas = Object.prototype.hasOwnProperty.call(start, key);
|
|
19
|
+
const endHas = Object.prototype.hasOwnProperty.call(end, key);
|
|
20
|
+
const diskHas = Object.prototype.hasOwnProperty.call(disk, key);
|
|
21
|
+
if (startHas && !endHas)
|
|
22
|
+
continue;
|
|
23
|
+
if (!endHas) {
|
|
24
|
+
if (diskHas)
|
|
25
|
+
next[key] = structuredClone(disk[key]);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!startHas) {
|
|
29
|
+
next[key] = structuredClone(end[key]);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const startVal = start[key];
|
|
33
|
+
const endVal = end[key];
|
|
34
|
+
const diskVal = disk[key];
|
|
35
|
+
if (isPlainObject(startVal) && isPlainObject(endVal) && (!diskHas || isPlainObject(diskVal))) {
|
|
36
|
+
if (!diskHas && sameDraftValue(startVal, endVal))
|
|
37
|
+
continue;
|
|
38
|
+
next[key] = mergeTurnDraft(startVal, endVal, diskHas && isPlainObject(diskVal) ? diskVal : {});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (sameDraftValue(startVal, endVal)) {
|
|
42
|
+
if (diskHas)
|
|
43
|
+
next[key] = structuredClone(diskVal);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
next[key] = structuredClone(endVal);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
5
51
|
export function resetAgentAuthorSessionsForTests() {
|
|
6
52
|
for (const session of sessions.values())
|
|
7
53
|
session.close();
|
|
@@ -12,26 +58,49 @@ export async function runAgentAuthorTurn(input) {
|
|
|
12
58
|
if (!message)
|
|
13
59
|
throw new Error("agent.invalid");
|
|
14
60
|
let sessionId = input.sessionId?.trim();
|
|
61
|
+
let record = sessionId ? readAuthorSession(sessionId) : null;
|
|
15
62
|
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
16
|
-
if (sessionId && !session)
|
|
63
|
+
if (sessionId && !session && !record)
|
|
17
64
|
throw new Error("agent.session_not_found");
|
|
18
65
|
const posted = createAgentAuthorContext({
|
|
19
66
|
projectRoot: input.projectRoot,
|
|
20
|
-
draft: input.draft,
|
|
67
|
+
draft: input.draft ?? record?.draft,
|
|
21
68
|
});
|
|
69
|
+
if (!record) {
|
|
70
|
+
const draft = readAgentAuthorDraft(posted);
|
|
71
|
+
if (!sessionId) {
|
|
72
|
+
record = createAuthorSession({ draft, model: input.model ?? undefined });
|
|
73
|
+
sessionId = record.id;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
record = saveAuthorSession({
|
|
78
|
+
version: 1,
|
|
79
|
+
id: sessionId,
|
|
80
|
+
createdAt: now,
|
|
81
|
+
updatedAt: now,
|
|
82
|
+
draft,
|
|
83
|
+
transcript: [],
|
|
84
|
+
...(input.model?.trim() ? { model: input.model.trim() } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!sessionId || !record)
|
|
89
|
+
throw new Error("agent.invalid");
|
|
22
90
|
if (!session) {
|
|
23
|
-
sessionId = randomUUID();
|
|
24
91
|
session = input.host.openSession({
|
|
25
92
|
profileId: AGENT_AUTHOR_PROFILE_ID,
|
|
26
93
|
context: posted,
|
|
27
94
|
});
|
|
28
95
|
sessions.set(sessionId, session);
|
|
96
|
+
if (record.transcript.length > 0) {
|
|
97
|
+
await session.prepareRestart?.(record.transcript.map((entry) => ({ role: entry.role, text: entry.text })));
|
|
98
|
+
}
|
|
29
99
|
}
|
|
30
|
-
|
|
31
|
-
session.setContext(posted);
|
|
32
|
-
}
|
|
100
|
+
session.setContext(posted);
|
|
33
101
|
const before = readAgentAuthorDraft(session.getContext());
|
|
34
|
-
const
|
|
102
|
+
const modelId = resolveWorkshopModel({ sessionOverride: input.model ?? record.model });
|
|
103
|
+
const events = await session.send(message, { modelId });
|
|
35
104
|
const text = events
|
|
36
105
|
.filter((event) => event.type === "message" && event.role === "assistant")
|
|
37
106
|
.map((event) => (event.type === "message" ? event.text : ""))
|
|
@@ -39,11 +108,33 @@ export async function runAgentAuthorTurn(input) {
|
|
|
39
108
|
.join("\n\n");
|
|
40
109
|
const error = events.find((event) => event.type === "error");
|
|
41
110
|
const draft = readAgentAuthorDraft(session.getContext());
|
|
111
|
+
const summaryLines = describeChanges(before, draft);
|
|
112
|
+
const latest = readAuthorSession(sessionId);
|
|
113
|
+
if (!latest)
|
|
114
|
+
throw new Error("agent.session_not_found");
|
|
115
|
+
const merged = mergeTurnDraft(before, draft, latest.draft);
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
const saved = saveAuthorSession({
|
|
118
|
+
...latest,
|
|
119
|
+
draft: merged,
|
|
120
|
+
...(input.model?.trim() ? { model: input.model.trim() } : {}),
|
|
121
|
+
transcript: [
|
|
122
|
+
...latest.transcript,
|
|
123
|
+
{ id: randomUUID(), role: "user", text: message, createdAt: now },
|
|
124
|
+
{
|
|
125
|
+
id: randomUUID(),
|
|
126
|
+
role: "assistant",
|
|
127
|
+
text: text || (error && error.type === "error" ? error.message : ""),
|
|
128
|
+
createdAt: now,
|
|
129
|
+
...(summaryLines.length > 0 ? { summaryLines } : {}),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
42
133
|
return {
|
|
43
|
-
sessionId:
|
|
44
|
-
text:
|
|
45
|
-
draft,
|
|
46
|
-
summaryLines
|
|
134
|
+
sessionId: saved.id,
|
|
135
|
+
text: saved.transcript.at(-1)?.text ?? "",
|
|
136
|
+
draft: merged,
|
|
137
|
+
summaryLines,
|
|
47
138
|
};
|
|
48
139
|
}
|
|
49
140
|
export function undoAgentAuthorTurn(sessionId) {
|
|
@@ -56,7 +147,17 @@ export function undoAgentAuthorTurn(sessionId) {
|
|
|
56
147
|
const context = session.getContext();
|
|
57
148
|
if (!isAgentAuthorContext(context))
|
|
58
149
|
throw new Error("agent.invalid");
|
|
59
|
-
|
|
150
|
+
const draft = readAgentAuthorDraft(context);
|
|
151
|
+
const record = readAuthorSession(sessionId);
|
|
152
|
+
if (record)
|
|
153
|
+
saveAuthorSession({ ...record, draft });
|
|
154
|
+
return draft;
|
|
155
|
+
}
|
|
156
|
+
export function authorSessionForApi(sessionId) {
|
|
157
|
+
const record = readAuthorSession(sessionId);
|
|
158
|
+
if (!record)
|
|
159
|
+
throw new Error("agent.session_not_found");
|
|
160
|
+
return record;
|
|
60
161
|
}
|
|
61
162
|
export function parseAgentAuthorDraft(body) {
|
|
62
163
|
return body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SummaryLine } from "./definitionSummary.js";
|
|
2
|
+
import type { AgentAuthorDraft } from "../operatorAgent/index.js";
|
|
3
|
+
export type AuthorSessionMessage = {
|
|
4
|
+
id: string;
|
|
5
|
+
role: "user" | "assistant";
|
|
6
|
+
text: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
summaryLines?: SummaryLine[];
|
|
9
|
+
};
|
|
10
|
+
export type AuthorSessionRecord = {
|
|
11
|
+
version: 1;
|
|
12
|
+
id: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
draft: AgentAuthorDraft;
|
|
16
|
+
transcript: AuthorSessionMessage[];
|
|
17
|
+
model?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function authorSessionsDir(storeRoot?: string): string;
|
|
20
|
+
export declare function authorSessionFilePath(sessionId: string, storeRoot?: string): string;
|
|
21
|
+
export declare function parseAuthorSession(value: unknown): AuthorSessionRecord | null;
|
|
22
|
+
export declare function createAuthorSession(input?: {
|
|
23
|
+
draft?: AgentAuthorDraft;
|
|
24
|
+
model?: string;
|
|
25
|
+
}, storeRoot?: string): AuthorSessionRecord;
|
|
26
|
+
export declare function readAuthorSession(sessionId: string, storeRoot?: string): AuthorSessionRecord | null;
|
|
27
|
+
export declare function saveAuthorSession(record: AuthorSessionRecord, storeRoot?: string): AuthorSessionRecord;
|
|
28
|
+
export type AuthorDraftSummary = {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
messages: number;
|
|
33
|
+
};
|
|
34
|
+
/** Unsaved author sessions, newest first. Empty shells with no name and no chat are left out. */
|
|
35
|
+
export declare function listAuthorDrafts(storeRoot?: string): AuthorDraftSummary[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
5
|
+
function guardSessionId(sessionId) {
|
|
6
|
+
const trimmed = sessionId.trim();
|
|
7
|
+
if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..") || trimmed === ".") {
|
|
8
|
+
throw new Error("agent.invalid");
|
|
9
|
+
}
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
export function authorSessionsDir(storeRoot = globalStageflowHome()) {
|
|
13
|
+
return path.join(storeRoot, "agents", "author-sessions");
|
|
14
|
+
}
|
|
15
|
+
export function authorSessionFilePath(sessionId, storeRoot = globalStageflowHome()) {
|
|
16
|
+
return path.join(authorSessionsDir(storeRoot), guardSessionId(sessionId), "session.json");
|
|
17
|
+
}
|
|
18
|
+
function isPlainObject(value) {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function parseMessage(value) {
|
|
22
|
+
if (!isPlainObject(value))
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof value.id !== "string" || !value.id)
|
|
25
|
+
return null;
|
|
26
|
+
if (value.role !== "user" && value.role !== "assistant")
|
|
27
|
+
return null;
|
|
28
|
+
if (typeof value.text !== "string")
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof value.createdAt !== "string" || !value.createdAt)
|
|
31
|
+
return null;
|
|
32
|
+
const summaryLines = Array.isArray(value.summaryLines)
|
|
33
|
+
? value.summaryLines.flatMap((line) => {
|
|
34
|
+
if (!isPlainObject(line) || typeof line.label !== "string" || typeof line.value !== "string")
|
|
35
|
+
return [];
|
|
36
|
+
return [{ label: line.label, value: line.value }];
|
|
37
|
+
})
|
|
38
|
+
: [];
|
|
39
|
+
return {
|
|
40
|
+
id: value.id,
|
|
41
|
+
role: value.role,
|
|
42
|
+
text: value.text,
|
|
43
|
+
createdAt: value.createdAt,
|
|
44
|
+
...(summaryLines.length > 0 ? { summaryLines } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function parseAuthorSession(value) {
|
|
48
|
+
if (!isPlainObject(value) || value.version !== 1)
|
|
49
|
+
return null;
|
|
50
|
+
if (typeof value.id !== "string" || !value.id)
|
|
51
|
+
return null;
|
|
52
|
+
if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string")
|
|
53
|
+
return null;
|
|
54
|
+
if (!isPlainObject(value.draft))
|
|
55
|
+
return null;
|
|
56
|
+
if (!Array.isArray(value.transcript))
|
|
57
|
+
return null;
|
|
58
|
+
const transcript = [];
|
|
59
|
+
for (const entry of value.transcript) {
|
|
60
|
+
const message = parseMessage(entry);
|
|
61
|
+
if (!message)
|
|
62
|
+
return null;
|
|
63
|
+
transcript.push(message);
|
|
64
|
+
}
|
|
65
|
+
const model = typeof value.model === "string" && value.model.trim() ? value.model.trim() : undefined;
|
|
66
|
+
return {
|
|
67
|
+
version: 1,
|
|
68
|
+
id: value.id,
|
|
69
|
+
createdAt: value.createdAt,
|
|
70
|
+
updatedAt: value.updatedAt,
|
|
71
|
+
draft: value.draft,
|
|
72
|
+
transcript,
|
|
73
|
+
...(model ? { model } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function writeRecord(record, storeRoot) {
|
|
77
|
+
const file = authorSessionFilePath(record.id, storeRoot);
|
|
78
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
79
|
+
writeFileSync(file, JSON.stringify(record));
|
|
80
|
+
}
|
|
81
|
+
export function createAuthorSession(input = {}, storeRoot) {
|
|
82
|
+
const now = new Date().toISOString();
|
|
83
|
+
const record = {
|
|
84
|
+
version: 1,
|
|
85
|
+
id: randomUUID(),
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
draft: { ...(input.draft ?? {}) },
|
|
89
|
+
transcript: [],
|
|
90
|
+
...(input.model?.trim() ? { model: input.model.trim() } : {}),
|
|
91
|
+
};
|
|
92
|
+
writeRecord(record, storeRoot);
|
|
93
|
+
return record;
|
|
94
|
+
}
|
|
95
|
+
export function readAuthorSession(sessionId, storeRoot) {
|
|
96
|
+
let file;
|
|
97
|
+
try {
|
|
98
|
+
file = authorSessionFilePath(sessionId, storeRoot);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (!existsSync(file))
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
return parseAuthorSession(JSON.parse(readFileSync(file, "utf8")));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function saveAuthorSession(record, storeRoot) {
|
|
113
|
+
const next = { ...record, updatedAt: new Date().toISOString() };
|
|
114
|
+
writeRecord(next, storeRoot);
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
function draftTitle(draft) {
|
|
118
|
+
const name = typeof draft.name === "string" ? draft.name.trim() : "";
|
|
119
|
+
return name || "Untitled draft";
|
|
120
|
+
}
|
|
121
|
+
function draftHasWork(record) {
|
|
122
|
+
if (record.transcript.length > 0)
|
|
123
|
+
return true;
|
|
124
|
+
for (const key of ["name", "instructions", "description"]) {
|
|
125
|
+
const value = record.draft[key];
|
|
126
|
+
if (typeof value === "string" && value.trim())
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
/** Unsaved author sessions, newest first. Empty shells with no name and no chat are left out. */
|
|
132
|
+
export function listAuthorDrafts(storeRoot = globalStageflowHome()) {
|
|
133
|
+
const dir = authorSessionsDir(storeRoot);
|
|
134
|
+
if (!existsSync(dir))
|
|
135
|
+
return [];
|
|
136
|
+
const drafts = [];
|
|
137
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
138
|
+
if (!entry.isDirectory())
|
|
139
|
+
continue;
|
|
140
|
+
const record = readAuthorSession(entry.name, storeRoot);
|
|
141
|
+
if (!record || !draftHasWork(record))
|
|
142
|
+
continue;
|
|
143
|
+
drafts.push({
|
|
144
|
+
id: record.id,
|
|
145
|
+
name: draftTitle(record.draft),
|
|
146
|
+
updatedAt: record.updatedAt,
|
|
147
|
+
messages: record.transcript.length,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
drafts.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0));
|
|
151
|
+
return drafts;
|
|
152
|
+
}
|
|
@@ -18,6 +18,8 @@ export declare class AgentChatController {
|
|
|
18
18
|
stop(sessionId: string): boolean;
|
|
19
19
|
/** Let the runtime drop what it holds for a session (model session, isolated MCP processes). Never throws. */
|
|
20
20
|
release(sessionId: string): Promise<void>;
|
|
21
|
+
/** An eval conversation is over: stop any running turn, release the runtime session, drop its in-memory overrides. */
|
|
22
|
+
closeEvalSession(projectRoot: string, sessionId: string): Promise<void>;
|
|
21
23
|
/** Stop a turn that is still opening its session (client left before the session id arrived). */
|
|
22
24
|
stopOpening(agentId: string): boolean;
|
|
23
25
|
}
|
package/dist/agents/chatTurn.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AgentError } from "./errors.js";
|
|
2
|
+
import { EVAL_SESSION_SOURCE, isEvalSession } from "./evalSession.js";
|
|
1
3
|
import { AgentConversation } from "./sessions.js";
|
|
2
4
|
/**
|
|
3
5
|
* Serializes turns per session (or per agent while a session is being
|
|
@@ -19,7 +21,12 @@ export class AgentChatController {
|
|
|
19
21
|
this.turnChains.set(key, run.then(() => undefined, () => undefined));
|
|
20
22
|
return run;
|
|
21
23
|
}
|
|
22
|
-
send(input) {
|
|
24
|
+
async send(input) {
|
|
25
|
+
if (input.sessionId && input.source !== EVAL_SESSION_SOURCE) {
|
|
26
|
+
const { session } = await this.options.service.getSession(input.projectRoot, input.sessionId);
|
|
27
|
+
if (isEvalSession(session))
|
|
28
|
+
throw new AgentError("agent.session_eval_readonly", "Eval sessions are read only");
|
|
29
|
+
}
|
|
23
30
|
return this.runExclusive(input.sessionId ?? `agent:${input.agentId}`, async () => {
|
|
24
31
|
const chat = new AgentConversation(this.options);
|
|
25
32
|
if (input.sessionId)
|
|
@@ -66,6 +73,15 @@ export class AgentChatController {
|
|
|
66
73
|
this.options.logError?.(`releasing agent session ${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
74
|
}
|
|
68
75
|
}
|
|
76
|
+
/** An eval conversation is over: stop any running turn, release the runtime session, drop its in-memory overrides. */
|
|
77
|
+
async closeEvalSession(projectRoot, sessionId) {
|
|
78
|
+
const { session } = await this.options.service.getSession(projectRoot, sessionId);
|
|
79
|
+
if (!isEvalSession(session))
|
|
80
|
+
throw new AgentError("agent.invalid", `Session ${sessionId} is not an eval session`);
|
|
81
|
+
this.stop(sessionId);
|
|
82
|
+
await this.release(sessionId);
|
|
83
|
+
this.options.service.forgetEvalSession(sessionId);
|
|
84
|
+
}
|
|
69
85
|
/** Stop a turn that is still opening its session (client left before the session id arrived). */
|
|
70
86
|
stopOpening(agentId) {
|
|
71
87
|
const chat = this.activeByAgent.get(agentId);
|
package/dist/agents/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentIssue } from "../config/loadAgent.js";
|
|
2
|
-
export type AgentErrorCode = "agent.invalid" | "agent.not_found" | "agent.exists" | "agent.archived" | "agent.disabled" | "agent.conflict" | "agent.revision_not_found" | "agent.session_not_found" | "agent.session_busy" | "agent.tool_not_granted" | "agent.approval_not_required" | "agent.approval_not_pending" | "agent.approval_expired" | "agent.approval_forbidden" | "agent.unbound" | "agent.sender_not_allowed" | "agent.outbound_unknown" | "agent.trace_metadata_body" | "agent.guardrail_blocked" | "agent.delegate_not_allowed" | "agent.delegate_depth_exceeded" | "agent.sandbox_unavailable" | "agent.limit_reached" | "agent.model_timeout" | "agent.backend_unavailable" | "agent.approval_denied";
|
|
2
|
+
export type AgentErrorCode = "agent.invalid" | "agent.not_found" | "agent.exists" | "agent.archived" | "agent.disabled" | "agent.conflict" | "agent.revision_not_found" | "agent.session_not_found" | "agent.session_busy" | "agent.session_eval_readonly" | "agent.tool_not_granted" | "agent.approval_not_required" | "agent.approval_not_pending" | "agent.approval_expired" | "agent.approval_forbidden" | "agent.unbound" | "agent.sender_not_allowed" | "agent.outbound_unknown" | "agent.trace_metadata_body" | "agent.guardrail_blocked" | "agent.delegate_not_allowed" | "agent.delegate_depth_exceeded" | "agent.sandbox_unavailable" | "agent.limit_reached" | "agent.model_timeout" | "agent.backend_unavailable" | "agent.approval_denied";
|
|
3
3
|
/** Domain failure with a stable code; the HTTP, CLI and MCP layers map it, they don't parse messages. */
|
|
4
4
|
export declare class AgentError extends Error {
|
|
5
5
|
readonly code: AgentErrorCode;
|
package/dist/agents/errors.js
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../config/loadAgent.js";
|
|
2
|
+
import type { GateDecision, ToolCall } from "./runtime/types.js";
|
|
3
|
+
export type EvalToolPolicy = "real" | "read_only" | "mocked";
|
|
4
|
+
export type EvalApprovalPolicy = "deny" | "auto_approve_read" | "record_only";
|
|
5
|
+
export type EvalPolicyOutcome = "ran" | "stubbed" | "mocked" | "would_ask_first" | "denied";
|
|
6
|
+
export type EvalGatePolicy = {
|
|
7
|
+
tools: EvalToolPolicy;
|
|
8
|
+
approvals: EvalApprovalPolicy;
|
|
9
|
+
/** Grant ids the eval author declares read-only (`<server>.<tool>`, `<server>.*`, or a builtin/Stageflow grant id). */
|
|
10
|
+
readOnlyTools: string[];
|
|
11
|
+
/** Canned results by tool grant id: `{ result }` or `{ error }`. */
|
|
12
|
+
mocks: Record<string, {
|
|
13
|
+
result?: unknown;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export type EvalToolCallRecord = {
|
|
18
|
+
id: string;
|
|
19
|
+
turn: number;
|
|
20
|
+
name: string;
|
|
21
|
+
args: Record<string, unknown>;
|
|
22
|
+
policyOutcome: EvalPolicyOutcome;
|
|
23
|
+
status: "ok" | "error" | "not_run";
|
|
24
|
+
result?: string;
|
|
25
|
+
};
|
|
26
|
+
export type EvalGate = {
|
|
27
|
+
readonly policy: EvalGatePolicy;
|
|
28
|
+
/** Set when tools run for real; the console shows it as a warning. */
|
|
29
|
+
readonly warning: string | undefined;
|
|
30
|
+
decide(call: ToolCall, definition?: AgentDefinition): GateDecision;
|
|
31
|
+
observeFinished(call: ToolCall, result: {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
output: string;
|
|
34
|
+
}): void;
|
|
35
|
+
recordOf(callId: string): EvalToolCallRecord | undefined;
|
|
36
|
+
records(): EvalToolCallRecord[];
|
|
37
|
+
};
|
|
38
|
+
export declare const APPROVAL_NOT_GRANTED: (tool: string) => string;
|
|
39
|
+
export declare const STUB_RESULT: (tool: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* The eval-only tool gate. Order: denied (not granted, forbidden path, operator-only, always-blocked), then the approval
|
|
42
|
+
* policy for ask-first tools, then the tool policy. A canned result can reach the model only as the reason of a blocked
|
|
43
|
+
* call, which the runtime turns into an error-flagged tool result.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createEvalGate(options: {
|
|
46
|
+
policy: EvalGatePolicy;
|
|
47
|
+
definition: AgentDefinition;
|
|
48
|
+
/** Called when a call is decided, and again when a call that ran finishes (same id; the later record wins). */
|
|
49
|
+
onCall?: (record: EvalToolCallRecord) => void;
|
|
50
|
+
/** The conversation turn a call belongs to; defaults to 0. */
|
|
51
|
+
turn?: () => number;
|
|
52
|
+
projectRoot?: string;
|
|
53
|
+
stageflowHome?: string;
|
|
54
|
+
}): EvalGate;
|
|
55
|
+
/** Which eval gate governs which eval session. An eval session with no entry is denied every tool. */
|
|
56
|
+
export declare class EvalGateRegistry {
|
|
57
|
+
private readonly gates;
|
|
58
|
+
set(sessionId: string, gate: EvalGate): void;
|
|
59
|
+
get(sessionId: string): EvalGate | undefined;
|
|
60
|
+
delete(sessionId: string): void;
|
|
61
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { redactSecrets } from "../agent/streamLogRedact.js";
|
|
2
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
3
|
+
import { forbiddenWriteTarget, isToolGranted, requiresAskFirst } from "./policy.js";
|
|
4
|
+
import { classifyTool } from "./toolRisk.js";
|
|
5
|
+
export const APPROVAL_NOT_GRANTED = (tool) => `Approval for ${tool} was not granted in this evaluation. Continue without it.`;
|
|
6
|
+
export const STUB_RESULT = (tool) => `${tool} was not executed in this evaluation.`;
|
|
7
|
+
const ALWAYS_BLOCKED = new Set([
|
|
8
|
+
"stageflow.run_eval",
|
|
9
|
+
"stageflow.agent_chat",
|
|
10
|
+
"stageflow.fire_trigger",
|
|
11
|
+
"stageflow.sort_eval_failures",
|
|
12
|
+
"stageflow.cancel_eval_run",
|
|
13
|
+
]);
|
|
14
|
+
const BLOCKED_UNLESS_REAL = new Set(["stageflow.probe_project_mcp"]);
|
|
15
|
+
function redactedArgs(input) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(redactSecrets(JSON.stringify(input)));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function mockText(mock) {
|
|
24
|
+
if (mock.error !== undefined)
|
|
25
|
+
return mock.error;
|
|
26
|
+
return typeof mock.result === "string" ? mock.result : JSON.stringify(mock.result ?? null);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The eval-only tool gate. Order: denied (not granted, forbidden path, operator-only, always-blocked), then the approval
|
|
30
|
+
* policy for ask-first tools, then the tool policy. A canned result can reach the model only as the reason of a blocked
|
|
31
|
+
* call, which the runtime turns into an error-flagged tool result.
|
|
32
|
+
*/
|
|
33
|
+
export function createEvalGate(options) {
|
|
34
|
+
const { policy } = options;
|
|
35
|
+
const byId = new Map();
|
|
36
|
+
const emit = (record) => {
|
|
37
|
+
byId.set(record.id, record);
|
|
38
|
+
options.onCall?.(record);
|
|
39
|
+
};
|
|
40
|
+
const decide = (call, definition = options.definition) => {
|
|
41
|
+
const base = { id: call.id, turn: options.turn?.() ?? 0, name: call.tool, args: redactedArgs(call.input) };
|
|
42
|
+
const refuse = (policyOutcome, reason) => {
|
|
43
|
+
emit({ ...base, policyOutcome, status: "not_run", result: reason });
|
|
44
|
+
return { allow: false, reason };
|
|
45
|
+
};
|
|
46
|
+
if (!isToolGranted(definition, call.tool))
|
|
47
|
+
return refuse("denied", `Tool ${call.tool} is not granted to this agent.`);
|
|
48
|
+
if (options.projectRoot && forbiddenWriteTarget(options.projectRoot, options.stageflowHome ?? globalStageflowHome(), call.tool, call.input) !== undefined) {
|
|
49
|
+
return refuse("denied", "Agent definitions and Stageflow state cannot be written by an agent.");
|
|
50
|
+
}
|
|
51
|
+
const kind = classifyTool(definition, call.tool, policy.readOnlyTools);
|
|
52
|
+
const blocked = kind.class === "operator_only" ||
|
|
53
|
+
ALWAYS_BLOCKED.has(call.tool) ||
|
|
54
|
+
(policy.tools !== "real" && (BLOCKED_UNLESS_REAL.has(call.tool) || kind.class === "sub_agent"));
|
|
55
|
+
if (blocked)
|
|
56
|
+
return refuse("denied", `${call.tool} cannot run in an evaluation.`);
|
|
57
|
+
if (requiresAskFirst(definition, call.tool)) {
|
|
58
|
+
if (policy.approvals === "deny")
|
|
59
|
+
return refuse("denied", APPROVAL_NOT_GRANTED(call.tool));
|
|
60
|
+
if (policy.approvals === "record_only" || !kind.readOnly)
|
|
61
|
+
return refuse("would_ask_first", APPROVAL_NOT_GRANTED(call.tool));
|
|
62
|
+
}
|
|
63
|
+
const mock = policy.mocks[call.tool];
|
|
64
|
+
if (policy.tools === "real" || (kind.readOnly && !(policy.tools === "mocked" && mock))) {
|
|
65
|
+
emit({ ...base, policyOutcome: "ran", status: "ok" });
|
|
66
|
+
return { allow: true };
|
|
67
|
+
}
|
|
68
|
+
if (mock) {
|
|
69
|
+
const text = mockText(mock);
|
|
70
|
+
emit({ ...base, policyOutcome: "mocked", status: mock.error !== undefined ? "error" : "ok", result: redactSecrets(text) });
|
|
71
|
+
return { allow: false, reason: text };
|
|
72
|
+
}
|
|
73
|
+
const text = STUB_RESULT(call.tool);
|
|
74
|
+
emit({ ...base, policyOutcome: "stubbed", status: "not_run", result: text });
|
|
75
|
+
return { allow: false, reason: text };
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
policy,
|
|
79
|
+
warning: policy.tools === "real" ? "Tools run for real in this evaluation: they can change files, call services and spend money." : undefined,
|
|
80
|
+
decide,
|
|
81
|
+
observeFinished(call, result) {
|
|
82
|
+
const record = byId.get(call.id);
|
|
83
|
+
if (!record || record.policyOutcome !== "ran")
|
|
84
|
+
return;
|
|
85
|
+
emit({ ...record, status: result.ok ? "ok" : "error", result: redactSecrets(result.output) });
|
|
86
|
+
},
|
|
87
|
+
recordOf: (callId) => byId.get(callId),
|
|
88
|
+
records: () => [...byId.values()],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Which eval gate governs which eval session. An eval session with no entry is denied every tool. */
|
|
92
|
+
export class EvalGateRegistry {
|
|
93
|
+
gates = new Map();
|
|
94
|
+
set(sessionId, gate) {
|
|
95
|
+
this.gates.set(sessionId, gate);
|
|
96
|
+
}
|
|
97
|
+
get(sessionId) {
|
|
98
|
+
return this.gates.get(sessionId);
|
|
99
|
+
}
|
|
100
|
+
delete(sessionId) {
|
|
101
|
+
this.gates.delete(sessionId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../config/loadAgent.js";
|
|
2
|
+
export declare const EVAL_SESSION_SOURCE = "eval";
|
|
3
|
+
export type EvalSessionOverrides = {
|
|
4
|
+
/** Open this revision instead of the current one. Pinned for the life of the session. */
|
|
5
|
+
revision?: number;
|
|
6
|
+
definition?: {
|
|
7
|
+
model?: Partial<AgentDefinition["model"]>;
|
|
8
|
+
limits?: Partial<AgentDefinition["limits"]>;
|
|
9
|
+
observability?: Partial<AgentDefinition["observability"]>;
|
|
10
|
+
evals?: Partial<AgentDefinition["evals"]>;
|
|
11
|
+
};
|
|
12
|
+
limits?: {
|
|
13
|
+
max_turns?: number;
|
|
14
|
+
session_minutes?: number;
|
|
15
|
+
cost_usd?: number | null;
|
|
16
|
+
};
|
|
17
|
+
capture?: AgentDefinition["observability"]["capture"];
|
|
18
|
+
};
|
|
19
|
+
export declare function isEvalSession(session: {
|
|
20
|
+
source: string;
|
|
21
|
+
}): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The definition an eval session runs on: the pinned revision with the eval's model, limits and capture, and with
|
|
24
|
+
* every side effect off (always-on, OTLP push, groundedness judge). Capture defaults to full so tool calls are recorded.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyEvalOverrides(base: AgentDefinition, overrides: EvalSessionOverrides): AgentDefinition;
|