@piagent/platform 1.6.0 → 1.6.1

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 (213) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +17 -14
  3. package/README.vi.md +26 -1
  4. package/benchmarks/production-v1/spend-control.v1.json +27 -3
  5. package/benchmarks/production-v1/suite.json +5 -1
  6. package/catalog/capabilities.json +16 -16
  7. package/docs/authority-manifest.md +1 -1
  8. package/docs/auto-delegation-policy.md +38 -73
  9. package/docs/command-reference-vietnamese.md +23 -31
  10. package/docs/context-engine.md +5 -3
  11. package/docs/context-window-policy.md +37 -1
  12. package/docs/distribution-standard.md +7 -7
  13. package/docs/en/architecture.md +26 -13
  14. package/docs/languages.json +4 -4
  15. package/docs/operator-manual-vietnamese.md +21 -41
  16. package/docs/publishing-for-teams.md +5 -0
  17. package/docs/quality-benchmark.md +179 -23
  18. package/docs/release-install-policy.md +6 -6
  19. package/docs/runtime-harness-standard.md +3 -3
  20. package/docs/security-threat-model.md +1 -1
  21. package/docs/subagent-orchestration-capabilities.md +19 -51
  22. package/docs/subagents-and-multiagent.md +21 -47
  23. package/docs/task-implementation-contract.md +3 -3
  24. package/docs/task-lifecycle-vietnamese.md +1 -1
  25. package/docs/team-onboarding.md +2 -2
  26. package/docs/vi/architecture.md +26 -13
  27. package/docs-site/architecture.html +1 -1
  28. package/docs-site/benchmark.html +89 -95
  29. package/docs-site/commands.html +1 -1
  30. package/docs-site/content/benchmark.html +88 -94
  31. package/docs-site/content/en/benchmark.html +134 -90
  32. package/docs-site/content/en/index.html +19 -19
  33. package/docs-site/content/en/runtime.html +4 -4
  34. package/docs-site/content/en/whats-new.html +12 -1
  35. package/docs-site/content/en/workflows.html +2 -2
  36. package/docs-site/content/index.html +19 -19
  37. package/docs-site/content/runtime.html +4 -4
  38. package/docs-site/content/whats-new.html +12 -1
  39. package/docs-site/content/workflows.html +2 -2
  40. package/docs-site/context-engine.html +1 -1
  41. package/docs-site/en/architecture.html +1 -1
  42. package/docs-site/en/benchmark.html +135 -91
  43. package/docs-site/en/commands.html +1 -1
  44. package/docs-site/en/context-engine.html +1 -1
  45. package/docs-site/en/index.html +20 -20
  46. package/docs-site/en/mcp-approval.html +1 -1
  47. package/docs-site/en/mcp-auth.html +1 -1
  48. package/docs-site/en/mcp-servers.html +1 -1
  49. package/docs-site/en/mcp.html +1 -1
  50. package/docs-site/en/permissions.html +1 -1
  51. package/docs-site/en/profiles.html +1 -1
  52. package/docs-site/en/quickstart.html +1 -1
  53. package/docs-site/en/runtime.html +5 -5
  54. package/docs-site/en/scope.html +1 -1
  55. package/docs-site/en/security.html +1 -1
  56. package/docs-site/en/team.html +1 -1
  57. package/docs-site/en/whats-new.html +13 -2
  58. package/docs-site/en/workflows.html +3 -3
  59. package/docs-site/index.html +20 -20
  60. package/docs-site/mcp-approval.html +1 -1
  61. package/docs-site/mcp-auth.html +1 -1
  62. package/docs-site/mcp-servers.html +1 -1
  63. package/docs-site/mcp.html +1 -1
  64. package/docs-site/permissions.html +1 -1
  65. package/docs-site/profiles.html +1 -1
  66. package/docs-site/quickstart.html +1 -1
  67. package/docs-site/runtime.html +5 -5
  68. package/docs-site/scope.html +1 -1
  69. package/docs-site/security.html +1 -1
  70. package/docs-site/team.html +1 -1
  71. package/docs-site/whats-new.html +13 -2
  72. package/docs-site/workflows.html +3 -3
  73. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  74. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  75. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  76. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  77. package/evals/fixtures/helper-request.invalid.json +1 -1
  78. package/evals/fixtures/helper-request.valid.json +1 -1
  79. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  80. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  81. package/evals/fs4-readiness-matrix.v1.json +2 -2
  82. package/evals/fs5-causal-arm.v1.json +2 -2
  83. package/evals/fs5-pilot-protocol.v1.json +3 -3
  84. package/evals/fs5-pilot-protocol.v2.json +8 -8
  85. package/evals/fs5-pilot-protocol.v3.json +8 -8
  86. package/evals/fs5-pilot-protocol.v4.json +6 -6
  87. package/evals/fs5-pilot-protocol.v5.json +7 -6
  88. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  89. package/evals/runtime-conformance-v1/README.md +43 -0
  90. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  91. package/evals/runtime-conformance-v1/lane.json +57 -0
  92. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  93. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  94. package/package.json +1 -1
  95. package/packages/piagent-core/README.md +13 -4
  96. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  97. package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
  98. package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
  99. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  100. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  101. package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
  102. package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
  103. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  104. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
  105. package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
  106. package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
  107. package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
  108. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  109. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  110. package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
  111. package/packages/piagent-core/extensions/context-engine.js +1 -11
  112. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  113. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  114. package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
  115. package/packages/piagent-core/extensions/criterion-graph.js +4 -1
  116. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  117. package/packages/piagent-core/extensions/guard-types.ts +4 -0
  118. package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
  119. package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
  120. package/packages/piagent-core/extensions/task-state.js +69 -8
  121. package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
  122. package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
  123. package/packages/piagent-core/package.json +1 -1
  124. package/packages/piagent-core/policies/base-policy.json +42 -2
  125. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  126. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  127. package/packages/piagent-core/prompts/plan.md +1 -1
  128. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  129. package/packages/piagent-core/prompts/review.md +2 -7
  130. package/packages/piagent-core/prompts/scout.md +1 -1
  131. package/packages/piagent-core/prompts/task.md +4 -3
  132. package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
  133. package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
  134. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
  135. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
  136. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  137. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  138. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  139. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  140. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
  141. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
  142. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  143. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  144. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  145. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  146. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  147. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  148. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
  149. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  150. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
  151. package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
  152. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  153. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  154. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  155. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  156. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  157. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
  158. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  159. package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
  160. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  161. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  162. package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
  163. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  164. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
  165. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  166. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
  167. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  168. package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
  169. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  170. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  171. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  172. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  173. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  174. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  175. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
  176. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  177. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  178. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  179. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  180. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  181. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  182. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  183. package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
  184. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  185. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  186. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  187. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  188. package/packages/piagent-core/subagents/piagent-worker.md +6 -4
  189. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
  190. package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
  191. package/packages/piagent-webui/dist/client/index.html +1 -1
  192. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  193. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  194. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  195. package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
  196. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  197. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  198. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
  199. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
  200. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  201. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  202. package/packages/piagent-webui/server/transcript-projection.ts +91 -5
  203. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  204. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  205. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  206. package/schemas/project-profile.schema.json +1 -1
  207. package/schemas/task-contract.schema.json +15 -0
  208. package/scripts/benchmark-runner-core.mjs +14 -6
  209. package/scripts/benchmark-runner-finalization.mjs +4 -0
  210. package/scripts/configure-subagents.sh +42 -34
  211. package/scripts/helper-evaluation.mjs +1 -1
  212. package/scripts/team-doctor.sh +1 -1
  213. package/scripts/verify-local.sh +5 -0
