stageflow 0.42.0 → 0.44.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.
Files changed (275) hide show
  1. package/README.md +12 -3
  2. package/dist/agent/activity.d.ts +18 -0
  3. package/dist/agent/claudeAdapter.js +7 -0
  4. package/dist/agent/claudeTools.d.ts +27 -18
  5. package/dist/agent/claudeTools.js +54 -15
  6. package/dist/agent/compactionEvents.d.ts +34 -0
  7. package/dist/agent/compactionEvents.js +37 -0
  8. package/dist/agent/openRouterRouting.d.ts +7 -0
  9. package/dist/agent/openRouterRouting.js +23 -0
  10. package/dist/agent/piAdapter.d.ts +6 -0
  11. package/dist/agent/piAdapter.js +128 -27
  12. package/dist/agent/piSettings.d.ts +7 -0
  13. package/dist/agent/piSettings.js +9 -0
  14. package/dist/agent/sandboxPiTools.d.ts +3 -1
  15. package/dist/agent/sandboxPiTools.js +8 -2
  16. package/dist/agent/stageTurnDriver.d.ts +11 -2
  17. package/dist/agent/stageTurnDriver.js +45 -4
  18. package/dist/agent/toolOutputBudget.d.ts +15 -1
  19. package/dist/agent/toolOutputBudget.js +18 -0
  20. package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
  21. package/dist/agent/toolOutputBudgetExtension.js +40 -3
  22. package/dist/agents/authorChat.d.ts +1 -0
  23. package/dist/agents/authorChat.js +7 -0
  24. package/dist/agents/authorSessionStore.d.ts +3 -0
  25. package/dist/agents/authorSessionStore.js +17 -2
  26. package/dist/agents/hostContext.d.ts +1 -1
  27. package/dist/agents/runtime/piRuntime.d.ts +5 -1
  28. package/dist/agents/runtime/piRuntime.js +46 -12
  29. package/dist/agents/service.js +3 -2
  30. package/dist/agents/sessions.js +3 -1
  31. package/dist/compaction/directives.d.ts +18 -0
  32. package/dist/compaction/directives.js +119 -0
  33. package/dist/compaction/extension.d.ts +20 -0
  34. package/dist/compaction/extension.js +78 -0
  35. package/dist/compaction/mask.d.ts +18 -0
  36. package/dist/compaction/mask.js +155 -0
  37. package/dist/compaction/pin.d.ts +21 -0
  38. package/dist/compaction/pin.js +270 -0
  39. package/dist/compaction/scrub.d.ts +7 -0
  40. package/dist/compaction/scrub.js +26 -0
  41. package/dist/compaction/sessionWiring.d.ts +22 -0
  42. package/dist/compaction/sessionWiring.js +26 -0
  43. package/dist/compaction/summarize.d.ts +79 -0
  44. package/dist/compaction/summarize.js +171 -0
  45. package/dist/compaction/toolPolicy.d.ts +3 -0
  46. package/dist/compaction/toolPolicy.js +33 -0
  47. package/dist/compaction/types.d.ts +45 -0
  48. package/dist/compaction/types.js +7 -0
  49. package/dist/compaction/workingState.d.ts +56 -0
  50. package/dist/compaction/workingState.js +198 -0
  51. package/dist/config/createPipeline.js +2 -0
  52. package/dist/config/createTrigger.js +7 -1
  53. package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
  54. package/dist/config/ensureCatalogScanRoot.js +6 -0
  55. package/dist/config/evalCatalog.js +30 -2
  56. package/dist/config/githubTriggerAction.d.ts +8 -0
  57. package/dist/config/githubTriggerAction.js +29 -0
  58. package/dist/config/hostConfig.js +2 -0
  59. package/dist/config/loadEval.js +21 -3
  60. package/dist/config/pipelineNeeds.d.ts +2 -0
  61. package/dist/config/pipelineNeeds.js +16 -3
  62. package/dist/config/pipelineRoute.js +5 -4
  63. package/dist/config/validateCatalog.d.ts +1 -1
  64. package/dist/config/validateCatalog.js +12 -0
  65. package/dist/config/validateEval.js +43 -2
  66. package/dist/config/writeEval.js +2 -2
  67. package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
  68. package/dist/envelope/assertEmittedArtifacts.js +43 -0
  69. package/dist/envelope/preEmitChecks.d.ts +3 -0
  70. package/dist/evals/constants.d.ts +3 -1
  71. package/dist/evals/constants.js +3 -1
  72. package/dist/evals/engine/FakeEngine.d.ts +1 -1
  73. package/dist/evals/engine/FakeEngine.js +2 -1
  74. package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
  75. package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
  76. package/dist/evals/engine/promptfoo/generate.d.ts +1 -1
  77. package/dist/evals/engine/promptfoo/generate.js +11 -8
  78. package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +1 -1
  79. package/dist/evals/engine/promptfoo/rubricPrompt.js +5 -3
  80. package/dist/evals/manager.d.ts +13 -0
  81. package/dist/evals/manager.js +10 -2
  82. package/dist/evals/plan.d.ts +8 -1
  83. package/dist/evals/plan.js +28 -9
  84. package/dist/evals/planInternalAgent.d.ts +19 -0
  85. package/dist/evals/planInternalAgent.js +205 -0
  86. package/dist/evals/read/api.d.ts +12 -0
  87. package/dist/evals/read/api.js +5 -0
  88. package/dist/evals/read/list.js +10 -1
  89. package/dist/evals/read/runs.d.ts +5 -0
  90. package/dist/evals/read/runs.js +7 -1
  91. package/dist/evals/runner/caseRunner.d.ts +2 -1
  92. package/dist/evals/runner/caseRunner.js +16 -5
  93. package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
  94. package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
  95. package/dist/evals/snapshot.d.ts +10 -2
  96. package/dist/evals/snapshot.js +8 -3
  97. package/dist/evals/types.d.ts +12 -2
  98. package/dist/evals/types.js +4 -1
  99. package/dist/guide/cardContract.d.ts +133 -0
  100. package/dist/guide/cardContract.js +156 -0
  101. package/dist/guide/chat.d.ts +45 -0
  102. package/dist/guide/chat.js +378 -0
  103. package/dist/guide/knowledge/index.d.ts +14 -0
  104. package/dist/guide/knowledge/index.js +109 -0
  105. package/dist/guide/knowledge/topics/agents.d.ts +2 -0
  106. package/dist/guide/knowledge/topics/agents.js +58 -0
  107. package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
  108. package/dist/guide/knowledge/topics/bestPractices.js +67 -0
  109. package/dist/guide/knowledge/topics/connections.d.ts +2 -0
  110. package/dist/guide/knowledge/topics/connections.js +66 -0
  111. package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
  112. package/dist/guide/knowledge/topics/consoleMap.js +55 -0
  113. package/dist/guide/knowledge/topics/evals.d.ts +2 -0
  114. package/dist/guide/knowledge/topics/evals.js +72 -0
  115. package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
  116. package/dist/guide/knowledge/topics/limitations.js +84 -0
  117. package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
  118. package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
  119. package/dist/guide/knowledge/topics/memories.d.ts +2 -0
  120. package/dist/guide/knowledge/topics/memories.js +63 -0
  121. package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
  122. package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
  123. package/dist/guide/knowledge/topics/overview.d.ts +2 -0
  124. package/dist/guide/knowledge/topics/overview.js +56 -0
  125. package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
  126. package/dist/guide/knowledge/topics/pipelines.js +93 -0
  127. package/dist/guide/knowledge/topics/stages.d.ts +2 -0
  128. package/dist/guide/knowledge/topics/stages.js +54 -0
  129. package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
  130. package/dist/guide/knowledge/topics/tasks.js +52 -0
  131. package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
  132. package/dist/guide/knowledge/topics/triggers.js +68 -0
  133. package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
  134. package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
  135. package/dist/guide/knowledgeTools.d.ts +2 -0
  136. package/dist/guide/knowledgeTools.js +56 -0
  137. package/dist/guide/showUi.d.ts +6 -0
  138. package/dist/guide/showUi.js +546 -0
  139. package/dist/guide/threadStore.d.ts +48 -0
  140. package/dist/guide/threadStore.js +189 -0
  141. package/dist/guide/writeRoot.d.ts +11 -0
  142. package/dist/guide/writeRoot.js +70 -0
  143. package/dist/hosted/translate.js +2 -0
  144. package/dist/mcp/agentTools.js +12 -0
  145. package/dist/mcp/catalogTools.js +18 -20
  146. package/dist/mcp/catalogWriteTools.d.ts +8 -0
  147. package/dist/mcp/catalogWriteTools.js +225 -0
  148. package/dist/mcp/connectionTools.d.ts +3 -0
  149. package/dist/mcp/connectionTools.js +85 -0
  150. package/dist/mcp/controlTools.js +34 -1
  151. package/dist/mcp/deps.d.ts +9 -0
  152. package/dist/mcp/evalTools.js +684 -21
  153. package/dist/mcp/memoryHostTools.d.ts +3 -0
  154. package/dist/mcp/memoryHostTools.js +72 -0
  155. package/dist/mcp/pipelineAuthorTools.js +14 -1
  156. package/dist/mcp/server.js +5 -0
  157. package/dist/mcp/tools.js +6 -0
  158. package/dist/mcp/triggerTools.js +77 -31
  159. package/dist/operatorAgent/evalGate.d.ts +22 -0
  160. package/dist/operatorAgent/evalGate.js +77 -0
  161. package/dist/operatorAgent/fakeHost.js +10 -2
  162. package/dist/operatorAgent/host.js +18 -2
  163. package/dist/operatorAgent/index.d.ts +1 -0
  164. package/dist/operatorAgent/index.js +1 -0
  165. package/dist/operatorAgent/internalAgents.d.ts +11 -0
  166. package/dist/operatorAgent/internalAgents.js +13 -0
  167. package/dist/operatorAgent/piModel.d.ts +5 -1
  168. package/dist/operatorAgent/piModel.js +15 -10
  169. package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
  170. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
  171. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
  172. package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
  173. package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
  174. package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
  175. package/dist/operatorAgent/profiles/guideTools.js +123 -0
  176. package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
  177. package/dist/operatorAgent/profiles/stageflowGuide.js +132 -0
  178. package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
  179. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  180. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
  181. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
  182. package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
  183. package/dist/operatorAgent/types.d.ts +23 -0
  184. package/dist/package-meta.d.ts +1 -1
  185. package/dist/package-meta.js +1 -1
  186. package/dist/runstore/retention.d.ts +2 -1
  187. package/dist/runstore/retention.js +3 -7
  188. package/dist/runstore/runStatusClock.d.ts +17 -0
  189. package/dist/runstore/runStatusClock.js +19 -0
  190. package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
  191. package/dist/runstore/sqlite/SqliteRunStore.js +45 -45
  192. package/dist/runstore/workspaceLayout.js +6 -1
  193. package/dist/runtime/artifactDiskInspect.d.ts +14 -0
  194. package/dist/runtime/artifactDiskInspect.js +52 -0
  195. package/dist/runtime/artifactPath.d.ts +24 -0
  196. package/dist/runtime/artifactPath.js +84 -0
  197. package/dist/runtime/completionCheckRunner.js +15 -43
  198. package/dist/runtime/githubPollSource.d.ts +22 -4
  199. package/dist/runtime/githubPollSource.js +93 -8
  200. package/dist/runtime/priorArtifactGrants.d.ts +8 -0
  201. package/dist/runtime/priorArtifactGrants.js +105 -0
  202. package/dist/runtime/runActionEligibility.d.ts +46 -0
  203. package/dist/runtime/runActionEligibility.js +123 -0
  204. package/dist/runtime/runManager.d.ts +4 -1
  205. package/dist/runtime/runManager.js +52 -10
  206. package/dist/runtime/runRetryCoordinator.d.ts +2 -14
  207. package/dist/runtime/runRetryCoordinator.js +3 -67
  208. package/dist/runtime/sandboxContainer.d.ts +7 -0
  209. package/dist/runtime/sandboxContainer.js +48 -6
  210. package/dist/runtime/sandboxDesk.d.ts +2 -1
  211. package/dist/runtime/sandboxDesk.js +11 -12
  212. package/dist/runtime/stageEnvironment.js +1 -0
  213. package/dist/runtime/stageRoots.d.ts +12 -0
  214. package/dist/runtime/stageSandbox.d.ts +2 -1
  215. package/dist/runtime/stageSandbox.js +18 -2
  216. package/dist/runtime/stageWorker.js +20 -0
  217. package/dist/server/agentAuthorRoutes.js +12 -3
  218. package/dist/server/bootstrap.d.ts +5 -0
  219. package/dist/server/bootstrap.js +14 -1
  220. package/dist/server/evalRoutes.js +38 -9
  221. package/dist/server/guideRoutes.d.ts +5 -0
  222. package/dist/server/guideRoutes.js +166 -0
  223. package/dist/server/http.d.ts +10 -0
  224. package/dist/server/http.js +35 -1
  225. package/dist/server/internalAgentRoutes.d.ts +6 -0
  226. package/dist/server/internalAgentRoutes.js +12 -0
  227. package/dist/server/mcpHost.js +2 -0
  228. package/dist/toolkit/meta.d.ts +1 -1
  229. package/dist/toolkit/meta.js +39 -1
  230. package/dist/toolkit/profiles.d.ts +5 -0
  231. package/dist/toolkit/profiles.js +12 -0
  232. package/dist/tools/askOperator.js +2 -1
  233. package/dist/tools/emitStageEnvelope.d.ts +17 -16
  234. package/dist/tools/emitStageEnvelope.js +46 -32
  235. package/dist/tools/stageToolNames.d.ts +4 -0
  236. package/dist/tools/stageToolNames.js +4 -0
  237. package/dist/tools/workingState.d.ts +40 -0
  238. package/dist/tools/workingState.js +108 -0
  239. package/dist/tools/writeStageArtifact.d.ts +0 -9
  240. package/dist/tools/writeStageArtifact.js +2 -1
  241. package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-S5C_vemc.js} +2 -2
  242. package/dist/ui/assets/adapters-C0l4ruiR.js +17 -0
  243. package/dist/ui/assets/chat-CjnsmvqR.js +2 -0
  244. package/dist/ui/assets/chat-DFDd4UjS.css +2 -0
  245. package/dist/ui/assets/{main-CHlmTNJe.css → main-Del1GeLf.css} +1 -1
  246. package/dist/ui/assets/main-_yXVqcW9.js +277 -0
  247. package/dist/ui/chat.html +3 -4
  248. package/dist/ui/index.html +3 -4
  249. package/dist/workshop/chatTurn.js +1 -1
  250. package/dist/workshop/workshopPin.d.ts +3 -3
  251. package/dist/workshop/workshopPin.js +16 -15
  252. package/examples/compaction-stress/README.md +30 -0
  253. package/examples/compaction-stress/check-run.mjs +53 -0
  254. package/examples/compaction-stress/collect-evidence.yaml +60 -0
  255. package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
  256. package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
  257. package/examples/compaction-stress/generate-evidence.mjs +110 -0
  258. package/examples/compaction-stress/root-cause.yaml +26 -0
  259. package/examples/compaction-stress/stageflow.yaml +9 -0
  260. package/examples/compaction-stress/write-postmortem.yaml +40 -0
  261. package/examples/evals/README.md +18 -0
  262. package/examples/evals/guide-behavior.dataset.jsonl +15 -0
  263. package/examples/evals/guide-behavior.eval.yaml +35 -0
  264. package/examples/evals/guide-depth.dataset.jsonl +35 -0
  265. package/examples/evals/guide-depth.eval.yaml +169 -0
  266. package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
  267. package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
  268. package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
  269. package/examples/memory/evals/guide-depth.eval.yaml +169 -0
  270. package/package.json +1 -1
  271. package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
  272. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
  273. package/dist/ui/assets/chat-Df_wCpak.js +0 -5
  274. package/dist/ui/assets/chat-P7KQznHm.css +0 -2
  275. package/dist/ui/assets/main-CvVfxmMd.js +0 -271
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { globalStageflowHome } from "../project/globalHome.js";
5
5
  function guardSessionId(sessionId) {
@@ -114,11 +114,26 @@ export function saveAuthorSession(record, storeRoot) {
114
114
  writeRecord(next, storeRoot);
115
115
  return next;
116
116
  }
117
+ /** Removes the session directory. Returns false when the id is invalid or missing. */
118
+ export function deleteAuthorSession(sessionId, storeRoot) {
119
+ let file;
120
+ try {
121
+ file = authorSessionFilePath(sessionId, storeRoot);
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ const dir = path.dirname(file);
127
+ if (!existsSync(dir))
128
+ return false;
129
+ rmSync(dir, { recursive: true, force: true });
130
+ return true;
131
+ }
117
132
  function draftTitle(draft) {
118
133
  const name = typeof draft.name === "string" ? draft.name.trim() : "";
119
134
  return name || "Untitled draft";
120
135
  }
121
- function draftHasWork(record) {
136
+ export function draftHasWork(record) {
122
137
  if (record.transcript.length > 0)
123
138
  return true;
124
139
  for (const key of ["name", "instructions", "description"]) {
@@ -53,5 +53,5 @@ export declare function createAgentHostContext(options: {
53
53
  /** Host services the Stageflow tools need; agent services are added here. Without it agents get no Stageflow tools. */
54
54
  /** Hold streams open to AgentMail and iMessage for agents linked to them. Only a running Host does this. */
55
55
  listenOnChannels?: boolean;
56
- toolkitDeps?: Pick<McpToolDeps, "manager" | "store" | "agentDir" | "providerAuthContext" | "projectRoot" | "providerBoot">;
56
+ toolkitDeps?: Pick<McpToolDeps, "manager" | "store" | "agentDir" | "providerAuthContext" | "projectRoot" | "providerBoot" | "channels" | "memories" | "evalRunManager">;
57
57
  }): AgentHostContext;
@@ -1,4 +1,4 @@
1
- import { type AgentSession } from "@earendil-works/pi-coding-agent";
1
+ import { type AgentSession, type ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
2
  import { type AgentDefinition } from "../../config/loadAgent.js";
3
3
  import { type StageMemory } from "../../memory/access.js";
4
4
  import { type SandboxRunner } from "../sandbox.js";
@@ -38,6 +38,8 @@ export type OpenSessionInput = {
38
38
  /** Stageflow tools this agent is granted. */
39
39
  stageflowTools?: ToolkitTool[];
40
40
  memory?: StageMemory;
41
+ /** Defaults to true; one-shot judge sessions have no use for it. */
42
+ workingState?: boolean;
41
43
  hooks: () => TurnHooks | undefined;
42
44
  };
43
45
  export type OpenedSession = {
@@ -48,5 +50,7 @@ export type OpenedSession = {
48
50
  toolNames: string[];
49
51
  shutdown(): Promise<void>;
50
52
  };
53
+ /** Runs the agent tool gate before every call, except for tools the session wiring marks as internal. */
54
+ export declare function createAgentGateExtension(gate: OpenSessionInput["gate"], isInternalTool: (toolName: string) => boolean): ExtensionFactory;
51
55
  export declare const DEFAULT_IDLE_RELEASE_MS: number;
52
56
  export declare function createPiAgentRuntime(options: PiRuntimeOptions): ConversationBackend;
@@ -1,11 +1,13 @@
1
1
  import { mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { defineTool, ModelRuntime, resolveCliModel, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
3
+ import { defineTool, ModelRuntime, resolveCliModel, SessionManager, } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
5
  import { collectMcpExtensionToolNames, mcpToolMetadataMissing } from "../../agent/piAdapter.js";
6
6
  import { attachIsolatedMcp } from "../../agent/piIsolatedMcp.js";
7
7
  import { warmIsolatedMcpMetadata } from "../../agent/piIsolatedMcpProbe.js";
8
8
  import { createPiAgentSession, createSealedResourceLoader } from "../../agent/piSessionFactory.js";
9
+ import { createPiSettingsManager } from "../../agent/piSettings.js";
10
+ import { createSessionContextWiring } from "../../compaction/sessionWiring.js";
9
11
  import { bindCursorApiKey, isCursorModelRef } from "../../agent/cursorProvider.js";
10
12
  import { findProviderSupport } from "../../agent/providerSupport.js";
11
13
  import { grantedBuiltinTools } from "../../config/loadAgent.js";
@@ -19,11 +21,22 @@ import { isUsableAuthFile, resolveCredentialBinding } from "../../runtime/creden
19
21
  import { getModelCatalog } from "../../models/catalog.js";
20
22
  import { AgentError } from "../errors.js";
21
23
  import { MEMORY_STORE_BASH_DENIED, memoryStorePathInCommand } from "../../memory/bashDeny.js";
24
+ import { compactionUsageCost } from "../../agent/compactionEvents.js";
22
25
  import { memoryHome } from "../../memory/host.js";
23
26
  import { forbiddenWriteTarget } from "../policy.js";
24
27
  import { prepareExecutionLaunch } from "../sandbox.js";
25
28
  import { grantedStageflowTools, isOperatorOnlyTool } from "../../toolkit/index.js";
26
29
  import { grantIdForPiTool, STAGEFLOW_PI_PREFIX } from "./types.js";
30
+ /** Runs the agent tool gate before every call, except for tools the session wiring marks as internal. */
31
+ export function createAgentGateExtension(gate, isInternalTool) {
32
+ return (pi) => {
33
+ pi.on("tool_call", async (event) => {
34
+ if (isInternalTool(event.toolName))
35
+ return undefined;
36
+ return gate({ toolCallId: event.toolCallId, toolName: event.toolName, input: event.input });
37
+ });
38
+ };
39
+ }
27
40
  function piToolNames(definition) {
28
41
  return grantedBuiltinTools(definition);
29
42
  }
@@ -84,7 +97,7 @@ async function openPiSession(options, input) {
84
97
  try {
85
98
  await mkdir(options.agentDir, { recursive: true });
86
99
  const modelRuntime = await ModelRuntime.create({ authPath, modelsPath: path.join(path.dirname(authPath), "models.json") });
87
- const settingsManager = SettingsManager.inMemory({ compaction: { enabled: true } });
100
+ const settingsManager = createPiSettingsManager();
88
101
  const mcpServers = Object.keys(definition.tools.mcp);
89
102
  const resolvedMcp = mcpServers.length > 0
90
103
  ? await resolveStageMcpServers({ projectRoot, allowlist: mcpServers, env: {}, scope: { kind: "agent" } })
@@ -97,18 +110,18 @@ async function openPiSession(options, input) {
97
110
  throw new AgentError("agent.backend_unavailable", `Skill "${name}" is not installed`);
98
111
  skillPaths.push(skill.filePath);
99
112
  }
100
- const gateExtension = (pi) => {
101
- pi.on("tool_call", async (event) => {
102
- const outcome = await input.gate({ toolCallId: event.toolCallId, toolName: event.toolName, input: event.input });
103
- return outcome;
104
- });
105
- };
106
- const extensionFactories = [{ name: "stageflow-agent-gate", factory: gateExtension }, ...(attached.extensionFactories ?? [])];
113
+ const contextWiring = createSessionContextWiring({ workingState: input.workingState !== false });
114
+ const gateExtension = createAgentGateExtension(input.gate, contextWiring.isInternalTool);
115
+ const extensionFactories = [
116
+ { name: "stageflow-agent-gate", factory: gateExtension },
117
+ ...contextWiring.extensions,
118
+ ...(attached.extensionFactories ?? []),
119
+ ];
107
120
  const loader = createSealedResourceLoader({
108
121
  cwd: projectRoot,
109
122
  agentDir: options.agentDir,
110
123
  settingsManager,
111
- systemPrompt: input.systemPrompt,
124
+ systemPrompt: contextWiring.guidance ? `${input.systemPrompt}\n\n${contextWiring.guidance}` : input.systemPrompt,
112
125
  additionalExtensionPaths: extensionPaths,
113
126
  ...(skillPaths.length > 0 ? { additionalSkillPaths: skillPaths } : {}),
114
127
  extensionFactories,
@@ -127,6 +140,7 @@ async function openPiSession(options, input) {
127
140
  throw new AgentError("agent.backend_unavailable", `Failed to load extension(s): ${errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
128
141
  }
129
142
  const toolNames = [...input.toolNames];
143
+ toolNames.push(...contextWiring.toolNames);
130
144
  if (attached.extensionFactories) {
131
145
  const mcp = collectMcpExtensionToolNames(loader);
132
146
  if (!mcp.ok)
@@ -149,6 +163,7 @@ async function openPiSession(options, input) {
149
163
  label: `Ask ${agentId}`,
150
164
  description: `Hand a task to the "${agentId}" agent and get its reply.`,
151
165
  parameters: Type.Object({ message: Type.String({ description: "The task or question for the agent." }) }),
166
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
152
167
  execute: async (callId, args) => {
153
168
  const reply = await input.delegateTool(agentId, args.message, callId);
154
169
  return { content: [{ type: "text", text: reply }], details: {} };
@@ -159,7 +174,10 @@ async function openPiSession(options, input) {
159
174
  }
160
175
  if (input.memory) {
161
176
  for (const definition of memorySession(input.memory).tools) {
162
- customTools.push(defineTool(definition));
177
+ customTools.push(defineTool({
178
+ ...definition,
179
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
180
+ }));
163
181
  toolNames.push(definition.name);
164
182
  }
165
183
  }
@@ -169,6 +187,7 @@ async function openPiSession(options, input) {
169
187
  label: tool.name,
170
188
  description: tool.description,
171
189
  parameters: Type.Unsafe(tool.parameters),
190
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
172
191
  execute: async (_id, args, signal) => {
173
192
  const result = await tool.run(args ?? {}, { signal: signal ?? new AbortController().signal, caller: { agentId: definition.id, definition } });
174
193
  if (!result.ok)
@@ -334,6 +353,7 @@ export function createPiAgentRuntime(options) {
334
353
  let text = "";
335
354
  const usage = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
336
355
  let startedAt = new Date();
356
+ let compactionStartedAt;
337
357
  const calls = new Map();
338
358
  // A model call that goes quiet (provider queue, dropped connection) must not hold the session forever.
339
359
  // Waiting on a person (approval), a running tool or a sub-agent is not a stall.
@@ -370,7 +390,20 @@ export function createPiAgentRuntime(options) {
370
390
  const unsubscribe = session.subscribe((event) => {
371
391
  arm();
372
392
  const e = event;
373
- if (e.type === "message_start" && e.message?.role === "assistant")
393
+ if (e.type === "compaction_start")
394
+ compactionStartedAt = new Date();
395
+ else if (e.type === "compaction_end") {
396
+ const end = event;
397
+ const info = compactionUsageCost(end);
398
+ usage.inputTokens += info.inputTokens;
399
+ usage.outputTokens += info.outputTokens;
400
+ usage.costUsd += info.costUsd;
401
+ const model = session.model?.id;
402
+ if (end.result?.usage) {
403
+ input.hooks.modelFinished({ startedAt: compactionStartedAt ?? new Date(), endedAt: new Date(), ...info, ...(model ? { model } : {}) });
404
+ }
405
+ }
406
+ else if (e.type === "message_start" && e.message?.role === "assistant")
374
407
  startedAt = new Date();
375
408
  else if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta" && typeof e.assistantMessageEvent.delta === "string") {
376
409
  text += e.assistantMessageEvent.delta;
@@ -435,6 +468,7 @@ export function createPiAgentRuntime(options) {
435
468
  definition: { ...input.definition, tools: { ...input.definition.tools, builtin: [], builtin_tools: [], mcp: {}, skills: [] }, sub_agents: [] },
436
469
  systemPrompt: input.systemPrompt ?? "You are a strict evaluator. Answer only with the requested number.",
437
470
  toolNames: [],
471
+ workingState: false,
438
472
  gate: async () => ({ block: true, reason: "No tools" }),
439
473
  hooks: () => undefined,
440
474
  });
@@ -576,8 +576,9 @@ export class AgentService {
576
576
  return false;
577
577
  throw error;
578
578
  });
579
- if (onDisk || (existing && !existing.archived))
580
- throw new AgentError("agent.exists");
579
+ if (onDisk || (existing && !existing.archived)) {
580
+ throw new AgentError("agent.exists", `An agent with id "${definition.id}" already exists`);
581
+ }
581
582
  // A new agent always starts disabled (R1); enabling is its own revision.
582
583
  await this.writeRevision(projectRoot, { ...definition, enabled: false }, actor, "Created");
583
584
  });
@@ -5,6 +5,8 @@ import { EVAL_SESSION_SOURCE, isEvalSession } from "./evalSession.js";
5
5
  import { guardedStream } from "./guardrails.js";
6
6
  import { fitHistory } from "./historyFit.js";
7
7
  import { isToolGranted, requiresAskFirst } from "./policy.js";
8
+ import { scrubSecrets } from "../compaction/scrub.js";
9
+ import { WORKING_STATE_TOOL_NAME } from "../compaction/types.js";
8
10
  import { prepareSpan } from "./trace.js";
9
11
  const GROUNDEDNESS_PROMPT = (reply, evidence) => [
10
12
  "Rate how well the ANSWER is supported by the TOOL OUTPUTS, from 0 (unsupported or contradicted) to 1 (fully supported).",
@@ -202,7 +204,7 @@ export class AgentConversation {
202
204
  ...(isEvalSession(active.session) ? { "gen_ai.tool.call.id": call.id } : {}),
203
205
  ...(childSessionOf(call) ? { "stageflow.sub_agent.session_id": childSessionOf(call) } : {}),
204
206
  },
205
- input: toJson(call.input),
207
+ input: call.tool === WORKING_STATE_TOOL_NAME ? scrubSecrets(toJson(call.input)) : toJson(call.input),
206
208
  output: result.output,
207
209
  });
208
210
  },
@@ -0,0 +1,18 @@
1
+ import type { LooseMessage } from "./pin.js";
2
+ export type DirectiveSource = "operator" | "user" | "rejection";
3
+ /** Words from the people running the work, copied verbatim so a summary cannot reword a decision. */
4
+ export type Directive = {
5
+ source: DirectiveSource;
6
+ text: string;
7
+ };
8
+ export declare const DIRECTIVE_MAX_CHARS = 600;
9
+ export declare const USER_DIRECTIVE_MAX_CHARS = 280;
10
+ export declare const DIRECTIVES_CARRY_MAX = 60;
11
+ /**
12
+ * Operator answers, rejected emits and user messages, in order. The stage kickoff and reminder prompts are
13
+ * skipped: they carry no decision.
14
+ */
15
+ export declare function extractDirectives(messages: readonly LooseMessage[]): Directive[];
16
+ export declare function mergeDirectives(previous: readonly Directive[], fresh: readonly Directive[], max?: number): Directive[];
17
+ /** Directive-like entries first (newest up to 60% of the budget, then oldest), then the rest if room remains. */
18
+ export declare function renderDirectives(directives: readonly Directive[], maxChars: number): string;
@@ -0,0 +1,119 @@
1
+ import { ASK_OPERATOR_TOOL_NAME, EMIT_STAGE_ENVELOPE_TOOL_NAME } from "../tools/stageToolNames.js";
2
+ import { scrubSecrets } from "./scrub.js";
3
+ export const DIRECTIVE_MAX_CHARS = 600;
4
+ export const USER_DIRECTIVE_MAX_CHARS = 280;
5
+ export const DIRECTIVES_CARRY_MAX = 60;
6
+ const NOISE_USER = /^(?:Begin this stage\b|Reminder \d+ of \d+)/i;
7
+ const FEEDBACK_RESUME_PREFIX = /^Continue this stage after feedback-loop send-back\.\s*Incorporate the operator\/source feedback below[^\n]*\n*/i;
8
+ const OPERATOR_TOOLS = new Set([ASK_OPERATOR_TOOL_NAME]);
9
+ const ENVELOPE_TOOLS = new Set([EMIT_STAGE_ENVELOPE_TOOL_NAME]);
10
+ function blocksText(content) {
11
+ if (typeof content === "string")
12
+ return content;
13
+ if (!Array.isArray(content))
14
+ return "";
15
+ return content
16
+ .map((b) => (b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string" ? b.text : ""))
17
+ .filter(Boolean)
18
+ .join("\n");
19
+ }
20
+ function clip(text, max = DIRECTIVE_MAX_CHARS) {
21
+ const flat = text.replace(/\s+/g, " ").trim();
22
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
23
+ }
24
+ /**
25
+ * Operator answers, rejected emits and user messages, in order. The stage kickoff and reminder prompts are
26
+ * skipped: they carry no decision.
27
+ */
28
+ export function extractDirectives(messages) {
29
+ const out = [];
30
+ for (const m of messages) {
31
+ if (m.role === "user") {
32
+ const text = blocksText(m.content).trim().replace(FEEDBACK_RESUME_PREFIX, "");
33
+ if (!text.trim() || NOISE_USER.test(text.trim()))
34
+ continue;
35
+ out.push({ source: "user", text: clip(scrubSecrets(text), USER_DIRECTIVE_MAX_CHARS) });
36
+ }
37
+ else if (m.role === "toolResult") {
38
+ const name = typeof m.toolName === "string" ? m.toolName : "";
39
+ const text = blocksText(m.content);
40
+ if (!text.trim())
41
+ continue;
42
+ if (OPERATOR_TOOLS.has(name))
43
+ out.push({ source: "operator", text: clip(scrubSecrets(text)) });
44
+ else if (ENVELOPE_TOOLS.has(name) && m.isError === true)
45
+ out.push({ source: "rejection", text: clip(scrubSecrets(text)) });
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ export function mergeDirectives(previous, fresh, max = DIRECTIVES_CARRY_MAX) {
51
+ const seen = new Set();
52
+ const merged = [];
53
+ for (const d of [...previous, ...fresh]) {
54
+ const key = `${d.source}\u0000${d.text}`;
55
+ if (seen.has(key))
56
+ continue;
57
+ seen.add(key);
58
+ merged.push(d);
59
+ }
60
+ return merged.slice(-max);
61
+ }
62
+ const LABEL = { operator: "Operator", user: "User", rejection: "Rejected" };
63
+ const DIRECTIVE_WORDS = /\b(?:always|never|must|should|shall|don'?t|do not|cannot|can'?t|only|prefer|require[sd]?|approved?|rejected?|decid(?:e|ed|ion)|deadline|due|before|after|until|instead|budget|limit|forbidden|allowed|use|avoid|ensure|remember|important)\b/i;
64
+ const SHORT_MESSAGE_CHARS = 160;
65
+ /** Instruction-like or short messages outrank long prose, so filler cannot crowd out a rule. */
66
+ function isDirectiveLike(d) {
67
+ if (d.source !== "user")
68
+ return true;
69
+ return d.text.length <= SHORT_MESSAGE_CHARS || DIRECTIVE_WORDS.test(d.text);
70
+ }
71
+ /** Directive-like entries first (newest up to 60% of the budget, then oldest), then the rest if room remains. */
72
+ export function renderDirectives(directives, maxChars) {
73
+ if (directives.length === 0)
74
+ return "";
75
+ const header = "## Operator and user directives (verbatim, oldest first)";
76
+ const lineOf = (d) => `- ${LABEL[d.source]}: ${d.text}`;
77
+ const picked = new Set();
78
+ let used = header.length;
79
+ const take = (i, limit) => {
80
+ const cost = lineOf(directives[i]).length + 1;
81
+ if (used + cost > limit)
82
+ return false;
83
+ picked.add(i);
84
+ used += cost;
85
+ return true;
86
+ };
87
+ const primary = directives.map((d, i) => (isDirectiveLike(d) ? i : -1)).filter((i) => i >= 0);
88
+ const secondary = directives.map((d, i) => (isDirectiveLike(d) ? -1 : i)).filter((i) => i >= 0);
89
+ const newestLimit = Math.floor(maxChars * 0.6);
90
+ for (let k = primary.length - 1; k >= 0; k--)
91
+ if (!take(primary[k], newestLimit))
92
+ break;
93
+ for (const i of primary) {
94
+ if (picked.has(i))
95
+ continue;
96
+ if (!take(i, maxChars))
97
+ break;
98
+ }
99
+ for (const i of secondary) {
100
+ if (!take(i, maxChars))
101
+ break;
102
+ }
103
+ if (picked.size === 0)
104
+ return "";
105
+ const lines = [];
106
+ let gap = false;
107
+ for (let i = 0; i < directives.length; i++) {
108
+ if (picked.has(i)) {
109
+ if (gap)
110
+ lines.push("- (…)");
111
+ gap = false;
112
+ lines.push(lineOf(directives[i]));
113
+ }
114
+ else if (lines.length > 0)
115
+ gap = true;
116
+ }
117
+ const omitted = directives.length - picked.size;
118
+ return [header, ...lines, ...(omitted > 0 ? [`- (${omitted} omitted for space)`] : [])].join("\n");
119
+ }
@@ -0,0 +1,20 @@
1
+ import { compact as piCompact, type ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+ import { type ClearingPolicy } from "./mask.js";
3
+ export type CompactionPolicy = {
4
+ /** Clear stale re-fetchable tool results in the text the summarizer reads. */
5
+ clearInSummary?: ClearingPolicy;
6
+ /** Clear stale re-fetchable tool results in every request once the window is this full (0-1). */
7
+ preemptive?: {
8
+ usageFraction: number;
9
+ policy: ClearingPolicy;
10
+ };
11
+ };
12
+ export declare const DEFAULT_COMPACTION_POLICY: CompactionPolicy;
13
+ type CompactFn = typeof piCompact;
14
+ export type CompactionExtensionOptions = {
15
+ policy?: CompactionPolicy;
16
+ compact?: CompactFn;
17
+ onWarn?: (message: string) => void;
18
+ };
19
+ export declare function createCompactionExtension(options?: CompactionExtensionOptions): ExtensionFactory;
20
+ export {};
@@ -0,0 +1,78 @@
1
+ import { compact as piCompact, } from "@earendil-works/pi-coding-agent";
2
+ import { applyClearing, planClearing } from "./mask.js";
3
+ import { COMPACTION_FOCUS, composeCompactionSummary, readStageflowDetails, stageflowDetails, } from "./summarize.js";
4
+ import { getWorkingStateSections } from "./workingState.js";
5
+ export const DEFAULT_COMPACTION_POLICY = {};
6
+ function lastCompactionDetails(branch) {
7
+ for (let i = branch.length - 1; i >= 0; i--) {
8
+ const e = branch[i];
9
+ if (e?.type === "compaction")
10
+ return readStageflowDetails(e.details);
11
+ }
12
+ return undefined;
13
+ }
14
+ export function createCompactionExtension(options = {}) {
15
+ const policy = options.policy ?? DEFAULT_COMPACTION_POLICY;
16
+ const compact = options.compact ?? piCompact;
17
+ const warn = options.onWarn ?? (() => { });
18
+ return (pi) => {
19
+ pi.on("session_before_compact", async (event, rawCtx) => {
20
+ const ctx = rawCtx;
21
+ const model = ctx.model;
22
+ if (!model)
23
+ return undefined;
24
+ try {
25
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
26
+ if (!auth.ok)
27
+ return undefined;
28
+ const { preparation } = event;
29
+ const previousDetails = lastCompactionDetails(event.branchEntries);
30
+ const historyCount = preparation.messagesToSummarize.length;
31
+ const composed = await composeCompactionSummary({
32
+ messages: [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages],
33
+ ...(preparation.previousSummary !== undefined ? { previousSummary: preparation.previousSummary } : {}),
34
+ ...(previousDetails ? { previousDetails } : {}),
35
+ state: getWorkingStateSections(ctx.sessionManager.getEntries()),
36
+ ...(policy.clearInSummary ? { clearInSummary: policy.clearInSummary } : {}),
37
+ summarize: ({ messages, previousSummary }) => compact({
38
+ ...preparation,
39
+ messagesToSummarize: messages.slice(0, historyCount),
40
+ turnPrefixMessages: messages.slice(historyCount),
41
+ ...(previousSummary !== undefined ? { previousSummary } : {}),
42
+ }, model, auth.apiKey, auth.headers, COMPACTION_FOCUS, event.signal, ctx.thinkingLevel, undefined, auth.env),
43
+ });
44
+ const { result, prepared } = composed;
45
+ return {
46
+ compaction: {
47
+ summary: composed.summary,
48
+ firstKeptEntryId: result.firstKeptEntryId,
49
+ tokensBefore: result.tokensBefore,
50
+ ...(result.usage ? { usage: result.usage } : {}),
51
+ details: stageflowDetails(result.details, previousDetails, prepared.pinned, prepared.cleared, prepared.directives),
52
+ },
53
+ };
54
+ }
55
+ catch (err) {
56
+ if (event.signal.aborted)
57
+ throw err;
58
+ warn(`stageflow compaction fell back to Pi default: ${err instanceof Error ? err.message : String(err)}`);
59
+ return undefined;
60
+ }
61
+ });
62
+ const preemptive = policy.preemptive;
63
+ if (preemptive) {
64
+ pi.on("context", async (event, rawCtx) => {
65
+ const ctx = rawCtx;
66
+ const usage = ctx.getContextUsage();
67
+ if (!usage || usage.tokens === null || usage.contextWindow <= 0)
68
+ return undefined;
69
+ if (usage.tokens / usage.contextWindow < preemptive.usageFraction)
70
+ return undefined;
71
+ const plan = planClearing(event.messages, preemptive.policy);
72
+ if (plan.cleared.length === 0)
73
+ return undefined;
74
+ return { messages: applyClearing(event.messages, plan) };
75
+ });
76
+ }
77
+ };
78
+ }
@@ -0,0 +1,18 @@
1
+ import type { LooseMessage } from "./pin.js";
2
+ import type { ClearedResult } from "./types.js";
3
+ export type ClearingPolicy = {
4
+ /** Never touch results at or after the Nth assistant turn from the end. */
5
+ keepRecentTurns: number;
6
+ /** Ignore results smaller than this many UTF-8 bytes. */
7
+ minBytes: number;
8
+ /** Overrides the default recover hint when it returns a non-empty string. */
9
+ recoverHintFor?: (message: LooseMessage) => string | undefined;
10
+ };
11
+ export type ClearingPlan = {
12
+ cleared: ClearedResult[];
13
+ clearIds: Set<string>;
14
+ };
15
+ export declare function planClearing(messages: readonly LooseMessage[], policy: ClearingPolicy): ClearingPlan;
16
+ export declare function placeholderFor(item: ClearedResult): string;
17
+ export declare function applyClearing<T extends LooseMessage>(messages: readonly T[], plan: ClearingPlan): T[];
18
+ export declare function renderClearedIndex(cleared: readonly ClearedResult[], maxChars: number): string;
@@ -0,0 +1,155 @@
1
+ import { scrubSecrets } from "./scrub.js";
2
+ import { classifyForClearing } from "./toolPolicy.js";
3
+ const PLACEHOLDER_PREFIX = "[Tool result cleared to save context:";
4
+ const DESCRIPTOR_MAX = 100;
5
+ function record(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7
+ }
8
+ function resultText(message) {
9
+ const content = message.content;
10
+ if (typeof content === "string")
11
+ return content;
12
+ if (!Array.isArray(content))
13
+ return "";
14
+ let out = "";
15
+ for (const block of content) {
16
+ const b = record(block);
17
+ if (b?.type === "text" && typeof b.text === "string")
18
+ out += (out ? "\n" : "") + b.text;
19
+ }
20
+ return out;
21
+ }
22
+ function oneLine(text, max) {
23
+ const flat = text.replace(/\s+/g, " ").trim();
24
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
25
+ }
26
+ function findToolCall(messages, before, toolCallId) {
27
+ for (let i = before - 1; i >= 0; i--) {
28
+ const m = messages[i];
29
+ if (m.role !== "assistant" || !Array.isArray(m.content))
30
+ continue;
31
+ for (const block of m.content) {
32
+ const b = record(block);
33
+ if (b?.type === "toolCall" && b.id === toolCallId)
34
+ return b;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ function callArgs(call) {
40
+ return record(call?.arguments) ?? {};
41
+ }
42
+ function describe(toolName, args) {
43
+ const pathArg = typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : undefined;
44
+ if (pathArg)
45
+ return oneLine(`${toolName} ${pathArg}`, DESCRIPTOR_MAX);
46
+ if (typeof args.command === "string")
47
+ return oneLine(`${toolName}: ${args.command}`, DESCRIPTOR_MAX);
48
+ if (typeof args.pattern === "string")
49
+ return oneLine(`${toolName} ${args.pattern}${typeof args.path === "string" ? ` in ${args.path}` : ""}`, DESCRIPTOR_MAX);
50
+ const keys = Object.keys(args);
51
+ if (keys.length === 0)
52
+ return toolName;
53
+ let json = "";
54
+ try {
55
+ json = JSON.stringify(args);
56
+ }
57
+ catch {
58
+ json = "";
59
+ }
60
+ return oneLine(json ? `${toolName} ${json}` : toolName, DESCRIPTOR_MAX);
61
+ }
62
+ function defaultHint(message, toolName, args) {
63
+ const spill = record(record(message.details)?.stageflowToolOutput);
64
+ const spillPath = typeof spill?.path === "string" ? spill.path : undefined;
65
+ if (spillPath)
66
+ return `read ${spillPath}`;
67
+ if (toolName === "read") {
68
+ const p = typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : undefined;
69
+ if (p)
70
+ return `read ${p} (offset/limit as before)`;
71
+ }
72
+ return `re-run ${toolName} with the same arguments`;
73
+ }
74
+ function isPlaceholder(text) {
75
+ return text.startsWith(PLACEHOLDER_PREFIX);
76
+ }
77
+ export function planClearing(messages, policy) {
78
+ const cleared = [];
79
+ const clearIds = new Set();
80
+ let boundary = messages.length;
81
+ if (policy.keepRecentTurns > 0) {
82
+ let seen = 0;
83
+ boundary = 0;
84
+ for (let i = messages.length - 1; i >= 0; i--) {
85
+ if (messages[i].role === "assistant" && ++seen === policy.keepRecentTurns) {
86
+ boundary = i;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ for (let i = 0; i < boundary; i++) {
92
+ const m = messages[i];
93
+ if (m.role !== "toolResult")
94
+ continue;
95
+ const toolCallId = typeof m.toolCallId === "string" ? m.toolCallId : undefined;
96
+ const toolName = typeof m.toolName === "string" ? m.toolName : undefined;
97
+ if (!toolCallId || !toolName || clearIds.has(toolCallId))
98
+ continue;
99
+ if (classifyForClearing(toolName, m.isError === true, m.details) !== "clearable")
100
+ continue;
101
+ const text = resultText(m);
102
+ if (isPlaceholder(text))
103
+ continue;
104
+ const bytes = Buffer.byteLength(text, "utf8");
105
+ if (bytes < policy.minBytes)
106
+ continue;
107
+ const args = callArgs(findToolCall(messages, i, toolCallId));
108
+ const custom = policy.recoverHintFor?.(m);
109
+ cleared.push({
110
+ toolName,
111
+ toolCallId,
112
+ bytes,
113
+ descriptor: scrubSecrets(describe(toolName, args)),
114
+ recoverHint: scrubSecrets(custom && custom.trim().length > 0 ? oneLine(custom, 300) : defaultHint(m, toolName, args)),
115
+ });
116
+ clearIds.add(toolCallId);
117
+ }
118
+ return { cleared, clearIds };
119
+ }
120
+ export function placeholderFor(item) {
121
+ return `${PLACEHOLDER_PREFIX} ${item.descriptor}, ${item.bytes} bytes. To get it again: ${item.recoverHint}]`;
122
+ }
123
+ export function applyClearing(messages, plan) {
124
+ if (plan.clearIds.size === 0)
125
+ return [...messages];
126
+ const byId = new Map(plan.cleared.map((c) => [c.toolCallId, c]));
127
+ return messages.map((m) => {
128
+ if (m.role !== "toolResult" || typeof m.toolCallId !== "string" || !plan.clearIds.has(m.toolCallId))
129
+ return m;
130
+ const item = byId.get(m.toolCallId);
131
+ if (!item || isPlaceholder(resultText(m)))
132
+ return m;
133
+ return { ...m, content: [{ type: "text", text: placeholderFor(item) }] };
134
+ });
135
+ }
136
+ export function renderClearedIndex(cleared, maxChars) {
137
+ const title = "## Cleared tool results (re-fetch if needed)";
138
+ const omitted = (n) => `- (${n} more omitted)`;
139
+ if (cleared.length === 0)
140
+ return title;
141
+ const reserve = omitted(cleared.length).length + 1;
142
+ const lines = [];
143
+ let used = title.length;
144
+ for (const item of cleared) {
145
+ const line = `- ${item.descriptor} (${item.bytes} bytes) — ${oneLine(item.recoverHint, 200)}`;
146
+ if (used + line.length + 1 + reserve > maxChars)
147
+ break;
148
+ lines.push(line);
149
+ used += line.length + 1;
150
+ }
151
+ const out = [title, ...lines];
152
+ if (lines.length < cleared.length)
153
+ out.push(omitted(cleared.length - lines.length));
154
+ return out.join("\n").slice(0, Math.max(maxChars, 0));
155
+ }