stageflow 0.42.0 → 0.46.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 +12 -3
- package/dist/agent/activity.d.ts +18 -0
- package/dist/agent/claudeAdapter.js +7 -0
- package/dist/agent/claudeTools.d.ts +27 -18
- package/dist/agent/claudeTools.js +54 -15
- package/dist/agent/compactionEvents.d.ts +34 -0
- package/dist/agent/compactionEvents.js +37 -0
- package/dist/agent/openRouterRouting.d.ts +7 -0
- package/dist/agent/openRouterRouting.js +23 -0
- package/dist/agent/piAdapter.d.ts +6 -0
- package/dist/agent/piAdapter.js +128 -27
- package/dist/agent/piSettings.d.ts +7 -0
- package/dist/agent/piSettings.js +9 -0
- package/dist/agent/sandboxPiTools.d.ts +3 -1
- package/dist/agent/sandboxPiTools.js +8 -2
- package/dist/agent/stageTurnDriver.d.ts +11 -2
- package/dist/agent/stageTurnDriver.js +45 -4
- package/dist/agent/toolOutputBudget.d.ts +15 -1
- package/dist/agent/toolOutputBudget.js +18 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
- package/dist/agent/toolOutputBudgetExtension.js +40 -3
- package/dist/agents/authorChat.d.ts +1 -0
- package/dist/agents/authorChat.js +7 -0
- package/dist/agents/authorSessionStore.d.ts +3 -0
- package/dist/agents/authorSessionStore.js +17 -2
- package/dist/agents/evalGate.js +1 -0
- package/dist/agents/hostContext.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.d.ts +5 -1
- package/dist/agents/runtime/piRuntime.js +46 -12
- package/dist/agents/service.js +3 -2
- package/dist/agents/sessions.js +3 -1
- package/dist/cli/evalCommand.d.ts +1 -1
- package/dist/cli/evalCommand.js +39 -2
- package/dist/cli/hostClient.d.ts +6 -0
- package/dist/cli/hostClient.js +4 -0
- package/dist/compaction/directives.d.ts +18 -0
- package/dist/compaction/directives.js +119 -0
- package/dist/compaction/extension.d.ts +20 -0
- package/dist/compaction/extension.js +78 -0
- package/dist/compaction/mask.d.ts +18 -0
- package/dist/compaction/mask.js +155 -0
- package/dist/compaction/pin.d.ts +21 -0
- package/dist/compaction/pin.js +270 -0
- package/dist/compaction/scrub.d.ts +7 -0
- package/dist/compaction/scrub.js +26 -0
- package/dist/compaction/sessionWiring.d.ts +22 -0
- package/dist/compaction/sessionWiring.js +26 -0
- package/dist/compaction/summarize.d.ts +79 -0
- package/dist/compaction/summarize.js +171 -0
- package/dist/compaction/toolPolicy.d.ts +3 -0
- package/dist/compaction/toolPolicy.js +33 -0
- package/dist/compaction/types.d.ts +45 -0
- package/dist/compaction/types.js +7 -0
- package/dist/compaction/workingState.d.ts +56 -0
- package/dist/compaction/workingState.js +198 -0
- package/dist/config/createPipeline.js +2 -0
- package/dist/config/createTrigger.js +7 -1
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
- package/dist/config/ensureCatalogScanRoot.js +6 -0
- package/dist/config/evalCatalog.js +30 -2
- package/dist/config/githubTriggerAction.d.ts +8 -0
- package/dist/config/githubTriggerAction.js +29 -0
- package/dist/config/hostConfig.js +2 -0
- package/dist/config/loadEval.d.ts +3 -1
- package/dist/config/loadEval.js +96 -10
- package/dist/config/pipelineNeeds.d.ts +2 -0
- package/dist/config/pipelineNeeds.js +16 -3
- package/dist/config/pipelineRoute.js +5 -4
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +12 -0
- package/dist/config/validateEval.js +60 -3
- package/dist/config/writeEval.js +2 -2
- package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
- package/dist/envelope/assertEmittedArtifacts.js +43 -0
- package/dist/envelope/preEmitChecks.d.ts +3 -0
- package/dist/evals/calibration.d.ts +21 -0
- package/dist/evals/calibration.js +69 -0
- package/dist/evals/calibrationSuggest.d.ts +29 -0
- package/dist/evals/calibrationSuggest.js +72 -0
- package/dist/evals/calibrator.d.ts +46 -0
- package/dist/evals/calibrator.js +148 -0
- package/dist/evals/compare.d.ts +1 -1
- package/dist/evals/compare.js +3 -0
- package/dist/evals/constants.d.ts +7 -3
- package/dist/evals/constants.js +7 -1
- package/dist/evals/engine/FakeEngine.d.ts +2 -1
- package/dist/evals/engine/FakeEngine.js +9 -3
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +4 -2
- package/dist/evals/engine/promptfoo/generate.d.ts +3 -1
- package/dist/evals/engine/promptfoo/generate.js +43 -23
- package/dist/evals/engine/promptfoo/judgeProviders.js +2 -1
- package/dist/evals/engine/promptfoo/normalize.d.ts +4 -1
- package/dist/evals/engine/promptfoo/normalize.js +16 -3
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +3 -1
- package/dist/evals/engine/promptfoo/rubricPrompt.js +15 -5
- package/dist/evals/errors.d.ts +7 -0
- package/dist/evals/errors.js +10 -0
- package/dist/evals/judgeProviders.d.ts +6 -0
- package/dist/evals/judgeProviders.js +14 -0
- package/dist/evals/judged.d.ts +7 -0
- package/dist/evals/judged.js +7 -0
- package/dist/evals/manager.d.ts +27 -8
- package/dist/evals/manager.js +73 -12
- package/dist/evals/plan.d.ts +10 -1
- package/dist/evals/plan.js +33 -13
- package/dist/evals/planInternalAgent.d.ts +19 -0
- package/dist/evals/planInternalAgent.js +201 -0
- package/dist/evals/read/api.d.ts +18 -0
- package/dist/evals/read/api.js +10 -1
- package/dist/evals/read/labelQueue.d.ts +45 -0
- package/dist/evals/read/labelQueue.js +83 -0
- package/dist/evals/read/list.js +10 -1
- package/dist/evals/read/runs.d.ts +5 -0
- package/dist/evals/read/runs.js +7 -1
- package/dist/evals/runner/caseRunner.d.ts +2 -1
- package/dist/evals/runner/caseRunner.js +16 -5
- package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
- package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
- package/dist/evals/snapshot.d.ts +10 -2
- package/dist/evals/snapshot.js +8 -3
- package/dist/evals/stats.d.ts +40 -0
- package/dist/evals/stats.js +69 -0
- package/dist/evals/storeTypes.d.ts +42 -0
- package/dist/evals/types.d.ts +25 -2
- package/dist/evals/types.js +4 -1
- package/dist/evals/votes.d.ts +10 -0
- package/dist/evals/votes.js +60 -0
- package/dist/guide/cardContract.d.ts +133 -0
- package/dist/guide/cardContract.js +156 -0
- package/dist/guide/chat.d.ts +45 -0
- package/dist/guide/chat.js +378 -0
- package/dist/guide/knowledge/index.d.ts +14 -0
- package/dist/guide/knowledge/index.js +109 -0
- package/dist/guide/knowledge/topics/agents.d.ts +2 -0
- package/dist/guide/knowledge/topics/agents.js +58 -0
- package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
- package/dist/guide/knowledge/topics/bestPractices.js +67 -0
- package/dist/guide/knowledge/topics/connections.d.ts +2 -0
- package/dist/guide/knowledge/topics/connections.js +66 -0
- package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
- package/dist/guide/knowledge/topics/consoleMap.js +55 -0
- package/dist/guide/knowledge/topics/evals.d.ts +2 -0
- package/dist/guide/knowledge/topics/evals.js +72 -0
- package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
- package/dist/guide/knowledge/topics/limitations.js +84 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
- package/dist/guide/knowledge/topics/memories.d.ts +2 -0
- package/dist/guide/knowledge/topics/memories.js +63 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
- package/dist/guide/knowledge/topics/overview.d.ts +2 -0
- package/dist/guide/knowledge/topics/overview.js +56 -0
- package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
- package/dist/guide/knowledge/topics/pipelines.js +93 -0
- package/dist/guide/knowledge/topics/stages.d.ts +2 -0
- package/dist/guide/knowledge/topics/stages.js +54 -0
- package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
- package/dist/guide/knowledge/topics/tasks.js +52 -0
- package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
- package/dist/guide/knowledge/topics/triggers.js +68 -0
- package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
- package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
- package/dist/guide/knowledgeTools.d.ts +2 -0
- package/dist/guide/knowledgeTools.js +56 -0
- package/dist/guide/showUi.d.ts +6 -0
- package/dist/guide/showUi.js +546 -0
- package/dist/guide/threadStore.d.ts +48 -0
- package/dist/guide/threadStore.js +189 -0
- package/dist/guide/writeRoot.d.ts +11 -0
- package/dist/guide/writeRoot.js +70 -0
- package/dist/hosted/translate.js +2 -0
- package/dist/mcp/agentTools.js +12 -0
- package/dist/mcp/catalogTools.js +18 -20
- package/dist/mcp/catalogWriteTools.d.ts +8 -0
- package/dist/mcp/catalogWriteTools.js +225 -0
- package/dist/mcp/connectionTools.d.ts +3 -0
- package/dist/mcp/connectionTools.js +85 -0
- package/dist/mcp/controlTools.js +34 -1
- package/dist/mcp/deps.d.ts +9 -0
- package/dist/mcp/evalTools.js +723 -21
- package/dist/mcp/memoryHostTools.d.ts +3 -0
- package/dist/mcp/memoryHostTools.js +72 -0
- package/dist/mcp/pipelineAuthorTools.js +14 -1
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +6 -0
- package/dist/mcp/triggerTools.js +77 -31
- package/dist/operatorAgent/evalGate.d.ts +22 -0
- package/dist/operatorAgent/evalGate.js +77 -0
- package/dist/operatorAgent/fakeHost.js +10 -2
- package/dist/operatorAgent/host.js +18 -2
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/internalAgents.d.ts +11 -0
- package/dist/operatorAgent/internalAgents.js +13 -0
- package/dist/operatorAgent/piModel.d.ts +5 -1
- package/dist/operatorAgent/piModel.js +15 -10
- package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
- package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
- package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
- package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
- package/dist/operatorAgent/profiles/guideTools.js +123 -0
- package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
- package/dist/operatorAgent/profiles/stageflowGuide.js +133 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
- package/dist/operatorAgent/types.d.ts +23 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.d.ts +13 -1
- package/dist/runstore/retention.d.ts +2 -1
- package/dist/runstore/retention.js +3 -7
- package/dist/runstore/runStatusClock.d.ts +17 -0
- package/dist/runstore/runStatusClock.js +19 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +70 -45
- package/dist/runstore/sqlite/calibrationStore.d.ts +10 -0
- package/dist/runstore/sqlite/calibrationStore.js +106 -0
- package/dist/runstore/sqlite/evalStore.d.ts +5 -0
- package/dist/runstore/sqlite/evalStore.js +7 -0
- package/dist/runstore/sqlite/migrations/018-eval-grader-labels.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/018-eval-grader-labels.js +43 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/runstore/workspaceLayout.js +6 -1
- package/dist/runtime/artifactDiskInspect.d.ts +14 -0
- package/dist/runtime/artifactDiskInspect.js +52 -0
- package/dist/runtime/artifactPath.d.ts +24 -0
- package/dist/runtime/artifactPath.js +84 -0
- package/dist/runtime/completionCheckRunner.js +15 -43
- package/dist/runtime/githubPollSource.d.ts +22 -4
- package/dist/runtime/githubPollSource.js +93 -8
- package/dist/runtime/priorArtifactGrants.d.ts +8 -0
- package/dist/runtime/priorArtifactGrants.js +105 -0
- package/dist/runtime/runActionEligibility.d.ts +46 -0
- package/dist/runtime/runActionEligibility.js +123 -0
- package/dist/runtime/runManager.d.ts +4 -1
- package/dist/runtime/runManager.js +52 -10
- package/dist/runtime/runRetryCoordinator.d.ts +2 -14
- package/dist/runtime/runRetryCoordinator.js +3 -67
- package/dist/runtime/sandboxContainer.d.ts +7 -0
- package/dist/runtime/sandboxContainer.js +48 -6
- package/dist/runtime/sandboxDesk.d.ts +2 -1
- package/dist/runtime/sandboxDesk.js +11 -12
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageRoots.d.ts +12 -0
- package/dist/runtime/stageSandbox.d.ts +2 -1
- package/dist/runtime/stageSandbox.js +18 -2
- package/dist/runtime/stageWorker.js +20 -0
- package/dist/server/agentAuthorRoutes.js +12 -3
- package/dist/server/bootstrap.d.ts +5 -0
- package/dist/server/bootstrap.js +14 -1
- package/dist/server/evalRoutes.js +251 -12
- package/dist/server/guideRoutes.d.ts +5 -0
- package/dist/server/guideRoutes.js +166 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.js +35 -1
- package/dist/server/internalAgentRoutes.d.ts +6 -0
- package/dist/server/internalAgentRoutes.js +12 -0
- package/dist/server/mcpHost.js +2 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +41 -1
- package/dist/toolkit/profiles.d.ts +5 -0
- package/dist/toolkit/profiles.js +12 -0
- package/dist/tools/askOperator.js +2 -1
- package/dist/tools/emitStageEnvelope.d.ts +17 -16
- package/dist/tools/emitStageEnvelope.js +46 -32
- package/dist/tools/stageToolNames.d.ts +4 -0
- package/dist/tools/stageToolNames.js +4 -0
- package/dist/tools/workingState.d.ts +40 -0
- package/dist/tools/workingState.js +108 -0
- package/dist/tools/writeStageArtifact.d.ts +0 -9
- package/dist/tools/writeStageArtifact.js +2 -1
- package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-BS2BBbCx.js} +2 -2
- package/dist/ui/assets/adapters-DPD-cAsy.js +17 -0
- package/dist/ui/assets/chat-BD1-VTT6.js +2 -0
- package/dist/ui/assets/chat-DXnQv9st.css +2 -0
- package/dist/ui/assets/main-AX5XNLtX.js +278 -0
- package/dist/ui/assets/{main-CHlmTNJe.css → main-CFX0_n8J.css} +1 -1
- package/dist/ui/chat.html +3 -4
- package/dist/ui/index.html +3 -4
- package/dist/workshop/chatTurn.js +1 -1
- package/dist/workshop/workshopPin.d.ts +3 -3
- package/dist/workshop/workshopPin.js +16 -15
- package/examples/compaction-stress/README.md +30 -0
- package/examples/compaction-stress/check-run.mjs +53 -0
- package/examples/compaction-stress/collect-evidence.yaml +60 -0
- package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
- package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
- package/examples/compaction-stress/generate-evidence.mjs +110 -0
- package/examples/compaction-stress/root-cause.yaml +26 -0
- package/examples/compaction-stress/stageflow.yaml +9 -0
- package/examples/compaction-stress/write-postmortem.yaml +40 -0
- package/examples/evals/README.md +18 -0
- package/examples/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/evals/guide-behavior.eval.yaml +35 -0
- package/examples/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/evals/guide-depth.eval.yaml +169 -0
- package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
- package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/memory/evals/guide-depth.eval.yaml +169 -0
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +1 -1
- package/skills/stageflow/references/evals.md +1 -0
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
- package/dist/ui/assets/chat-Df_wCpak.js +0 -5
- package/dist/ui/assets/chat-P7KQznHm.css +0 -2
- package/dist/ui/assets/main-CvVfxmMd.js +0 -271
package/dist/agent/piAdapter.js
CHANGED
|
@@ -33,7 +33,7 @@ import { existsSync } from "node:fs";
|
|
|
33
33
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { BROWSER_SKILL_NAME } from "../config/builtinSkills.js";
|
|
36
|
-
import { defineTool, ModelRuntime, resolveCliModel, SessionManager,
|
|
36
|
+
import { defineTool, ModelRuntime, resolveCliModel, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
37
37
|
import { StageMcpError } from "../config/resolveStageMcpServers.js";
|
|
38
38
|
import { attachIsolatedMcp, STAGEFLOW_PI_MCP_EXTENSION_NAME, UNREQUESTED_MCP_ADAPTER_TOOLS, } from "./piIsolatedMcp.js";
|
|
39
39
|
import { warmIsolatedMcpMetadata } from "./piIsolatedMcpProbe.js";
|
|
@@ -61,6 +61,7 @@ import { findProviderSupport } from "./providerSupport.js";
|
|
|
61
61
|
import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
|
|
62
62
|
import { createStageActivityObserver, } from "./activityObserver.js";
|
|
63
63
|
import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./port.js";
|
|
64
|
+
import { addCompactionUsage, compactionActivityEvent, } from "./compactionEvents.js";
|
|
64
65
|
import { addModelUsage, emptyStageUsage } from "../types/usage.js";
|
|
65
66
|
import { TIMEOUT_ABORT_TOOL_RESULT, composeTimeoutResumePrompt, stageTimeoutReason, } from "./stageTimeout.js";
|
|
66
67
|
import { globalStageflowHome } from "../project/globalHome.js";
|
|
@@ -68,6 +69,10 @@ import { isUsableAuthFile, STAGEFLOW_AGENT_AUTH_PATH_ENV, } from "../runtime/cre
|
|
|
68
69
|
import { SF_STAGE_WORKER } from "../runtime/stageWorkerProtocol.js";
|
|
69
70
|
import { durableRootFileToolDenial, STAGEFLOW_PATH_DENIED, } from "../runstore/workspaceLayout.js";
|
|
70
71
|
export { STAGEFLOW_PATH_DENIED };
|
|
72
|
+
import { createPiSettingsManager } from "./piSettings.js";
|
|
73
|
+
import { createSessionContextWiring } from "../compaction/sessionWiring.js";
|
|
74
|
+
import { ASK_OPERATOR_TOOL_NAME } from "../tools/stageToolNames.js";
|
|
75
|
+
import { WORKING_STATE_GUIDANCE } from "../compaction/workingState.js";
|
|
71
76
|
export { createSealedResourceLoader } from "./piSessionFactory.js";
|
|
72
77
|
export const STAGEFLOW_PATH_DENY_EXTENSION_NAME = "stageflow-path-deny";
|
|
73
78
|
const PATH_DENY_FILE_TOOLS = new Set(["read", "write", "edit"]);
|
|
@@ -102,7 +107,7 @@ export function createDurableRootPathDenyExtension(options) {
|
|
|
102
107
|
* `write_stage_artifact` when registered (always, for bound and unbound —
|
|
103
108
|
* bound/unbound only gates cwd / env bind).
|
|
104
109
|
*/
|
|
105
|
-
export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName =
|
|
110
|
+
export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = ASK_OPERATOR_TOOL_NAME, gateKinds) {
|
|
106
111
|
const tools = [
|
|
107
112
|
"read",
|
|
108
113
|
"bash",
|
|
@@ -425,11 +430,37 @@ function attachStageProgress(session, onActivity, usage, onAssistantTextDelta) {
|
|
|
425
430
|
writeStderr: true,
|
|
426
431
|
});
|
|
427
432
|
const verbose = readActivityVerbose();
|
|
433
|
+
let lastContextPercent = -1;
|
|
428
434
|
const unsubscribe = session.subscribe((event) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
435
|
+
const raw = event;
|
|
436
|
+
routeSessionEventToProgress(raw, { observer, verbose, usage });
|
|
437
|
+
if (raw.type === "compaction_end") {
|
|
438
|
+
lastContextPercent = -1;
|
|
439
|
+
const end = raw;
|
|
440
|
+
if (usage)
|
|
441
|
+
addCompactionUsage(usage, session.model?.id, end);
|
|
442
|
+
observer.onActivity(compactionActivityEvent(end, session.getContextUsage()?.tokens ?? null));
|
|
443
|
+
const after = end.result?.estimatedTokensAfter;
|
|
444
|
+
const window = session.getContextUsage()?.contextWindow ?? 0;
|
|
445
|
+
if (session.getContextUsage()?.tokens == null && typeof after === "number" && Number.isFinite(after) && window > 0) {
|
|
446
|
+
lastContextPercent = Math.min(100, Math.round((after / window) * 100));
|
|
447
|
+
observer.onActivity({ event: "context_usage", tokens: after, contextWindow: window, percent: lastContextPercent });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (raw.type !== "message_end" && raw.type !== "compaction_end")
|
|
451
|
+
return;
|
|
452
|
+
const context = session.getContextUsage();
|
|
453
|
+
if (context?.tokens == null || context.contextWindow <= 0)
|
|
454
|
+
return;
|
|
455
|
+
const percent = Math.min(100, Math.round((context.tokens / context.contextWindow) * 100));
|
|
456
|
+
if (percent === lastContextPercent)
|
|
457
|
+
return;
|
|
458
|
+
lastContextPercent = percent;
|
|
459
|
+
observer.onActivity({
|
|
460
|
+
event: "context_usage",
|
|
461
|
+
tokens: context.tokens,
|
|
462
|
+
contextWindow: context.contextWindow,
|
|
463
|
+
percent,
|
|
433
464
|
});
|
|
434
465
|
});
|
|
435
466
|
return () => {
|
|
@@ -437,6 +468,20 @@ function attachStageProgress(session, onActivity, usage, onAssistantTextDelta) {
|
|
|
437
468
|
observer.dispose();
|
|
438
469
|
};
|
|
439
470
|
}
|
|
471
|
+
function formatGrantedArtifacts(grants) {
|
|
472
|
+
if (grants === undefined || grants.length === 0) {
|
|
473
|
+
return "No prior artifacts are granted in this sandbox. Do not open stages/<id>/attempts/... host layouts.";
|
|
474
|
+
}
|
|
475
|
+
const lines = [
|
|
476
|
+
"Prior artifacts are read-only. Read the sandbox path on each line. A file grant is a file at that path. A directory grant is a directory. Do not open stages/<id>/attempts/... host layouts.",
|
|
477
|
+
];
|
|
478
|
+
for (const grant of grants) {
|
|
479
|
+
const name = grant.listedPath.split("/").pop() || grant.listedPath;
|
|
480
|
+
lines.push(`- ${name} (${grant.kind}) → ${grant.containerPath}`);
|
|
481
|
+
lines.push(` from ${grant.listedPath}`);
|
|
482
|
+
}
|
|
483
|
+
return lines.join("\n");
|
|
484
|
+
}
|
|
440
485
|
function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName) {
|
|
441
486
|
let emitHint = emitHintOverride ??
|
|
442
487
|
`Required final action: call ${emitToolName} exactly once with status, summary, and artifacts (run-relative paths).`;
|
|
@@ -480,14 +525,18 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
|
|
|
480
525
|
: "";
|
|
481
526
|
const artifactGuidance = sandboxed
|
|
482
527
|
? [
|
|
483
|
-
"Sandbox desk: /workspace. read,
|
|
528
|
+
"Sandbox desk: /workspace. read, write, and edit run only inside that desk.",
|
|
529
|
+
input.roots.readonlyDir
|
|
530
|
+
? `One folder is mounted read-only at ${input.roots.readonlyDir}. Read the files there. If a file you were given is not in that folder, stop and say so. Do not create stand-in documents.`
|
|
531
|
+
: "The host filesystem is not mounted.",
|
|
484
532
|
input.roots.checkoutRoot
|
|
485
533
|
? `Checkout: /workspace/checkout. The host checkout path is not mounted.`
|
|
486
534
|
: "This stage has no checkout. Scratch files go in /workspace/work.",
|
|
487
|
-
"
|
|
535
|
+
"Scratch space is /workspace/work. Desk /workspace/artifacts is not durable — bash or builtin writes there do not persist for emit or the next stage.",
|
|
488
536
|
artifactToolName !== undefined
|
|
489
|
-
? `
|
|
490
|
-
: `
|
|
537
|
+
? `Durable stage artifacts only via ${artifactToolName} (path relative to ${attemptArtifactsPath}). Use the returned run-relative path in ${emitToolName}.`
|
|
538
|
+
: `Durable stage artifacts belong under ${attemptArtifactsPath} relative to the run folder.`,
|
|
539
|
+
formatGrantedArtifacts(input.roots.artifactGrants),
|
|
491
540
|
].join("\n")
|
|
492
541
|
: input.roots.mode === "bound" && artifactToolName !== undefined
|
|
493
542
|
? [
|
|
@@ -569,6 +618,30 @@ export function composeStageUserPrompt(input, emitToolName, emitHintOverride, ar
|
|
|
569
618
|
}
|
|
570
619
|
return body;
|
|
571
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Pi stages carry the whole assignment (task, prior envelopes, envelope
|
|
623
|
+
* contract, guidance) in the system prompt: compaction never summarizes it.
|
|
624
|
+
*/
|
|
625
|
+
export function composeStagePiSystemPrompt(input, emitToolName, emitHintOverride, artifactToolName) {
|
|
626
|
+
return [
|
|
627
|
+
input.stage.system_prompt,
|
|
628
|
+
"",
|
|
629
|
+
"# Working state",
|
|
630
|
+
WORKING_STATE_GUIDANCE,
|
|
631
|
+
"",
|
|
632
|
+
"# Stage assignment",
|
|
633
|
+
buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName),
|
|
634
|
+
].join("\n");
|
|
635
|
+
}
|
|
636
|
+
export function composeStagePiKickoffPrompt(input) {
|
|
637
|
+
const body = "Begin this stage. Your task, inputs, constraints and the required final action are in your system prompt under \"Stage assignment\".";
|
|
638
|
+
if (input.stage.skill !== undefined)
|
|
639
|
+
return `/skill:${input.stage.skill} ${body}`;
|
|
640
|
+
if (input.browserSkillFilePath !== undefined && input.stage.browser !== undefined) {
|
|
641
|
+
return `/skill:${BROWSER_SKILL_NAME} ${body}`;
|
|
642
|
+
}
|
|
643
|
+
return body;
|
|
644
|
+
}
|
|
572
645
|
export function composeFeedbackResumePrompt(input) {
|
|
573
646
|
if (input.feedbackLoopContext === undefined) {
|
|
574
647
|
throw new Error("feedback_resume requires feedbackLoopContext");
|
|
@@ -744,7 +817,7 @@ export async function repairPrematureAskOperatorClosure(sessionFile) {
|
|
|
744
817
|
const message = entry.message;
|
|
745
818
|
if (!message || message.role !== "toolResult")
|
|
746
819
|
continue;
|
|
747
|
-
if (message.toolName !==
|
|
820
|
+
if (message.toolName !== ASK_OPERATOR_TOOL_NAME || message.isError !== true)
|
|
748
821
|
continue;
|
|
749
822
|
const content = message.content;
|
|
750
823
|
if (!Array.isArray(content))
|
|
@@ -876,8 +949,16 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
876
949
|
const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext, {
|
|
877
950
|
checks: input.stage.pre_emit_checks,
|
|
878
951
|
readQaTrail: input.readQaTrail,
|
|
952
|
+
artifactPresence: {
|
|
953
|
+
runWorkspaceDir: roots.runWorkspaceDir,
|
|
954
|
+
stageId: runtimeStageId(input),
|
|
955
|
+
attempt: roots.attempt ?? 1,
|
|
956
|
+
},
|
|
879
957
|
}, input.feedbackLoopEmitContext);
|
|
880
|
-
const emitTool = defineTool(
|
|
958
|
+
const emitTool = defineTool({
|
|
959
|
+
...emitDef,
|
|
960
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
961
|
+
});
|
|
881
962
|
const gateKinds = input.stage.gate_kinds;
|
|
882
963
|
const includeAsk = Array.isArray(gateKinds) && gateKinds.length > 0;
|
|
883
964
|
const askDef = includeAsk
|
|
@@ -886,13 +967,21 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
886
967
|
allowedKinds: gateKinds,
|
|
887
968
|
})
|
|
888
969
|
: undefined;
|
|
889
|
-
const askTool = askDef
|
|
970
|
+
const askTool = askDef
|
|
971
|
+
? defineTool({
|
|
972
|
+
...askDef,
|
|
973
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
974
|
+
})
|
|
975
|
+
: undefined;
|
|
890
976
|
const artifactDef = createWriteStageArtifactTool({
|
|
891
977
|
runWorkspaceDir: roots.runWorkspaceDir,
|
|
892
978
|
stageId: runtimeStageId(input),
|
|
893
979
|
attempt: roots.attempt ?? 1,
|
|
894
980
|
});
|
|
895
|
-
const artifactTool = defineTool(
|
|
981
|
+
const artifactTool = defineTool({
|
|
982
|
+
...artifactDef,
|
|
983
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
984
|
+
});
|
|
896
985
|
if (input.stage.skill !== undefined && input.skillFilePath === undefined) {
|
|
897
986
|
return {
|
|
898
987
|
ok: false,
|
|
@@ -926,9 +1015,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
926
1015
|
modelsPath: path.join(path.dirname(roots.authPath), "models.json"),
|
|
927
1016
|
}
|
|
928
1017
|
: undefined);
|
|
929
|
-
const settingsManager =
|
|
930
|
-
compaction: { enabled: false },
|
|
931
|
-
});
|
|
1018
|
+
const settingsManager = createPiSettingsManager();
|
|
932
1019
|
attached = await attachIsolatedMcp(input.resolvedMcpServers, {
|
|
933
1020
|
lifecycle: "eager",
|
|
934
1021
|
});
|
|
@@ -937,6 +1024,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
937
1024
|
restoreProvider?.();
|
|
938
1025
|
return { ok: false, reason };
|
|
939
1026
|
};
|
|
1027
|
+
const contextWiring = createSessionContextWiring({ workingState: true });
|
|
940
1028
|
const extensionFactories = [
|
|
941
1029
|
{
|
|
942
1030
|
name: STAGEFLOW_PATH_DENY_EXTENSION_NAME,
|
|
@@ -960,22 +1048,25 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
960
1048
|
runWorkspaceDir: roots.runWorkspaceDir,
|
|
961
1049
|
stageId: runtimeStageId(input),
|
|
962
1050
|
attempt: roots.attempt ?? 1,
|
|
1051
|
+
turnCapped: new Set(["read", "bash"]),
|
|
963
1052
|
exempt: new Set([
|
|
964
1053
|
"read",
|
|
965
1054
|
"bash",
|
|
1055
|
+
...contextWiring.budgetExemptNames,
|
|
966
1056
|
emitDef.name,
|
|
967
1057
|
artifactDef.name,
|
|
968
|
-
askDef?.name ??
|
|
1058
|
+
askDef?.name ?? ASK_OPERATOR_TOOL_NAME,
|
|
969
1059
|
]),
|
|
970
1060
|
}),
|
|
971
1061
|
},
|
|
1062
|
+
...contextWiring.extensions,
|
|
972
1063
|
...(attached.extensionFactories ?? []),
|
|
973
1064
|
];
|
|
974
1065
|
const loader = createSealedResourceLoader({
|
|
975
1066
|
cwd: roots.cwd,
|
|
976
1067
|
agentDir: roots.agentDir,
|
|
977
1068
|
settingsManager,
|
|
978
|
-
systemPrompt: input.
|
|
1069
|
+
systemPrompt: composeStagePiSystemPrompt(input, emitDef.name, provider?.emitToolHint?.(emitDef.name), artifactDef.name),
|
|
979
1070
|
additionalExtensionPaths,
|
|
980
1071
|
...(additionalSkillPaths.length > 0 ? { additionalSkillPaths } : {}),
|
|
981
1072
|
extensionFactories,
|
|
@@ -1020,7 +1111,8 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
1020
1111
|
const customTools = askTool
|
|
1021
1112
|
? [emitTool, askTool, artifactTool]
|
|
1022
1113
|
: [emitTool, artifactTool];
|
|
1023
|
-
const tools = resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ??
|
|
1114
|
+
const tools = resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ?? ASK_OPERATOR_TOOL_NAME, gateKinds);
|
|
1115
|
+
tools.push(...contextWiring.toolNames);
|
|
1024
1116
|
if (attached.extensionFactories !== undefined) {
|
|
1025
1117
|
const mcpTools = collectMcpExtensionToolNames(loader);
|
|
1026
1118
|
if (!mcpTools.ok) {
|
|
@@ -1030,12 +1122,18 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
1030
1122
|
}
|
|
1031
1123
|
if (input.channels && input.stage.channels?.some(channel => channel.operations.includes("send"))) {
|
|
1032
1124
|
const definition = createSendMessageTool(input.channels);
|
|
1033
|
-
customTools.push(defineTool(
|
|
1125
|
+
customTools.push(defineTool({
|
|
1126
|
+
...definition,
|
|
1127
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
1128
|
+
}));
|
|
1034
1129
|
tools.push(definition.name);
|
|
1035
1130
|
}
|
|
1036
1131
|
if (input.memory) {
|
|
1037
1132
|
for (const definition of memorySession(input.memory).tools) {
|
|
1038
|
-
customTools.push(defineTool(
|
|
1133
|
+
customTools.push(defineTool({
|
|
1134
|
+
...definition,
|
|
1135
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
1136
|
+
}));
|
|
1039
1137
|
tools.push(definition.name);
|
|
1040
1138
|
}
|
|
1041
1139
|
}
|
|
@@ -1050,7 +1148,10 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
1050
1148
|
];
|
|
1051
1149
|
for (const definition of definitions)
|
|
1052
1150
|
if (definition) {
|
|
1053
|
-
customTools.push(defineTool(
|
|
1151
|
+
customTools.push(defineTool({
|
|
1152
|
+
...definition,
|
|
1153
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
1154
|
+
}));
|
|
1054
1155
|
tools.push(definition.name);
|
|
1055
1156
|
}
|
|
1056
1157
|
}
|
|
@@ -1060,8 +1161,10 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
1060
1161
|
...(roots.containerDockerBin !== undefined
|
|
1061
1162
|
? { dockerBin: roots.containerDockerBin }
|
|
1062
1163
|
: {}),
|
|
1164
|
+
...(roots.readonlyDir !== undefined ? { readonlyDir: roots.readonlyDir } : {}),
|
|
1063
1165
|
};
|
|
1064
|
-
|
|
1166
|
+
const sampling = { type: "json_schema", strict: "prefer" };
|
|
1167
|
+
customTools.push(defineTool({ ...createSandboxReadTool(sandboxTools), constrainedSampling: sampling }), defineTool({ ...createSandboxWriteTool(sandboxTools), constrainedSampling: sampling }), defineTool({ ...createSandboxEditTool(sandboxTools), constrainedSampling: sampling }), defineTool({ ...createSandboxBashTool(sandboxTools), constrainedSampling: sampling }));
|
|
1065
1168
|
}
|
|
1066
1169
|
return {
|
|
1067
1170
|
sessionManager,
|
|
@@ -1073,7 +1176,6 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
1073
1176
|
emitDefName: emitDef.name,
|
|
1074
1177
|
...(askDef ? { askOperatorDefName: askDef.name } : {}),
|
|
1075
1178
|
artifactDefName: artifactDef.name,
|
|
1076
|
-
providerEmitHint: provider?.emitToolHint?.(emitDef.name),
|
|
1077
1179
|
restoreProvider,
|
|
1078
1180
|
capture,
|
|
1079
1181
|
usage,
|
|
@@ -1415,8 +1517,7 @@ export class PiAgentAdapter {
|
|
|
1415
1517
|
await session.prompt(composeTimeoutResumePrompt());
|
|
1416
1518
|
return;
|
|
1417
1519
|
}
|
|
1418
|
-
|
|
1419
|
-
await session.prompt(userPrompt);
|
|
1520
|
+
await session.prompt(composeStagePiKickoffPrompt(input));
|
|
1420
1521
|
});
|
|
1421
1522
|
},
|
|
1422
1523
|
resume: resumeWaiting
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Settings for every Pi session Stageflow starts. Auto-compaction is always on
|
|
4
|
+
* and not configurable: a long session compacts at the threshold, and recovers
|
|
5
|
+
* once from a provider context-overflow, instead of failing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createPiSettingsManager(): SettingsManager;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Settings for every Pi session Stageflow starts. Auto-compaction is always on
|
|
4
|
+
* and not configurable: a long session compacts at the threshold, and recovers
|
|
5
|
+
* once from a provider context-overflow, instead of failing.
|
|
6
|
+
*/
|
|
7
|
+
export function createPiSettingsManager() {
|
|
8
|
+
return SettingsManager.inMemory({ compaction: { enabled: true } });
|
|
9
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { type SandboxFileTarget } from "../runtime/sandboxFiles.js";
|
|
3
|
-
export type SandboxPiToolOptions = SandboxFileTarget
|
|
3
|
+
export type SandboxPiToolOptions = SandboxFileTarget & {
|
|
4
|
+
readonlyDir?: string;
|
|
5
|
+
};
|
|
4
6
|
export declare function createSandboxReadTool(options: SandboxPiToolOptions): {
|
|
5
7
|
name: string;
|
|
6
8
|
label: string;
|
|
@@ -89,9 +89,15 @@ export function createSandboxBashTool(options) {
|
|
|
89
89
|
return {
|
|
90
90
|
name: "bash",
|
|
91
91
|
label: "bash",
|
|
92
|
-
description:
|
|
92
|
+
description: options.readonlyDir
|
|
93
|
+
? `Execute a bash command in this stage's sandbox. The working directory is /workspace. One folder is mounted read-only at ${options.readonlyDir}. Read files there. If a file is missing, stop. Do not create a stand-in.`
|
|
94
|
+
: "Execute a bash command in this stage's sandbox. The working directory is /workspace. The host filesystem is not mounted.",
|
|
93
95
|
promptSnippet: "Execute bash commands in /workspace",
|
|
94
|
-
promptGuidelines: [
|
|
96
|
+
promptGuidelines: [
|
|
97
|
+
options.readonlyDir
|
|
98
|
+
? `The shell starts in /workspace. The only host folder you can read is ${options.readonlyDir}, and it is read-only. If a file is not there, stop. Do not create stand-in documents.`
|
|
99
|
+
: "The shell starts in /workspace. Do not use host paths.",
|
|
100
|
+
],
|
|
95
101
|
parameters: Type.Object({
|
|
96
102
|
command: Type.String({ description: "Shell command to execute" }),
|
|
97
103
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })),
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { EmitCapture } from "../tools/emitStageEnvelope.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Follow-up turns in a row, with no progress between them, sent when the agent ends its turn
|
|
4
|
+
* without an accepted envelope. A reminder turn that runs a tool call counts as progress and
|
|
5
|
+
* resets this budget.
|
|
6
|
+
*/
|
|
3
7
|
export declare const DEFAULT_EMIT_REMINDERS = 2;
|
|
8
|
+
/** Hard ceiling on reminders in one stage attempt, however much progress each one made. */
|
|
9
|
+
export declare const MAX_TOTAL_EMIT_REMINDERS = 20;
|
|
10
|
+
export declare const LEAKED_TOOL_CALL_ERROR = "the model wrote its tool call out as plain text instead of making it, even after reminders. This is usually a provider-side tool-call parsing problem (for example one OpenRouter backend serving the model). Run again, pin a different provider for this model, or choose another model.";
|
|
4
11
|
/** The slice of a Pi session the driver needs; a fake in tests. */
|
|
5
12
|
export type StageTurnSession = {
|
|
6
13
|
prompt(text: string): Promise<void>;
|
|
@@ -24,7 +31,9 @@ export declare function formatProviderError(text: string | undefined): string;
|
|
|
24
31
|
* model unavailable, or a transient error Pi already gave up retrying). Undefined otherwise.
|
|
25
32
|
*/
|
|
26
33
|
export declare function lastTurnProviderError(messages: readonly unknown[]): string | undefined;
|
|
27
|
-
|
|
34
|
+
/** True when the last assistant turn wrote a tool call out as plain text instead of making a real one. */
|
|
35
|
+
export declare function lastTurnLeakedToolCall(messages: readonly unknown[]): boolean;
|
|
36
|
+
export declare function emitReminderPrompt(emitToolName: string, attempt: number, max: number, rejectedEmit?: string, leakedToolCall?: boolean): string;
|
|
28
37
|
/**
|
|
29
38
|
* Called after a turn returns. While no envelope was accepted, nudge the agent with a short
|
|
30
39
|
* follow-up turn, up to `maxReminders` times. Stops at once when an envelope is captured,
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Follow-up turns in a row, with no progress between them, sent when the agent ends its turn
|
|
3
|
+
* without an accepted envelope. A reminder turn that runs a tool call counts as progress and
|
|
4
|
+
* resets this budget.
|
|
5
|
+
*/
|
|
2
6
|
export const DEFAULT_EMIT_REMINDERS = 2;
|
|
7
|
+
/** Hard ceiling on reminders in one stage attempt, however much progress each one made. */
|
|
8
|
+
export const MAX_TOTAL_EMIT_REMINDERS = 20;
|
|
3
9
|
const PROVIDER_ERROR_MAX_CHARS = 500;
|
|
10
|
+
export const LEAKED_TOOL_CALL_ERROR = "the model wrote its tool call out as plain text instead of making it, even after reminders. This is usually a provider-side tool-call parsing problem (for example one OpenRouter backend serving the model). Run again, pin a different provider for this model, or choose another model.";
|
|
4
11
|
function lastAssistant(messages) {
|
|
5
12
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6
13
|
const m = messages[i];
|
|
@@ -27,10 +34,37 @@ export function lastTurnProviderError(messages) {
|
|
|
27
34
|
function lastTurnAborted(messages) {
|
|
28
35
|
return lastAssistant(messages)?.stopReason === "aborted";
|
|
29
36
|
}
|
|
30
|
-
|
|
37
|
+
const LEAKED_TOOL_CALL = /<[||]?\s*tool[▁_ ]?calls?[▁_ ]?(begin|start)|<tool_call>|<function_calls?>/i;
|
|
38
|
+
/** True when the last assistant turn wrote a tool call out as plain text instead of making a real one. */
|
|
39
|
+
export function lastTurnLeakedToolCall(messages) {
|
|
40
|
+
const m = lastAssistant(messages);
|
|
41
|
+
if (!m || !Array.isArray(m.content))
|
|
42
|
+
return false;
|
|
43
|
+
return m.content.some((block) => {
|
|
44
|
+
if (block.type === "toolCall")
|
|
45
|
+
return false;
|
|
46
|
+
const text = typeof block.text === "string" ? block.text : typeof block.thinking === "string" ? block.thinking : "";
|
|
47
|
+
return LEAKED_TOOL_CALL.test(text);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Tool calls the agent made in the messages after `fromIndex`. */
|
|
51
|
+
function toolCallsSince(messages, fromIndex) {
|
|
52
|
+
let count = 0;
|
|
53
|
+
for (const raw of messages.slice(fromIndex)) {
|
|
54
|
+
const m = raw;
|
|
55
|
+
if (m?.role !== "assistant" || !Array.isArray(m.content))
|
|
56
|
+
continue;
|
|
57
|
+
count += m.content.filter((block) => block.type === "toolCall").length;
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
export function emitReminderPrompt(emitToolName, attempt, max, rejectedEmit, leakedToolCall = false) {
|
|
31
62
|
const lines = [
|
|
32
63
|
`Reminder ${attempt} of ${max}: your turn ended without an accepted ${emitToolName} call, so this stage has not finished.`,
|
|
33
64
|
];
|
|
65
|
+
if (leakedToolCall) {
|
|
66
|
+
lines.push(`Your last tool call was written out as plain text, so it was NOT run. Make it again as a real tool call.`);
|
|
67
|
+
}
|
|
34
68
|
if (rejectedEmit !== undefined) {
|
|
35
69
|
lines.push(`Your last ${emitToolName} call was rejected: ${rejectedEmit}`);
|
|
36
70
|
lines.push(`Fix that and call ${emitToolName} again.`);
|
|
@@ -49,16 +83,23 @@ export function emitReminderPrompt(emitToolName, attempt, max, rejectedEmit) {
|
|
|
49
83
|
export async function remindUntilEmitted(session, capture, options) {
|
|
50
84
|
const max = options.maxReminders ?? DEFAULT_EMIT_REMINDERS;
|
|
51
85
|
let reminders = 0;
|
|
86
|
+
let withoutProgress = 0;
|
|
52
87
|
for (;;) {
|
|
53
88
|
if (capture.envelope)
|
|
54
89
|
return { reminders };
|
|
55
90
|
const providerError = lastTurnProviderError(session.messages);
|
|
56
91
|
if (providerError !== undefined)
|
|
57
92
|
return { reminders, providerError };
|
|
58
|
-
if (options.shouldStop() || lastTurnAborted(session.messages)
|
|
93
|
+
if (options.shouldStop() || lastTurnAborted(session.messages))
|
|
59
94
|
return { reminders };
|
|
95
|
+
if (withoutProgress >= max || reminders >= MAX_TOTAL_EMIT_REMINDERS) {
|
|
96
|
+
return lastTurnLeakedToolCall(session.messages) ? { reminders, providerError: LEAKED_TOOL_CALL_ERROR } : { reminders };
|
|
60
97
|
}
|
|
61
98
|
reminders++;
|
|
62
|
-
|
|
99
|
+
withoutProgress++;
|
|
100
|
+
const before = session.messages.length;
|
|
101
|
+
await session.prompt(emitReminderPrompt(options.emitToolName, withoutProgress, max, capture.error, lastTurnLeakedToolCall(session.messages)));
|
|
102
|
+
if (toolCallsSince(session.messages, before) > 0)
|
|
103
|
+
withoutProgress = 0;
|
|
63
104
|
}
|
|
64
105
|
}
|
|
@@ -16,7 +16,7 @@ export type BudgetInput = {
|
|
|
16
16
|
knownTokens: number | null;
|
|
17
17
|
turnLedgerTokens: number;
|
|
18
18
|
};
|
|
19
|
-
export type SpillReason = "exceeds_remaining" | "exceeds_per_result_cap" | "unknown_usage_fallback" | "adapter_truncated";
|
|
19
|
+
export type SpillReason = "exceeds_remaining" | "exceeds_per_result_cap" | "unknown_usage_fallback" | "adapter_truncated" | "turn_cap_deferred";
|
|
20
20
|
export type BudgetDecision = {
|
|
21
21
|
kind: "inline";
|
|
22
22
|
estTokens: number;
|
|
@@ -27,6 +27,20 @@ export type BudgetDecision = {
|
|
|
27
27
|
};
|
|
28
28
|
export declare function estimateTokens(bytes: number): number;
|
|
29
29
|
export declare function decideToolOutput(input: BudgetInput): BudgetDecision;
|
|
30
|
+
export declare const TURN_TOTAL_MAX_FRACTION = 0.4;
|
|
31
|
+
export type TurnCapInput = {
|
|
32
|
+
resultTokens: number;
|
|
33
|
+
contextWindow: number;
|
|
34
|
+
knownTokens: number | null;
|
|
35
|
+
turnLedgerTokens: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Turn cap for tools whose output is kept inline (read, bash): the first result
|
|
39
|
+
* of a turn always passes so a lone call can make progress; later results in
|
|
40
|
+
* the same turn are deferred once the turn's total would pass the smaller of
|
|
41
|
+
* the remaining window (after the reserve) and TURN_TOTAL_MAX_FRACTION of it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function decideTurnCap(input: TurnCapInput): "inline" | "defer";
|
|
30
44
|
export declare function utf8ByteLength(text: string): number;
|
|
31
45
|
export declare function truncateUtf8(text: string, maxBytes: number): string;
|
|
32
46
|
export declare function tryParseJson(text: string): {
|
|
@@ -31,6 +31,24 @@ export function decideToolOutput(input) {
|
|
|
31
31
|
}
|
|
32
32
|
return { kind: "inline", estTokens };
|
|
33
33
|
}
|
|
34
|
+
export const TURN_TOTAL_MAX_FRACTION = 0.4;
|
|
35
|
+
/**
|
|
36
|
+
* Turn cap for tools whose output is kept inline (read, bash): the first result
|
|
37
|
+
* of a turn always passes so a lone call can make progress; later results in
|
|
38
|
+
* the same turn are deferred once the turn's total would pass the smaller of
|
|
39
|
+
* the remaining window (after the reserve) and TURN_TOTAL_MAX_FRACTION of it.
|
|
40
|
+
*/
|
|
41
|
+
export function decideTurnCap(input) {
|
|
42
|
+
const window = input.contextWindow;
|
|
43
|
+
if (input.knownTokens === null || !Number.isFinite(window) || window <= 0)
|
|
44
|
+
return "inline";
|
|
45
|
+
if (input.turnLedgerTokens <= 0)
|
|
46
|
+
return "inline";
|
|
47
|
+
const reserve = Math.max(RESERVE_MIN_TOKENS, RESERVE_FRACTION * window);
|
|
48
|
+
const remaining = window - input.knownTokens - reserve;
|
|
49
|
+
const allowed = Math.min(remaining, TURN_TOTAL_MAX_FRACTION * window);
|
|
50
|
+
return input.turnLedgerTokens + input.resultTokens > allowed ? "defer" : "inline";
|
|
51
|
+
}
|
|
34
52
|
export function utf8ByteLength(text) {
|
|
35
53
|
return Buffer.byteLength(text, "utf8");
|
|
36
54
|
}
|
|
@@ -16,6 +16,8 @@ export type ToolOutputBudgetOptions = {
|
|
|
16
16
|
stageId: string;
|
|
17
17
|
attempt: number;
|
|
18
18
|
exempt: ReadonlySet<string>;
|
|
19
|
+
/** Exempt tools that keep output inline but share the per-turn token cap (read, bash). */
|
|
20
|
+
turnCapped?: ReadonlySet<string>;
|
|
19
21
|
onSpill?: (event: ToolOutputSpillEvent) => void;
|
|
20
22
|
};
|
|
21
23
|
export declare function formatSpillNotice(spilled: SpilledToolOutput, estTokens: number, lostReason?: string): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildPreview, decideToolOutput, estimateTokens, IMAGE_BLOCK_EST_TOKENS, truncateUtf8, UNKNOWN_USAGE_INLINE_MAX_BYTES, utf8ByteLength, } from "./toolOutputBudget.js";
|
|
1
|
+
import { buildPreview, decideToolOutput, decideTurnCap, estimateTokens, IMAGE_BLOCK_EST_TOKENS, truncateUtf8, UNKNOWN_USAGE_INLINE_MAX_BYTES, utf8ByteLength, } from "./toolOutputBudget.js";
|
|
2
2
|
import { spillToolOutput } from "./toolOutputSpill.js";
|
|
3
3
|
import { discardAdapterSpill, readAdapterSpill } from "./adapterSpill.js";
|
|
4
4
|
export const STAGEFLOW_TOOL_OUTPUT_EXTENSION_NAME = "stageflow-tool-output";
|
|
@@ -37,11 +37,48 @@ export function createToolOutputBudgetExtension(options) {
|
|
|
37
37
|
}
|
|
38
38
|
return undefined;
|
|
39
39
|
});
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const capTurnTotal = (event, ctx) => {
|
|
41
|
+
const images = event.content.filter((block) => block.type === "image");
|
|
42
|
+
const resultTokens = estimateTokens(utf8ByteLength(joinText(event.content))) +
|
|
43
|
+
images.length * IMAGE_BLOCK_EST_TOKENS;
|
|
44
|
+
const usage = ctx.getContextUsage();
|
|
45
|
+
const verdict = decideTurnCap({
|
|
46
|
+
resultTokens,
|
|
47
|
+
contextWindow: usage?.contextWindow ?? 0,
|
|
48
|
+
knownTokens: usage?.tokens ?? null,
|
|
49
|
+
turnLedgerTokens,
|
|
50
|
+
});
|
|
51
|
+
if (verdict === "inline") {
|
|
52
|
+
turnLedgerTokens += resultTokens;
|
|
42
53
|
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const notice = event.toolName === "bash"
|
|
56
|
+
? `[bash output not returned: this turn's tool results already fill the context budget (this one is ~${resultTokens} tokens). The command already ran; do not run it again. Re-read its output with a read-only command (for example write it to a file earlier, or use grep/head/tail on the files it produced) in a later turn, and keep to one large call per turn.]`
|
|
57
|
+
: `[${event.toolName} output not returned: this turn's tool results already fill the context budget (this one is ~${resultTokens} tokens). Nothing was lost. Call ${event.toolName} again for this one on its own, in a later turn, and keep to one large ${event.toolName} call per turn.]`;
|
|
58
|
+
turnLedgerTokens += estimateTokens(utf8ByteLength(notice));
|
|
59
|
+
options.onSpill?.({
|
|
60
|
+
toolName: event.toolName,
|
|
61
|
+
toolCallId: event.toolCallId,
|
|
62
|
+
reason: "turn_cap_deferred",
|
|
63
|
+
originalBytes: utf8ByteLength(joinText(event.content)),
|
|
64
|
+
estTokens: resultTokens,
|
|
65
|
+
});
|
|
66
|
+
const previousDetails = event.details !== null && typeof event.details === "object"
|
|
67
|
+
? event.details
|
|
68
|
+
: {};
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: notice }],
|
|
71
|
+
details: { ...previousDetails, stageflowToolOutput: { deferred: true, estTokens: resultTokens } },
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
43
75
|
if (event.parentToolCallId !== undefined)
|
|
44
76
|
return undefined;
|
|
77
|
+
if (options.exempt.has(event.toolName)) {
|
|
78
|
+
if (!options.turnCapped?.has(event.toolName))
|
|
79
|
+
return undefined;
|
|
80
|
+
return capTurnTotal(event, ctx);
|
|
81
|
+
}
|
|
45
82
|
const images = event.content.filter((block) => block.type === "image");
|
|
46
83
|
turnLedgerTokens += images.length * IMAGE_BLOCK_EST_TOKENS;
|
|
47
84
|
const visible = joinText(event.content);
|
|
@@ -9,6 +9,7 @@ export type AgentAuthorTurnResult = {
|
|
|
9
9
|
summaryLines: SummaryLine[];
|
|
10
10
|
};
|
|
11
11
|
export declare function resetAgentAuthorSessionsForTests(): void;
|
|
12
|
+
export declare function forgetAgentAuthorSession(sessionId: string): void;
|
|
12
13
|
export declare function runAgentAuthorTurn(input: {
|
|
13
14
|
host: OperatorAgentHost;
|
|
14
15
|
projectRoot: string;
|
|
@@ -53,6 +53,13 @@ export function resetAgentAuthorSessionsForTests() {
|
|
|
53
53
|
session.close();
|
|
54
54
|
sessions.clear();
|
|
55
55
|
}
|
|
56
|
+
export function forgetAgentAuthorSession(sessionId) {
|
|
57
|
+
const session = sessions.get(sessionId);
|
|
58
|
+
if (!session)
|
|
59
|
+
return;
|
|
60
|
+
session.close();
|
|
61
|
+
sessions.delete(sessionId);
|
|
62
|
+
}
|
|
56
63
|
export async function runAgentAuthorTurn(input) {
|
|
57
64
|
const message = input.message.trim();
|
|
58
65
|
if (!message)
|
|
@@ -25,11 +25,14 @@ export declare function createAuthorSession(input?: {
|
|
|
25
25
|
}, storeRoot?: string): AuthorSessionRecord;
|
|
26
26
|
export declare function readAuthorSession(sessionId: string, storeRoot?: string): AuthorSessionRecord | null;
|
|
27
27
|
export declare function saveAuthorSession(record: AuthorSessionRecord, storeRoot?: string): AuthorSessionRecord;
|
|
28
|
+
/** Removes the session directory. Returns false when the id is invalid or missing. */
|
|
29
|
+
export declare function deleteAuthorSession(sessionId: string, storeRoot?: string): boolean;
|
|
28
30
|
export type AuthorDraftSummary = {
|
|
29
31
|
id: string;
|
|
30
32
|
name: string;
|
|
31
33
|
updatedAt: string;
|
|
32
34
|
messages: number;
|
|
33
35
|
};
|
|
36
|
+
export declare function draftHasWork(record: AuthorSessionRecord): boolean;
|
|
34
37
|
/** Unsaved author sessions, newest first. Empty shells with no name and no chat are left out. */
|
|
35
38
|
export declare function listAuthorDrafts(storeRoot?: string): AuthorDraftSummary[];
|