@@ -5,18 +5,19 @@ import type { RuntimeModelSnapshot } from "../model/runtime-snapshot.ts";
5
5
  import type { SolverDecision, TaskFeatures } from "../solver/solver-types.ts";
6
6
  import { bindRole, type RoleBinding } from "./role-binder.ts";
7
7
  import { AUTOMATIC_OWNED_WORK_CEILINGS, OwnedWorkBudgetController } from "./owned-work-budget.ts";
8
+ import { assessMinimalDelegation, type MinimalDelegationEvidence } from "./minimal-delegation-policy.ts";
8
9
  import { createHelperRequest, defaultRolePolicy, type HelperRequest, type HelperRole } from "./role-policy.ts";
9
10
 
10
11
  export const HELPERS_MODE_VALUES = Object.freeze(["off", "recommend", "on"] as const);
11
12
  export type HelpersMode = typeof HELPERS_MODE_VALUES[number];
12
- export type HelperDecision = { mode: HelpersMode; action: "solo" | "recommend" | "dispatch" | "unavailable" | "blocked"; role: HelperRole | null; reasonCodes: string[]; binding: RoleBinding | null; request: HelperRequest | null };
13
+ export type HelperDecision = { mode: HelpersMode; action: "solo" | "recommend" | "dispatch" | "unavailable" | "blocked"; role: HelperRole | null; reasonCodes: string[]; projectedSavingsRatio: number | null; binding: RoleBinding | null; request: HelperRequest | null };
13
14
  export type HelperDispatchResult = { status: "succeeded" | "failed" | "timeout" | "cancelled"; calls: number; tokens: number; output: string; summary?: string };
14
- export type HelperUsageReceipt = { role: HelperRole; disposition: string; reasonCodes: string[]; requestRef: string; outputDigest: string | null; summary: string | null; mergeOwner: "parent" | null; calls: number; tokens: number; helperUsed: boolean };
15
+ export type HelperUsageReceipt = { role: HelperRole; disposition: string; decision: "dispatch" | "skip"; reasonCodes: string[]; projectedSavingsRatio: number | null; requestRef: string; outputDigest: string | null; summary: string | null; mergeOwner: "parent" | null; calls: number; tokens: number; helperUsed: boolean };
15
16
  type DispatchAdapter = (request: HelperRequest, signal: AbortSignal) => Promise<HelperDispatchResult>;
16
17
  type DispatchOptions = { timeoutMs?: number };
17
18
 
18
19
  function digest(value: unknown): string { return crypto.createHash("sha256").update(String(value ?? "")).digest("hex"); }
19
- export function helpersMode(value = process.env.PIAGENT_HELPERS_MODE): HelpersMode { return HELPERS_MODE_VALUES.includes(value as HelpersMode) ? value as HelpersMode : "recommend"; }
20
+ export function helpersMode(value = process.env.PIAGENT_HELPERS_MODE): HelpersMode { return HELPERS_MODE_VALUES.includes(value as HelpersMode) ? value as HelpersMode : "off"; }
20
21
 
