@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.
- package/CHANGELOG.md +49 -0
- package/README.md +17 -14
- package/README.vi.md +26 -1
- package/benchmarks/production-v1/spend-control.v1.json +27 -3
- package/benchmarks/production-v1/suite.json +5 -1
- package/catalog/capabilities.json +16 -16
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +23 -31
- package/docs/context-engine.md +5 -3
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +26 -13
- package/docs/languages.json +4 -4
- package/docs/operator-manual-vietnamese.md +21 -41
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +179 -23
- package/docs/release-install-policy.md +6 -6
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/security-threat-model.md +1 -1
- package/docs/subagent-orchestration-capabilities.md +19 -51
- package/docs/subagents-and-multiagent.md +21 -47
- package/docs/task-implementation-contract.md +3 -3
- package/docs/task-lifecycle-vietnamese.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +26 -13
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +89 -95
- package/docs-site/commands.html +1 -1
- package/docs-site/content/benchmark.html +88 -94
- package/docs-site/content/en/benchmark.html +134 -90
- package/docs-site/content/en/index.html +19 -19
- package/docs-site/content/en/runtime.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -1
- package/docs-site/content/en/workflows.html +2 -2
- package/docs-site/content/index.html +19 -19
- package/docs-site/content/runtime.html +4 -4
- package/docs-site/content/whats-new.html +12 -1
- package/docs-site/content/workflows.html +2 -2
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +135 -91
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +20 -20
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +5 -5
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -2
- package/docs-site/en/workflows.html +3 -3
- package/docs-site/index.html +20 -20
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +5 -5
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -2
- package/docs-site/workflows.html +3 -3
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
- package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
- package/evals/fixtures/helper-request.invalid.json +1 -1
- package/evals/fixtures/helper-request.valid.json +1 -1
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
- package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +3 -3
- package/evals/fs5-pilot-protocol.v2.json +8 -8
- package/evals/fs5-pilot-protocol.v3.json +8 -8
- package/evals/fs5-pilot-protocol.v4.json +6 -6
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/evals/runtime-conformance-v1/README.md +43 -0
- package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
- package/evals/runtime-conformance-v1/lane.json +57 -0
- package/evals/runtime-conformance-v1/paid-canary.json +31 -0
- package/evals/runtime-conformance-v1/runner.mjs +456 -0
- package/package.json +1 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
- package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
- package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
- package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
- package/packages/piagent-core/extensions/context-engine.js +1 -11
- package/packages/piagent-core/extensions/context-index-policy.js +4 -0
- package/packages/piagent-core/extensions/context-query-paths.js +57 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
- package/packages/piagent-core/extensions/criterion-graph.js +4 -1
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +4 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
- package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
- package/packages/piagent-core/extensions/task-state.js +69 -8
- package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +42 -2
- package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
- package/packages/piagent-core/prompts/be-to-fe.md +2 -8
- package/packages/piagent-core/prompts/plan.md +1 -1
- package/packages/piagent-core/prompts/platform-improve.md +2 -6
- package/packages/piagent-core/prompts/review.md +2 -7
- package/packages/piagent-core/prompts/scout.md +1 -1
- package/packages/piagent-core/prompts/task.md +4 -3
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
- package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
- package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
- package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
- package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
- package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
- package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
- package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
- package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
- package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
- package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
- package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
- package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
- package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
- package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
- package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
- package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
- package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
- package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
- package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
- package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
- package/packages/piagent-core/subagents/piagent-planner.md +6 -4
- package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
- package/packages/piagent-core/subagents/piagent-scout.md +4 -2
- package/packages/piagent-core/subagents/piagent-worker.md +6 -4
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
- package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
- package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
- package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
- package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
- package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
- package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
- package/packages/piagent-webui/server/transcript-projection.ts +91 -5
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
- package/schemas/project-profile.schema.json +1 -1
- package/schemas/task-contract.schema.json +15 -0
- package/scripts/benchmark-runner-core.mjs +14 -6
- package/scripts/benchmark-runner-finalization.mjs +4 -0
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/team-doctor.sh +1 -1
- 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 : "
|
|
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
|
-
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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
|
|
15
|
-
export const
|
|
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
|
-
|
|
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 >=
|
|
162
|
-
if (state.reservations.filter((item) => item.role === request.role).length >= roleLimit(request.role,
|
|
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 =
|
|
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:
|
|
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 !==
|
|
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:
|
|
91
|
-
modelSelectionSource: "runtime-catalog", effortSelectionSource: "runtime-snapshot", contextBudget: role === "oracle" ?
|
|
92
|
-
ceilings: { timeSeconds:
|
|
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:
|
|
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
|
}
|