21
22
  export function selectHelperRole(input: { features: TaskFeatures; solver: SolverDecision; confidence?: string; conflictingEvidence?: boolean; repeatedSourceFailure?: boolean; independentReviewUseful?: boolean }): { role: HelperRole | null; reasons: string[] } {
22
23
  if (input.features.riskLane === "tiny") return { role: null, reasons: ["tiny-task-solo"] };
@@ -31,20 +32,23 @@ export class HelperLifecycleRuntime {
31
32
  readonly #budgets: OwnedWorkBudgetController;
32
33
  readonly #active = new Map<string, Set<AbortController>>();
33
34
  constructor(budgets = new OwnedWorkBudgetController()) { this.#budgets = budgets; }
34
- decide(input: { mode: HelpersMode; objective: string; taskId: string; taskRunId: string; sessionId: string; taskScope: string[]; parentAllowedTools: string[]; features: TaskFeatures; solver: SolverDecision; runtime: RuntimeModelSnapshot; catalog: AuthenticatedModelCatalog; confidence?: string; conflictingEvidence?: boolean; repeatedSourceFailure?: boolean; independentReviewUseful?: boolean }): HelperDecision {
35
- if (input.mode === "off") return { mode: input.mode, action: "solo", role: null, reasonCodes: ["helpers-off"], binding: null, request: null };
36
- const selection = selectHelperRole(input); if (!selection.role) return { mode: input.mode, action: "solo", role: null, reasonCodes: selection.reasons, binding: null, request: null };
35
+ decide(input: { mode: HelpersMode; objective: string; taskId: string; taskRunId: string; sessionId: string; taskScope: string[]; parentAllowedTools: string[]; features: TaskFeatures; solver: SolverDecision; runtime: RuntimeModelSnapshot; catalog: AuthenticatedModelCatalog; delegationEvidence?: MinimalDelegationEvidence; confidence?: string; conflictingEvidence?: boolean; repeatedSourceFailure?: boolean; independentReviewUseful?: boolean }): HelperDecision {
36
+ if (input.mode === "off") return { mode: input.mode, action: "solo", role: null, reasonCodes: ["helpers-off", "parent-direct-default"], projectedSavingsRatio: null, binding: null, request: null };
37
+ const selection = selectHelperRole(input); if (!selection.role) return { mode: input.mode, action: "solo", role: null, reasonCodes: selection.reasons, projectedSavingsRatio: null, binding: null, request: null };
38
+ const delegation = assessMinimalDelegation(input.delegationEvidence);
39
+ if (!delegation.eligible) return { mode: input.mode, action: "solo", role: null, reasonCodes: [...selection.reasons, ...delegation.reasonCodes], projectedSavingsRatio: delegation.projectedSavingsRatio, binding: null, request: null };
37
40
  const policy = defaultRolePolicy(selection.role, input.taskScope);
38
41
  const binding = bindRole({ policy, features: input.features, solver: input.solver, runtime: input.runtime, catalog: input.catalog, helperBudgetAvailable: true });
39
- if (binding.disposition !== "recommended") return { mode: input.mode, action: "unavailable", role: selection.role, reasonCodes: [...selection.reasons, ...binding.reasonCodes], binding, request: null };
42
+ if (binding.disposition !== "recommended") return { mode: input.mode, action: "unavailable", role: selection.role, reasonCodes: [...selection.reasons, ...delegation.reasonCodes, ...binding.reasonCodes], projectedSavingsRatio: delegation.projectedSavingsRatio, binding, request: null };
40
43
  const request = createHelperRequest({ policy, objective: input.objective, taskId: input.taskId, taskRunId: input.taskRunId, sessionId: input.sessionId, parentReadScope: input.taskScope, parentWriteScope: input.taskScope, parentAllowedTools: input.parentAllowedTools, requestedReadScope: input.taskScope, requestedWriteScope: [], model: { provider: binding.provider as string, modelId: binding.modelId as string, effort: binding.effort as string, source: binding.mappingVersion }, singleWriterOwnership: null });
41
44
  const action = input.mode === "recommend" ? "recommend" : policy.authority === "read-only" ? "dispatch" : "blocked";
42
- return { mode: input.mode, action, role: selection.role, reasonCodes: action === "blocked" ? [...selection.reasons, "automatic-worker-disabled"] : selection.reasons, binding, request };
45
+ const reasonCodes = [...selection.reasons, ...delegation.reasonCodes];
46
+ return { mode: input.mode, action, role: selection.role, reasonCodes: action === "blocked" ? [...reasonCodes, "automatic-worker-disabled"] : reasonCodes, projectedSavingsRatio: delegation.projectedSavingsRatio, binding, request };
43
47
  }
44
48
  async dispatch(cwd: string, decision: HelperDecision, adapter: DispatchAdapter, options: DispatchOptions = {}): Promise<HelperUsageReceipt> {
45
- if (decision.action !== "dispatch" || !decision.request || !decision.role) return { role: decision.role ?? "scout", disposition: decision.action, reasonCodes: decision.reasonCodes, requestRef: "none", outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
49
+ if (decision.action !== "dispatch" || !decision.request || !decision.role) return { role: decision.role ?? "scout", disposition: decision.action, decision: "skip", reasonCodes: decision.reasonCodes, projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: "none", outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
46
50
  const reservation = this.#budgets.reserve(cwd, decision.request, undefined, AUTOMATIC_OWNED_WORK_CEILINGS);
47
- if (reservation.decision !== "reserved" || !reservation.reservationId) return { role: decision.role, disposition: reservation.reason, reasonCodes: [...decision.reasonCodes, reservation.reason], requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
51
+ if (reservation.decision !== "reserved" || !reservation.reservationId) return { role: decision.role, disposition: reservation.reason, decision: "skip", reasonCodes: [...decision.reasonCodes, reservation.reason], projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
48
52
  const controller = new AbortController(), active = this.#active.get(decision.request.taskRunId) ?? new Set<AbortController>();
49
53
  active.add(controller); this.#active.set(decision.request.taskRunId, active);
50
54
  const ceilingMs = decision.request.ceilings.timeSeconds * 1000;
@@ -66,10 +70,10 @@ export class HelperLifecycleRuntime {
66
70
  const released = this.#budgets.release(cwd, decision.request, reservation.reservationId, terminal, { calls: result.calls, tokens: result.tokens, output: result.output });
67
71
  const calls = Math.min(Math.max(0, Number(result.calls) || 0), decision.request.ceilings.calls + 1);
68
72
  const tokens = Math.min(Math.max(0, Number(result.tokens) || 0), decision.request.contextBudget + 1);
69
- if (!released.accepted) return { role: decision.role, disposition: released.status === "cancelled" ? "cancelled" : "stale-result", reasonCodes: [...decision.reasonCodes, released.reason], requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls, tokens, helperUsed: true };
73
+ if (!released.accepted) return { role: decision.role, disposition: released.status === "cancelled" ? "cancelled" : "stale-result", decision: "dispatch", reasonCodes: [...decision.reasonCodes, released.reason], projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls, tokens, helperUsed: true };
70
74
  const withinBudget = released.reason === "released", succeeded = result.status === "succeeded" && withinBudget;
71
75
  const summary = succeeded && typeof result.summary === "string" ? redactSensitiveText(result.summary).text.replace(/\s+/g, " ").trim().slice(0, 1000) || null : null;
72
- return { role: decision.role, disposition: withinBudget ? result.status : "budget-exceeded", reasonCodes: withinBudget ? decision.reasonCodes : [...decision.reasonCodes, released.reason], requestRef: decision.request.deduplicationKey, outputDigest: succeeded && result.output ? digest(result.output) : null, summary, mergeOwner: summary ? "parent" : null, calls, tokens, helperUsed: true };
76
+ return { role: decision.role, disposition: withinBudget ? result.status : "budget-exceeded", decision: "dispatch", reasonCodes: withinBudget ? decision.reasonCodes : [...decision.reasonCodes, released.reason], projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: decision.request.deduplicationKey, outputDigest: succeeded && result.output ? digest(result.output) : null, summary, mergeOwner: summary ? "parent" : null, calls, tokens, helperUsed: true };
73
77
  }
74
78
  cancelTask(cwd: string, decision: HelperDecision, now = new Date().toISOString()): number {
75
79
  if (!decision.request) return 0;
@@ -0,0 +1,71 @@
1
+ export const MINIMAL_DELEGATION_POLICY_VERSION = "minimal-delegation-v1" as const;
2
+
3
+ export const MINIMAL_DELEGATION_LIMITS = Object.freeze({
4
+ maxConcurrentHelpers: 1,
5
+ maxTotalHelpers: 1,
6
+ maxRetries: 0,
7
+ maxHelperCalls: 8,
8
+ maxTransferTokens: 2_048,
9
+ minimumProjectedSavingsRatio: 0.3
10
+ });
11
+
12
+ export type MinimalDelegationEvidence = {
13
+ source: "runtime-estimate";
14
+ independentWorkstreams: string[];
15
+ canRunWithoutParentOutput: boolean;
16
+ estimatedSoloTokens: number;
17
+ estimatedTotalTokensWithHelper: number;
18
+ transferTokens: number;
19
+ inheritedParentTokens: number;
20
+ retryOfDeterministicFailure: boolean;
21
+ };
22
+
23
+ export type MinimalDelegationAssessment = {
24
+ eligible: boolean;
25
+ projectedSavingsRatio: number | null;
26
+ reasonCodes: string[];
27
+ };
28
+
29
+ function boundedInteger(value: unknown, minimum: number, maximum: number): value is number {
30
+ return Number.isInteger(value) && Number(value) >= minimum && Number(value) <= maximum;
31
+ }
32
+
33
+ function validWorkstreams(value: unknown): value is string[] {
34
+ return Array.isArray(value)
35
+ && value.length >= 2
36
+ && value.length <= 8
37
+ && new Set(value).size === value.length
38
+ && value.every((item) => typeof item === "string" && /^[a-z0-9][a-z0-9:._/-]{0,127}$/i.test(item));
39
+ }
40
+
41
+ /**
42
+ * Helpers are an optimization, not a default reasoning layer. A helper is only
43
+ * eligible when the runtime can show two genuinely independent lanes, a fresh
44
+ * minimal handoff, and at least 30% projected token savings after merge cost.
45
+ * The estimate is advisory, while the one-helper ledger below is enforcing.
46
+ */
47
+ export function assessMinimalDelegation(evidence?: MinimalDelegationEvidence): MinimalDelegationAssessment {
48
+ if (!evidence) return { eligible: false, projectedSavingsRatio: null, reasonCodes: ["delegation-evidence-missing", "parent-direct-default"] };
49
+
50
+ const reasons: string[] = [];
51
+ if (evidence.source !== "runtime-estimate") reasons.push("delegation-estimate-not-runtime-owned");
52
+ if (!validWorkstreams(evidence.independentWorkstreams)) reasons.push("independent-workstreams-not-proven");
53
+ if (evidence.canRunWithoutParentOutput !== true) reasons.push("helper-depends-on-parent-output");
54
+ if (!boundedInteger(evidence.estimatedSoloTokens, 1, 100_000_000)
55
+ || !boundedInteger(evidence.estimatedTotalTokensWithHelper, 1, 100_000_000)) reasons.push("delegation-token-estimate-invalid");
56
+ if (!boundedInteger(evidence.transferTokens, 0, MINIMAL_DELEGATION_LIMITS.maxTransferTokens)) reasons.push("helper-context-transfer-too-large");
57
+ if (evidence.inheritedParentTokens !== 0) reasons.push("parent-history-inheritance-forbidden");
58
+ if (evidence.retryOfDeterministicFailure !== false) reasons.push("deterministic-helper-retry-forbidden");
59
+
60
+ const projectedSavingsRatio = boundedInteger(evidence.estimatedSoloTokens, 1, 100_000_000)
61
+ && boundedInteger(evidence.estimatedTotalTokensWithHelper, 1, 100_000_000)
62
+ ? (evidence.estimatedSoloTokens - evidence.estimatedTotalTokensWithHelper) / evidence.estimatedSoloTokens
63
+ : null;
64
+ if (projectedSavingsRatio === null || projectedSavingsRatio < MINIMAL_DELEGATION_LIMITS.minimumProjectedSavingsRatio) {
65
+ reasons.push("projected-token-savings-below-30pct");
66
+ }
67
+
68
+ return reasons.length
69
+ ? { eligible: false, projectedSavingsRatio, reasonCodes: [...new Set(reasons), "parent-direct-default"] }
70
+ : { eligible: true, projectedSavingsRatio, reasonCodes: ["independent-workstreams-proven", "projected-token-savings-at-least-30pct", "isolated-minimal-context"] };
71
+ }
@@ -7,12 +7,12 @@ import type { HelperRequest, HelperRole } from "./role-policy.ts";
7
7
  import { validateHelperRequest } from "./role-policy.ts";
8
8
 
9
9
  export const OWNED_WORK_BUDGET_VERSION = "owned-work-budget-v1" as const;
10
- // `maxRepairPasses` is declared for the repair lane, which reserves no helper
11
- // role here, so this mechanism never reads it. It is named rather than removed
12
- // because the repair lane is bounded elsewhere; nothing in this file enforces it.
10
+ // Keep the complete ceiling shape for persisted/operator projections, but clamp
11
+ // every caller to one read-only helper and zero retries/writers below.
13
12
  export type OwnedWorkCeilings = Readonly<{ maxConcurrentHelpers: number; maxTotalHelpers: number; maxScoutPasses: number; maxPlannerPasses: number; maxReviewPasses: number; maxOracleCalls: number; maxRepairPasses: number; maxWriters: number }>;
14
- export const DEFAULT_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ maxConcurrentHelpers: 2, maxTotalHelpers: 3, maxScoutPasses: 1, maxPlannerPasses: 1, maxReviewPasses: 1, maxOracleCalls: 1, maxRepairPasses: 1, maxWriters: 1 });
15
- export const AUTOMATIC_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ ...DEFAULT_OWNED_WORK_CEILINGS, maxConcurrentHelpers: 1, maxTotalHelpers: 1 });
13
+ export const ABSOLUTE_OWNED_WORK_CEILINGS: OwnedWorkCeilings = Object.freeze({ maxConcurrentHelpers: 1, maxTotalHelpers: 1, maxScoutPasses: 1, maxPlannerPasses: 1, maxReviewPasses: 1, maxOracleCalls: 1, maxRepairPasses: 0, maxWriters: 0 });
14
+ export const DEFAULT_OWNED_WORK_CEILINGS: OwnedWorkCeilings = ABSOLUTE_OWNED_WORK_CEILINGS;
15
+ export const AUTOMATIC_OWNED_WORK_CEILINGS: OwnedWorkCeilings = ABSOLUTE_OWNED_WORK_CEILINGS;
16
16
  const LOCK_WAIT_MS = 5;
17
17
  const LOCK_WAIT_CEILING_MS = 250;
18
18
  const STALE_LOCK_MS = 30_000;
@@ -29,16 +29,6 @@ function statePath(cwd: string, taskRunId: string): string { return path.join(cw
29
29
  function lockPath(cwd: string, taskRunId: string): string { return `${statePath(cwd, taskRunId)}.lock`; }
30
30
  function empty(request: HelperRequest, now: string): BudgetState { return { version: OWNED_WORK_BUDGET_VERSION, taskId: request.taskId, taskRunId: request.taskRunId, terminal: false, reservations: [], updatedAt: now }; }
31
31
  function active(state: BudgetState): OwnedWorkReservation[] { return state.reservations.filter((item) => item.status === "active"); }
32
- // The per-role ceiling, read from the ceilings the caller passed rather than
33
- // from constants written here. These limits were real but fixed at one apiece,
34
- // so `OwnedWorkCeilings` advertised six numbers that nothing ever read: a
35
- // profile could raise `maxScoutPasses` and be silently ignored, while the two
36
- // neighbouring fields in the same object did take effect. Half-honoured
37
- // configuration is worse than none, because the half that works teaches the
38
- // reader to trust the half that does not.
39
- //
40
- // Every default is one, so this changes no behaviour under the shipped
41
- // ceilings; it makes the object mean what it says.
42
32
  function roleLimit(role: HelperRole, ceilings: OwnedWorkCeilings): number {
43
33
  switch (role) {
44
34
  case "scout": case "retriever": case "researcher": return ceilings.maxScoutPasses;
@@ -51,6 +41,19 @@ function roleLimit(role: HelperRole, ceilings: OwnedWorkCeilings): number {
51
41
  }
52
42
  }
53
43
 
44
+ function effectiveCeilings(requested: OwnedWorkCeilings): OwnedWorkCeilings {
45
+ return {
46
+ maxConcurrentHelpers: Math.min(requested.maxConcurrentHelpers, ABSOLUTE_OWNED_WORK_CEILINGS.maxConcurrentHelpers),
47
+ maxTotalHelpers: Math.min(requested.maxTotalHelpers, ABSOLUTE_OWNED_WORK_CEILINGS.maxTotalHelpers),
48
+ maxScoutPasses: Math.min(requested.maxScoutPasses, ABSOLUTE_OWNED_WORK_CEILINGS.maxScoutPasses),
49
+ maxPlannerPasses: Math.min(requested.maxPlannerPasses, ABSOLUTE_OWNED_WORK_CEILINGS.maxPlannerPasses),
50
+ maxReviewPasses: Math.min(requested.maxReviewPasses, ABSOLUTE_OWNED_WORK_CEILINGS.maxReviewPasses),
51
+ maxOracleCalls: Math.min(requested.maxOracleCalls, ABSOLUTE_OWNED_WORK_CEILINGS.maxOracleCalls),
52
+ maxRepairPasses: 0,
53
+ maxWriters: 0
54
+ };
55
+ }
56
+
54
57
  function read(cwd: string, request: HelperRequest, now: string): BudgetState {
55
58
  const target = statePath(cwd, request.taskRunId); if (!fs.existsSync(target)) return empty(request, now);
56
59
  const resolved = resolveLocalStatePath(cwd, target, { label: "Helper budget state", kind: "file" });
@@ -153,14 +156,19 @@ export function inspectOwnedWorkBudget(cwd: string, taskId: string, taskRunId: s
153
156
  export class OwnedWorkBudgetController {
154
157
  reserve(cwd: string, requestInput: HelperRequest, now = new Date().toISOString(), ceilings: OwnedWorkCeilings = DEFAULT_OWNED_WORK_CEILINGS): { decision: "reserved" | "duplicate" | "blocked"; reason: string; reservationId: string | null; recoveredOrphans: number } {
155
158
  const request = validateHelperRequest(structuredClone(requestInput));
159
+ const limits = effectiveCeilings(ceilings);
156
160
  return withLock(cwd, request.taskRunId, () => {
157
161
  const state = read(cwd, request, now); const recoveredOrphans = recoverOrphans(state, now);
158
- if (state.terminal) return { decision: "blocked", reason: "parent-task-terminal", reservationId: null, recoveredOrphans };
162
+ const settle = (result: { decision: "reserved" | "duplicate" | "blocked"; reason: string; reservationId: string | null; recoveredOrphans: number }) => {
163
+ if (recoveredOrphans > 0) { state.updatedAt = now; write(cwd, state); }
164
+ return result;
165
+ };
166
+ if (state.terminal) return settle({ decision: "blocked", reason: "parent-task-terminal", reservationId: null, recoveredOrphans });
159
167
  const duplicate = state.reservations.find((item) => item.deduplicationKey === request.deduplicationKey && ["active", "succeeded"].includes(item.status));
160
- if (duplicate) return { decision: "duplicate", reason: "equivalent-helper-already-owned", reservationId: duplicate.id, recoveredOrphans };
161
- if (active(state).length >= ceilings.maxConcurrentHelpers || state.reservations.length >= ceilings.maxTotalHelpers) return { decision: "blocked", reason: "helper-budget-exhausted", reservationId: null, recoveredOrphans };
162
- if (state.reservations.filter((item) => item.role === request.role).length >= roleLimit(request.role, ceilings)) return { decision: "blocked", reason: `${request.role}-ceiling-reached`, reservationId: null, recoveredOrphans };
163
- if (request.authority === "single-writer" && active(state).some((item) => item.authority === "single-writer")) return { decision: "blocked", reason: "single-writer-already-owned", reservationId: null, recoveredOrphans };
168
+ if (duplicate) return settle({ decision: "duplicate", reason: "equivalent-helper-already-owned", reservationId: duplicate.id, recoveredOrphans });
169
+ if (active(state).length >= limits.maxConcurrentHelpers || state.reservations.length >= limits.maxTotalHelpers) return settle({ decision: "blocked", reason: "helper-budget-exhausted", reservationId: null, recoveredOrphans });
170
+ if (state.reservations.filter((item) => item.role === request.role).length >= roleLimit(request.role, limits)) return settle({ decision: "blocked", reason: `${request.role}-ceiling-reached`, reservationId: null, recoveredOrphans });
171
+ if (request.authority === "single-writer" && active(state).some((item) => item.authority === "single-writer")) return settle({ decision: "blocked", reason: "single-writer-already-owned", reservationId: null, recoveredOrphans });
164
172
  const id = digest(`${request.taskRunId}:${request.deduplicationKey}:${state.reservations.length}`).slice(0, 32);
165
173
  state.reservations.push({ id, deduplicationKey: request.deduplicationKey, role: request.role, authority: request.authority, status: "active", reservedAt: now, expiresAt: new Date(Date.parse(now) + request.ceilings.timeSeconds * 1000).toISOString(), completedAt: null, usageRef: null });
166
174
  state.updatedAt = now; write(cwd, state); return { decision: "reserved", reason: "budget-reserved", reservationId: id, recoveredOrphans };
@@ -3,7 +3,7 @@ import { globMatchesPath, normalizePathCandidate } from "../../extensions/policy
3
3
 
4
4
  export const ROLE_POLICY_SCHEMA_VERSION = 1 as const;
5
5
  export const ROLE_POLICY_VERSION = "role-policy-v1" as const;
6
- export const HELPER_REQUEST_SCHEMA_VERSION = 1 as const;
6
+ export const HELPER_REQUEST_SCHEMA_VERSION = 2 as const;
7
7
  export const HELPER_ROLES = Object.freeze(["retriever", "scout", "planner", "worker", "reviewer", "oracle", "researcher"] as const);
8
8
  export type HelperRole = typeof HELPER_ROLES[number];
9
9
  export type HelperAuthority = "read-only" | "single-writer";
@@ -15,12 +15,13 @@ export type RolePolicy = {
15
15
  outputSchema: string; stoppingRule: string; approvalRestrictions: string[];
16
16
  };
17
17
  export type HelperRequest = {
18
- schemaVersion: 1; policyVersion: typeof ROLE_POLICY_VERSION; role: HelperRole; objectiveHash: string; objectiveText: string;
18
+ schemaVersion: 2; policyVersion: typeof ROLE_POLICY_VERSION; role: HelperRole; objectiveHash: string; objectiveText: string;
19
19
  taskId: string; taskRunId: string; sessionHash: string; authority: HelperAuthority; readScope: string[]; writeScope: string[];
20
20
  allowedTools: string[]; model: { provider: string; modelId: string; effort: string; source: string } | null;
21
21
  contextBudget: number; ceilings: { timeSeconds: number; calls: number; retries: number }; outputSchema: string;
22
22
  stoppingRule: string; approvalRestrictions: string[]; parentAuthorityDigest: string; singleWriterOwnership: string | null;
23
23
  deduplicationKey: string;
24
+ contextTransfer: { mode: "isolated-minimal"; inheritParentHistory: false; maxSeedTokens: number; estimatedSeedTokens: number };
24
25
  };
25
26
 
26
27
  const READ_TOOLS = ["read", "grep", "find", "ls"];
@@ -28,7 +29,7 @@ const MUTATION_TOOLS = new Set(["edit", "write", "apply_patch", "bash", "contact
28
29
  const HASH = /^[a-f0-9]{64}$/;
29
30
  const REF = /^[a-z0-9][a-z0-9:._/-]{0,255}$/i;
30
31
  const ROLE_FIELDS = new Set(["schemaVersion", "policyVersion", "role", "authority", "enabledByDefault", "readScope", "writeScope", "allowedTools", "modelSelectionSource", "effortSelectionSource", "contextBudget", "ceilings", "outputSchema", "stoppingRule", "approvalRestrictions"]);
31
- const REQUEST_FIELDS = new Set(["schemaVersion", "policyVersion", "role", "objectiveHash", "objectiveText", "taskId", "taskRunId", "sessionHash", "authority", "readScope", "writeScope", "allowedTools", "model", "contextBudget", "ceilings", "outputSchema", "stoppingRule", "approvalRestrictions", "parentAuthorityDigest", "singleWriterOwnership", "deduplicationKey"]);
32
+ const REQUEST_FIELDS = new Set(["schemaVersion", "policyVersion", "role", "objectiveHash", "objectiveText", "taskId", "taskRunId", "sessionHash", "authority", "readScope", "writeScope", "allowedTools", "model", "contextBudget", "ceilings", "outputSchema", "stoppingRule", "approvalRestrictions", "parentAuthorityDigest", "singleWriterOwnership", "deduplicationKey", "contextTransfer"]);
32
33
 
33
34
  function object(value: unknown): Record<string, any> | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : undefined; }
34
35
  function exact(value: Record<string, any>, fields: Set<string>, label: string): string[] { return [...Object.keys(value).filter((field) => !fields.has(field)).map((field) => `${label} unknown field: ${field}`), ...[...fields].filter((field) => !(field in value)).map((field) => `${label} missing field: ${field}`)]; }
@@ -36,6 +37,15 @@ function strings(value: unknown, max: number, itemMax = 300): value is string[]
36
37
  function ceilings(value: unknown): boolean { const item = object(value); return Boolean(item && exact(item, new Set(["timeSeconds", "calls", "retries"]), "ceilings").length === 0 && Number.isInteger(item.timeSeconds) && item.timeSeconds >= 1 && item.timeSeconds <= 3600 && Number.isInteger(item.calls) && item.calls >= 1 && item.calls <= 100 && Number.isInteger(item.retries) && item.retries >= 0 && item.retries <= 3); }
37
38
  function digest(value: unknown): string { return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
38
39
  function boundedObjective(value: unknown): string { return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, 1000); }
40
+ function estimatedSeedTokens(value: Record<string, any>): number {
41
+ const seed = {
42
+ schemaVersion: value.schemaVersion, policyVersion: value.policyVersion, role: value.role, objectiveText: value.objectiveText,
43
+ taskId: value.taskId, taskRunId: value.taskRunId, authority: value.authority, readScope: value.readScope, writeScope: value.writeScope,
44
+ allowedTools: value.allowedTools, model: value.model, contextBudget: value.contextBudget, ceilings: value.ceilings,
45
+ outputSchema: value.outputSchema, stoppingRule: value.stoppingRule, approvalRestrictions: value.approvalRestrictions
46
+ };
47
+ return Math.ceil(JSON.stringify(seed).length / 4);
48
+ }
39
49
  function safeScopePattern(value: string): boolean {
40
50
  const normalized = normalizePathCandidate(value);
41
51
  return Boolean(normalized && !/^\/|^[A-Za-z]:\//.test(normalized) && !normalized.split("/").some((part) => part === "." || part === "..") && !/[?\[\]{}\0]/.test(normalized));
@@ -69,7 +79,7 @@ export function validateRolePolicy(input: unknown): RolePolicy { const errors =
69
79
  export function helperRequestValidationErrors(input: unknown): string[] {
70
80
  const value = object(input); if (!value) return ["helper request must be an object"];
71
81
  const errors = exact(value, REQUEST_FIELDS, "helper request");
72
- if (value.schemaVersion !== 1 || value.policyVersion !== ROLE_POLICY_VERSION || !HELPER_ROLES.includes(value.role) || !HASH.test(String(value.objectiveHash)) || !HASH.test(String(value.sessionHash)) || !HASH.test(String(value.parentAuthorityDigest)) || !HASH.test(String(value.deduplicationKey))) errors.push("helper identity/digests are invalid");
82
+ if (value.schemaVersion !== 2 || value.policyVersion !== ROLE_POLICY_VERSION || !HELPER_ROLES.includes(value.role) || !HASH.test(String(value.objectiveHash)) || !HASH.test(String(value.sessionHash)) || !HASH.test(String(value.parentAuthorityDigest)) || !HASH.test(String(value.deduplicationKey))) errors.push("helper identity/digests are invalid");
73
83
  if (typeof value.objectiveText !== "string" || value.objectiveText.length < 3 || value.objectiveText.length > 1000 || !REF.test(String(value.taskId)) || !REF.test(String(value.taskRunId))) errors.push("helper objective/task identity is invalid");
74
84
  if (!["read-only", "single-writer"].includes(value.authority) || !strings(value.readScope, 100) || !strings(value.writeScope, 100) || !value.readScope.every(safeScopePattern) || !value.writeScope.every(safeScopePattern) || !strings(value.allowedTools, 32) || !strings(value.approvalRestrictions, 16)) errors.push("helper authority collections are invalid");
75
85
  if (value.authority === "read-only" && (value.writeScope.length > 0 || value.allowedTools.some((tool: string) => MUTATION_TOOLS.has(tool)))) errors.push("read-only helper cannot mutate");
@@ -79,6 +89,12 @@ export function helperRequestValidationErrors(input: unknown): string[] {
79
89
  if (typeof value.outputSchema !== "string" || value.outputSchema.length > 200 || typeof value.stoppingRule !== "string" || value.stoppingRule.length > 500) errors.push("helper output/stopping contract is invalid");
80
90
  const model = value.model === null ? null : object(value.model);
81
91
  if (value.model !== null && (!model || exact(model, new Set(["provider", "modelId", "effort", "source"]), "model").length || ![model.provider, model.modelId, model.effort, model.source].every((item) => typeof item === "string" && item.length > 0 && item.length <= 160))) errors.push("helper model binding is invalid");
92
+ const contextTransfer = object(value.contextTransfer);
93
+ if (!contextTransfer || exact(contextTransfer, new Set(["mode", "inheritParentHistory", "maxSeedTokens", "estimatedSeedTokens"]), "context transfer").length
94
+ || contextTransfer.mode !== "isolated-minimal" || contextTransfer.inheritParentHistory !== false
95
+ || !Number.isInteger(contextTransfer.maxSeedTokens) || contextTransfer.maxSeedTokens < 256 || contextTransfer.maxSeedTokens > 2048
96
+ || !Number.isInteger(contextTransfer.estimatedSeedTokens) || contextTransfer.estimatedSeedTokens !== estimatedSeedTokens(value)
97
+ || contextTransfer.estimatedSeedTokens > contextTransfer.maxSeedTokens) errors.push("helper context transfer must be isolated and minimal");
82
98
  return errors;
83
99
  }
84
100
  export function validateHelperRequest(input: unknown): HelperRequest { const errors = helperRequestValidationErrors(input); if (errors.length) throw new Error(errors.join("; ")); return input as HelperRequest; }
@@ -87,9 +103,9 @@ export function defaultRolePolicy(role: HelperRole, scope: string[] = ["**"]): R
87
103
  const readOnly = role !== "worker";
88
104
  const policy: RolePolicy = {
89
105
  schemaVersion: 1, policyVersion: ROLE_POLICY_VERSION, role, authority: readOnly ? "read-only" : "single-writer",
90
- enabledByDefault: readOnly, readScope: [...scope], writeScope: [], allowedTools: readOnly ? [...READ_TOOLS] : [...READ_TOOLS, "bash", "edit", "write", "apply_patch", "contact_supervisor"],
91
- modelSelectionSource: "runtime-catalog", effortSelectionSource: "runtime-snapshot", contextBudget: role === "oracle" ? 12000 : role === "reviewer" ? 9000 : 6000,
92
- ceilings: { timeSeconds: role === "oracle" ? 900 : 600, calls: role === "retriever" ? 20 : 40, retries: 0 },
106
+ enabledByDefault: false, readScope: [...scope], writeScope: [], allowedTools: readOnly ? [...READ_TOOLS] : [...READ_TOOLS, "bash", "edit", "write", "apply_patch", "contact_supervisor"],
107
+ modelSelectionSource: "runtime-catalog", effortSelectionSource: "runtime-snapshot", contextBudget: role === "oracle" ? 8000 : role === "reviewer" || role === "planner" ? 6000 : 4000,
108
+ ceilings: { timeSeconds: 300, calls: 8, retries: 0 },
93
109
  outputSchema: `${role}-result-v1`, stoppingRule: "Stop when the bounded objective is answered or evidence is insufficient; return uncertainty.",
94
110
  approvalRestrictions: ["no-external-write", "no-destructive-action", "no-permission-expansion"]
95
111
  };
@@ -103,11 +119,13 @@ export function createHelperRequest(input: { policy: RolePolicy; objective: stri
103
119
  if (policy.allowedTools.some((tool) => !input.parentAllowedTools.includes(tool))) throw new Error("helper request cannot broaden parent tools");
104
120
  const authority = policy.authority;
105
121
  const request: HelperRequest = {
106
- schemaVersion: 1, policyVersion: ROLE_POLICY_VERSION, role: policy.role, objectiveHash: digest(objectiveText), objectiveText,
122
+ schemaVersion: 2, policyVersion: ROLE_POLICY_VERSION, role: policy.role, objectiveHash: digest(objectiveText), objectiveText,
107
123
  taskId: input.taskId, taskRunId: input.taskRunId, sessionHash: digest(input.sessionId), authority, readScope: [...readScope], writeScope: [...writeScope], allowedTools: [...policy.allowedTools],
108
124
  model: input.model ?? null, contextBudget: policy.contextBudget, ceilings: { ...policy.ceilings }, outputSchema: policy.outputSchema, stoppingRule: policy.stoppingRule,
109
125
  approvalRestrictions: [...policy.approvalRestrictions], parentAuthorityDigest: digest({ read: input.parentReadScope, write: input.parentWriteScope, tools: input.parentAllowedTools }),
110
- singleWriterOwnership: input.singleWriterOwnership ?? null, deduplicationKey: digest({ role: policy.role, objective: objectiveText, readScope, writeScope })
126
+ singleWriterOwnership: input.singleWriterOwnership ?? null, deduplicationKey: digest({ role: policy.role, objective: objectiveText, readScope, writeScope }),
127
+ contextTransfer: { mode: "isolated-minimal", inheritParentHistory: false, maxSeedTokens: 2048, estimatedSeedTokens: 0 }
111
128
  };
129
+ request.contextTransfer.estimatedSeedTokens = estimatedSeedTokens(request);
112
130
  return validateHelperRequest(request);
113
131
  }