@piagent/platform 1.5.5 → 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 +166 -0
- package/README.md +25 -21
- package/README.vi.md +34 -5
- package/adapters/web-frontend/profile.json +2 -4
- package/architecture/layers.json +18 -3
- package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
- package/benchmarks/deep-logic-v1/grade.mjs +272 -10
- package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
- package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
- package/benchmarks/deep-logic-v1/project/package.json +1 -1
- package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
- package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
- package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
- package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
- package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
- package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
- package/benchmarks/deep-logic-v1/suite.json +51 -3
- package/benchmarks/deep-logic-v1/variant.mjs +22 -1
- package/benchmarks/production-v1/spend-control.v1.json +92 -0
- package/benchmarks/production-v1/suite.json +46 -3
- package/catalog/capabilities.json +18 -18
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +27 -35
- package/docs/context-engine.md +12 -4
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +27 -13
- package/docs/languages.json +4 -4
- package/docs/model-options.md +8 -5
- package/docs/operator-manual-vietnamese.md +27 -45
- package/docs/project-adapters.md +2 -2
- package/docs/publishing-for-teams.md +10 -0
- package/docs/quality-benchmark.md +427 -62
- package/docs/release-install-policy.md +10 -9
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/runtime-policy-design.md +4 -4
- package/docs/runtime-quality-baseline.md +13 -6
- package/docs/security-threat-model.md +2 -2
- 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 +3 -3
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +7 -2
- package/docs/vi/architecture.md +27 -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/session-live-state-v1.invalid.json +18 -0
- package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
- 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 +3 -3
- 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 +7 -6
- package/evals/fs5-pilot-protocol.v5.json +9 -7
- 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 +3 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
- package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
- package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
- package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
- package/packages/piagent-core/benchmark/benchmark-core.js +736 -664
- package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
- package/packages/piagent-core/benchmark/benchmark-forensics.js +441 -4
- package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
- package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
- package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
- package/packages/piagent-core/benchmark/benchmark-preflight.js +5 -1
- package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +252 -0
- package/packages/piagent-core/benchmark/benchmark-report.js +130 -12
- package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
- package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +979 -0
- package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
- package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
- package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
- package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
- package/packages/piagent-core/benchmark/benchmark-suite.js +149 -2
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +380 -0
- package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +322 -0
- package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
- package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
- package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
- package/packages/piagent-core/extensions/acceptance-receipt.js +123 -59
- package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
- package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
- package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
- package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
- package/packages/piagent-core/extensions/context-engine.js +149 -156
- package/packages/piagent-core/extensions/context-evidence.js +48 -0
- package/packages/piagent-core/extensions/context-import-links.js +296 -0
- 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/context-telemetry-integrity.js +65 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +435 -7
- package/packages/piagent-core/extensions/criterion-graph.js +82 -9
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +6 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +420 -260
- package/packages/piagent-core/extensions/task-contract-view.js +17 -3
- package/packages/piagent-core/extensions/task-lifecycle.js +39 -2
- package/packages/piagent-core/extensions/task-state.js +109 -142
- package/packages/piagent-core/extensions/verification-intelligence.js +52 -13
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +193 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +44 -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/context/context-delivery.ts +103 -0
- package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
- package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
- package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
- package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
- package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
- package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +12 -12
- package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -1
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +107 -3
- package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +134 -166
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
- package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -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-compaction-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +70 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +73 -9
- package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
- package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
- package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
- 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/activity-inspector.ts +7 -2
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +7 -2
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +22 -11
- package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +9 -5
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
- package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
- 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 +60 -13
- package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +82 -7
- 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 +50 -15
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +61 -25
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +169 -0
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +108 -103
- 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 +161 -5
- 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/apply-patch-tool.ts +500 -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/trajectory/trajectory-runtime.ts +2 -1
- 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 +207 -38
- package/packages/piagent-core/security/sensitive-data.js +13 -218
- package/packages/piagent-core/security/sensitive-project-file.js +42 -0
- package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
- package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
- package/packages/piagent-core/security/sensitive-text.js +234 -0
- 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 +7 -5
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-CgOEyRPG.js +176 -0
- 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-events.ts +109 -5
- package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +122 -20
- package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
- package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
- package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
- 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-start.ts +5 -1
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +165 -0
- package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +141 -125
- 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/loopback-server.ts +8 -0
- package/packages/piagent-webui/server/transcript-projection.ts +113 -9
- package/packages/piagent-webui/shared/text-visibility.ts +10 -0
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
- 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 +27 -1
- package/scripts/benchmark-runner-core.mjs +367 -363
- package/scripts/benchmark-runner-finalization.mjs +313 -0
- package/scripts/benchmark-runner-host-readiness.mjs +94 -0
- package/scripts/benchmark-runner-support.mjs +200 -0
- package/scripts/benchmark-session.mjs +95 -41
- package/scripts/configure-model-scope.sh +4 -3
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +2 -2
- package/scripts/install-global.sh +2 -2
- package/scripts/pi-usage-history.mjs +37 -10
- package/scripts/setup.sh +2 -2
- package/scripts/team-doctor.sh +1 -1
- package/scripts/verify-local.sh +10 -1
- package/templates/global/settings.json +2 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
|
@@ -8,6 +8,8 @@ import { workingTreeEvidenceDigest, workingTreeSnapshotUsesCurrentAlgorithm } fr
|
|
|
8
8
|
import { inspectTaskContinuationBudget } from "../recovery/continuation-budget.ts";
|
|
9
9
|
import { readHandoffProjection } from "../recovery/handoff-projection.ts";
|
|
10
10
|
import type { ActivityInspectorEvent, CurrentActivity } from "../product/activity-inspector.ts";
|
|
11
|
+
import { recoveredToolCalls } from "./activity-recovery.ts";
|
|
12
|
+
import { classifyToolFailure, handledToolFailure } from "./tool-failure-classification.ts";
|
|
11
13
|
import { projectCriteriaFileVerifier, type CriteriaLinkProjection } from "./criteria-links.ts";
|
|
12
14
|
import { collectSourceChangeViews, type SourceChangeDocument, type WebUiIdentity } from "./source-change-projection.ts";
|
|
13
15
|
|
|
@@ -82,6 +84,60 @@ function scopedEvents(events: ActivityInspectorEvent[], sessionId: string, task?
|
|
|
82
84
|
return [...deduped.values()].sort((left, right) => String(left.recordedAt).localeCompare(String(right.recordedAt))).slice(-2_000);
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
type CanonicalToolResult = { content: unknown; isError: boolean; toolName: string; recordedAt?: string };
|
|
88
|
+
|
|
89
|
+
function canonicalToolEvidence(sessionEntries: unknown[]): {
|
|
90
|
+
inputs: Map<string, unknown>;
|
|
91
|
+
results: Map<string, CanonicalToolResult>;
|
|
92
|
+
} {
|
|
93
|
+
const inputs = new Map<string, unknown>(), results = new Map<string, CanonicalToolResult>();
|
|
94
|
+
for (const entry of sessionEntries) {
|
|
95
|
+
if (!entry || typeof entry !== "object" || (entry as any).type !== "message") continue;
|
|
96
|
+
const message = (entry as any).message;
|
|
97
|
+
if (message?.role === "assistant" && Array.isArray(message.content)) {
|
|
98
|
+
for (const block of message.content) if (block?.type === "toolCall" && typeof block.id === "string") inputs.set(block.id, block.arguments);
|
|
99
|
+
}
|
|
100
|
+
if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
|
|
101
|
+
const recordedAt = TIMESTAMP.test(String((entry as any).timestamp ?? message.timestamp ?? ""))
|
|
102
|
+
? String((entry as any).timestamp ?? message.timestamp) : undefined;
|
|
103
|
+
results.set(message.toolCallId, { content: message.content, isError: message.isError === true,
|
|
104
|
+
toolName: typeof message.toolName === "string" ? message.toolName : "", recordedAt });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { inputs, results };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function reconciledToolResults(events: ActivityInspectorEvent[], evidence: ReturnType<typeof canonicalToolEvidence>): ActivityInspectorEvent[] {
|
|
111
|
+
const resultIds = new Set(events.filter((event) => event.event === "tool_result" && event.toolCallId).map((event) => event.toolCallId as string));
|
|
112
|
+
const reconciled = events.map((event) => {
|
|
113
|
+
if (event.event !== "tool_result" || !event.toolCallId) return event;
|
|
114
|
+
const raw = evidence.results.get(event.toolCallId);
|
|
115
|
+
if (!raw) return event;
|
|
116
|
+
const reasonCode = classifyToolFailure(raw.toolName || event.toolName, raw.isError, raw.content, evidence.inputs.get(event.toolCallId));
|
|
117
|
+
if (!event.isError) {
|
|
118
|
+
// A newer explicit success remains authoritative over an older canonical
|
|
119
|
+
// hard error. The exception is a provider-level helper soft failure:
|
|
120
|
+
// legacy telemetry could only see `isError=false`, while the canonical
|
|
121
|
+
// result proves that no useful child actually ran.
|
|
122
|
+
if (raw.isError || !["helper-dispatch-rejected", "helper-insufficient-evidence"].includes(String(reasonCode))) return event;
|
|
123
|
+
return { ...event, isError: true, reasonCode };
|
|
124
|
+
}
|
|
125
|
+
return reasonCode ? { ...event, reasonCode } : event;
|
|
126
|
+
});
|
|
127
|
+
for (const call of events) {
|
|
128
|
+
if (call.event !== "tool_call" || !call.toolCallId || resultIds.has(call.toolCallId)) continue;
|
|
129
|
+
const raw = evidence.results.get(call.toolCallId);
|
|
130
|
+
if (!raw) continue;
|
|
131
|
+
const reasonCode = classifyToolFailure(raw.toolName || call.toolName, raw.isError, raw.content, evidence.inputs.get(call.toolCallId));
|
|
132
|
+
const effectiveError = raw.isError || Boolean(reasonCode && !handledToolFailure(reasonCode));
|
|
133
|
+
reconciled.push({ activityId: `canonical-result:${call.toolCallId}`, event: "tool_result",
|
|
134
|
+
recordedAt: raw.recordedAt ?? call.recordedAt, sessionId: call.sessionId, taskRunId: call.taskRunId,
|
|
135
|
+
toolCallId: call.toolCallId, toolName: raw.toolName || call.toolName, isError: effectiveError,
|
|
136
|
+
...(reasonCode ? { reasonCode } : {}) });
|
|
137
|
+
}
|
|
138
|
+
return reconciled.sort((left, right) => String(left.recordedAt ?? "").localeCompare(String(right.recordedAt ?? "")));
|
|
139
|
+
}
|
|
140
|
+
|
|
85
141
|
function sourceSummary(document: SourceChangeDocument | null, view: "task" | "working-tree" | "staged") {
|
|
86
142
|
if (!document || document.availability.state === "unavailable") return {
|
|
87
143
|
view, base: view === "task" ? "task-baseline" : "HEAD", state: "unavailable", revision: null,
|
|
@@ -109,21 +165,26 @@ function sourceSummary(document: SourceChangeDocument | null, view: "task" | "wo
|
|
|
109
165
|
function activityProjection(events: ActivityInspectorEvent[], current: CurrentActivity[]) {
|
|
110
166
|
const results = new Map(events.filter((event) => event.event === "tool_result").map((event) => [event.toolCallId, event]));
|
|
111
167
|
const decisions = new Map(events.filter((event) => event.event === "tool_decision").map((event) => [event.toolCallId, event]));
|
|
168
|
+
const recoveries = recoveredToolCalls(events);
|
|
112
169
|
const calls = events.filter((event) => event.event === "tool_call").slice(-200);
|
|
113
170
|
const projected = calls.map((call) => {
|
|
114
171
|
const result = results.get(call.toolCallId), decision = decisions.get(call.toolCallId);
|
|
115
172
|
const blocked = decision?.decision === "blocked";
|
|
116
|
-
const
|
|
117
|
-
const
|
|
173
|
+
const handled = Boolean(result && handledToolFailure(result.reasonCode));
|
|
174
|
+
const failed = !blocked && !handled && Boolean(result && (result.isError === true || (typeof result.exitCode === "number" && result.exitCode !== 0)));
|
|
175
|
+
const recovery = call.toolCallId ? recoveries.get(call.toolCallId) : undefined;
|
|
176
|
+
const state = blocked ? "blocked" : !result ? "running" : failed && !recovery ? "failed" : "passed";
|
|
118
177
|
const toolName = display(call.toolName ?? "unknown", 80).replace(/[^A-Za-z0-9._:@~-]/g, "-") || "unknown";
|
|
119
178
|
const rawId = String(call.toolCallId ?? call.activityId ?? `${call.recordedAt}:${toolName}`);
|
|
120
179
|
const command = display(call.command ?? call.targetPath ?? toolName, 65_536);
|
|
121
180
|
return {
|
|
122
181
|
activityRef: token("activity", [rawId, call.recordedAt]), kind: ["bash", "shell", "exec"].includes(toolName) ? "command" : "tool", state,
|
|
123
|
-
label: display(state === "running" ? `${toolName} running` : `${toolName} ${state}`, 500), preview: command,
|
|
182
|
+
label: display(recovery ? `${toolName} recovered` : handled ? `${toolName} warning` : state === "running" ? `${toolName} running` : `${toolName} ${state}`, 500), preview: command,
|
|
124
183
|
toolCallId: token("tool", rawId), toolName, commandDigest: call.command ? `sha256:${hash(call.command)}` : null,
|
|
125
|
-
logRef: null, exitCode: typeof result?.exitCode === "number" ? result.exitCode : null,
|
|
126
|
-
|
|
184
|
+
logRef: null, exitCode: recovery?.exitCode ?? (typeof result?.exitCode === "number" ? result.exitCode : null),
|
|
185
|
+
exitCodeExact: recovery?.exitCodeExact ?? (result?.exitCodeExact === true),
|
|
186
|
+
startedAt: timestamp(call.recordedAt), finishedAt: recovery ? timestamp(recovery.recoveredAt, timestamp(call.recordedAt))
|
|
187
|
+
: result || blocked ? timestamp(result?.recordedAt ?? decision?.recordedAt, timestamp(call.recordedAt)) : null
|
|
127
188
|
};
|
|
128
189
|
});
|
|
129
190
|
const callIds = new Set(calls.map((call) => String(call.toolCallId ?? "")));
|
|
@@ -274,12 +335,15 @@ export async function buildWebUiInspectionProjection(input: {
|
|
|
274
335
|
const taskRevision = input.task ? webUiTaskRevision(input.task) : null;
|
|
275
336
|
const views = await collectSourceChangeViews({ cwd: input.cwd, identity, generatedAt, taskRevision,
|
|
276
337
|
isProtectedPath: (_root, repoPath) => Boolean(matchesProtectedPath(repoPath, effectiveProtectedPaths)) });
|
|
277
|
-
const
|
|
338
|
+
const canonicalEvidence = canonicalToolEvidence(input.sessionEntries ?? []);
|
|
339
|
+
const events = reconciledToolResults(scopedEvents(input.events ?? [], input.sessionId, input.task), canonicalEvidence);
|
|
278
340
|
const linked = projectCriteriaFileVerifier({ cwd: input.cwd, task: input.task, sourceViews: views, currentSnapshot,
|
|
279
341
|
protectedPaths: input.protectedPaths, events, at: new Date(generatedAt) });
|
|
280
342
|
const sourceChanges = { task: sourceSummary(linked.sourceViews.task, "task"), workingTree: sourceSummary(linked.sourceViews.workingTree, "working-tree"), staged: sourceSummary(linked.sourceViews.staged, "staged") };
|
|
281
343
|
const currentDigest = workingTreeSnapshotUsesCurrentAlgorithm(currentSnapshot) ? workingTreeEvidenceDigest(currentSnapshot) : null;
|
|
282
|
-
const current = input.current ?? []
|
|
344
|
+
const current = (input.current ?? []).filter((item) => (item.status ?? "running") !== "running" || !canonicalEvidence.results.has(item.toolCallId));
|
|
345
|
+
const runningCurrent = current.find((item) => (item.status ?? "running") === "running");
|
|
346
|
+
const controlState = input.task?.trace.outcome === "pending" ? "active" as const : "terminal" as const;
|
|
283
347
|
const context = contextProjection(input.contextUsage, generatedAt), usageFacts = usage(input.sessionEntries ?? []);
|
|
284
348
|
const eventCursor = input.eventCursor ?? token("event-cursor", events.map((event) => event.activityId ?? [event.event, event.toolCallId, event.recordedAt]));
|
|
285
349
|
const runtimeRevision = token("runtime-rev", [taskRevision, sourceChanges.workingTree.revision, sourceChanges.staged.revision, eventCursor, current]);
|
|
@@ -300,8 +364,8 @@ export async function buildWebUiInspectionProjection(input: {
|
|
|
300
364
|
capabilities: capabilities(identity, generatedAt, input.resyncRequired === true, input.eventReplay),
|
|
301
365
|
session: {
|
|
302
366
|
connectionState: input.resyncRequired ? "resync-required" : "connected", connectionReason: input.resyncRequired ? "event-replay-gap" : null, displayName: input.task?.sessionName ?? null,
|
|
303
|
-
operation:
|
|
304
|
-
? { liveness: "running", operationRef: null, hostPhase: { state: "known", value: "tool", evidence: "derived", reasonCode: null }, startedAt: timestamp(
|
|
367
|
+
operation: runningCurrent
|
|
368
|
+
? { liveness: "running", operationRef: null, hostPhase: { state: "known", value: "tool", evidence: "derived", reasonCode: null }, startedAt: timestamp(runningCurrent.startedAt, generatedAt), settledAt: null, reasonCode: null }
|
|
305
369
|
: { liveness: "idle", operationRef: null, hostPhase: { state: "known", value: "idle", evidence: "derived", reasonCode: null }, startedAt: null, settledAt: null, reasonCode: null },
|
|
306
370
|
controlState: input.task ? controlState : "active", taskOutcome: input.task?.trace.outcome ?? null, approvalState: approvals.state,
|
|
307
371
|
verificationState: linked.verification.state, permissionProfile: { state: "unavailable", value: null, evidence: null, reasonCode: "permission-profile-not-projected" },
|
|
@@ -50,14 +50,22 @@ function positiveInteger(value: unknown): number | null {
|
|
|
50
50
|
return Number.isInteger(value) && Number(value) > 0 && Number(value) <= 10_000_000 ? Number(value) : null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
53
|
+
const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
54
|
+
|
|
55
|
+
function thinkingLevels(value: ModelLike): string[] | null {
|
|
56
|
+
if (typeof value.reasoning !== "boolean") return null;
|
|
57
|
+
if (!value.reasoning) return ["off"];
|
|
58
|
+
const mapping = value.thinkingLevelMap && typeof value.thinkingLevelMap === "object" && !Array.isArray(value.thinkingLevelMap)
|
|
59
|
+
? value.thinkingLevelMap as Record<string, unknown>
|
|
60
|
+
: {};
|
|
61
|
+
// Match Pi's getSupportedThinkingLevels semantics. Reasoning models support
|
|
62
|
+
// the standard host levels through `high` unless a model explicitly marks a
|
|
63
|
+
// level null. `xhigh` and `max` are opt-in capabilities.
|
|
64
|
+
return PI_THINKING_LEVELS.filter((level) => {
|
|
65
|
+
const mapped = mapping[level];
|
|
66
|
+
if (mapped === null) return false;
|
|
67
|
+
return level === "xhigh" || level === "max" ? mapped !== undefined : true;
|
|
68
|
+
});
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
function boundedModel(value: ModelLike): AuthenticatedModelCatalogEntry | undefined {
|
|
@@ -71,7 +79,7 @@ function boundedModel(value: ModelLike): AuthenticatedModelCatalogEntry | undefi
|
|
|
71
79
|
contextWindow: positiveInteger(value.contextWindow),
|
|
72
80
|
reasoning: typeof value.reasoning === "boolean" ? value.reasoning : null,
|
|
73
81
|
imageInput: input ? input.includes("image") : null,
|
|
74
|
-
supportedThinkingLevels: thinkingLevels(value
|
|
82
|
+
supportedThinkingLevels: thinkingLevels(value)
|
|
75
83
|
};
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type ProviderPayload = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
const GPT56_CODEX_MODEL = /^gpt-5\.6-(?:luna|sol|terra)$/;
|
|
4
|
+
const PROVIDER_EFFORT_BY_HOST_LEVEL: Readonly<Record<string, string>> = Object.freeze({
|
|
5
|
+
off: "none",
|
|
6
|
+
minimal: "low",
|
|
7
|
+
low: "low",
|
|
8
|
+
medium: "medium",
|
|
9
|
+
high: "high",
|
|
10
|
+
xhigh: "xhigh",
|
|
11
|
+
max: "max"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type OpenAiCodexReasoningNormalization = {
|
|
15
|
+
payload: unknown;
|
|
16
|
+
applicable: boolean;
|
|
17
|
+
changed: boolean;
|
|
18
|
+
hostThinkingLevel: string;
|
|
19
|
+
expectedProviderEffort: string | null;
|
|
20
|
+
providerEffort: string | null;
|
|
21
|
+
reasonCode: "not-applicable" | "observed" | "off-normalized-to-none" | "minimal-normalized-to-low" | "provider-effort-normalized";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function record(value: unknown): ProviderPayload | null {
|
|
25
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as ProviderPayload : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function effortFromPayload(payload: ProviderPayload): string | null {
|
|
29
|
+
const reasoning = record(payload.reasoning);
|
|
30
|
+
return typeof reasoning?.effort === "string" ? reasoning.effort : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compatibility bridge for the pinned Pi host's OpenAI Codex serializer.
|
|
35
|
+
*
|
|
36
|
+
* Pi names the provider's `none` effort `off`. Pi 0.84.1 currently drops
|
|
37
|
+
* `off` before its request builder, which omits the reasoning object and lets
|
|
38
|
+
* GPT-5.6 use its server default. Enforce the same-model/effort invariant for
|
|
39
|
+
* every host level, while keeping the correction deliberately narrow to the
|
|
40
|
+
* three authenticated GPT-5.6 Codex models whose capability map we audit.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeOpenAiCodexReasoningPayload(input: {
|
|
43
|
+
payload: unknown;
|
|
44
|
+
provider: unknown;
|
|
45
|
+
modelId: unknown;
|
|
46
|
+
hostThinkingLevel: unknown;
|
|
47
|
+
}): OpenAiCodexReasoningNormalization {
|
|
48
|
+
const provider = String(input.provider ?? "");
|
|
49
|
+
const modelId = String(input.modelId ?? "");
|
|
50
|
+
const hostThinkingLevel = String(input.hostThinkingLevel ?? "");
|
|
51
|
+
const payload = record(input.payload);
|
|
52
|
+
const expectedProviderEffort = PROVIDER_EFFORT_BY_HOST_LEVEL[hostThinkingLevel] ?? null;
|
|
53
|
+
const applicable = provider === "openai-codex" && GPT56_CODEX_MODEL.test(modelId)
|
|
54
|
+
&& payload !== null && payload.model === modelId && expectedProviderEffort !== null;
|
|
55
|
+
if (!applicable || !payload) {
|
|
56
|
+
return { payload: input.payload, applicable: false, changed: false, hostThinkingLevel,
|
|
57
|
+
expectedProviderEffort, providerEffort: payload ? effortFromPayload(payload) : null, reasonCode: "not-applicable" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const currentEffort = effortFromPayload(payload);
|
|
61
|
+
if (currentEffort === expectedProviderEffort) {
|
|
62
|
+
return { payload: input.payload, applicable: true, changed: false, hostThinkingLevel,
|
|
63
|
+
expectedProviderEffort, providerEffort: currentEffort, reasonCode: "observed" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const currentReasoning = record(payload.reasoning) ?? {};
|
|
67
|
+
const normalizedReasoning = {
|
|
68
|
+
...currentReasoning,
|
|
69
|
+
effort: expectedProviderEffort,
|
|
70
|
+
...(typeof currentReasoning.summary === "string" ? {} : { summary: "auto" })
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
payload: { ...payload, reasoning: normalizedReasoning },
|
|
74
|
+
applicable: true,
|
|
75
|
+
changed: true,
|
|
76
|
+
hostThinkingLevel,
|
|
77
|
+
expectedProviderEffort,
|
|
78
|
+
providerEffort: expectedProviderEffort,
|
|
79
|
+
reasonCode: hostThinkingLevel === "off" ? "off-normalized-to-none"
|
|
80
|
+
: hostThinkingLevel === "minimal" ? "minimal-normalized-to-low"
|
|
81
|
+
: "provider-effort-normalized"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
type JsonRecord = Record<string, unknown>;
|
|
4
|
+
type BoundReason = "instructions-unavailable" | "instructions-too-large" | "tools-invalid" | "tools-too-large"
|
|
5
|
+
| "input-invalid" | "input-too-large" | "deferred-tools-invalid" | "deferred-tools-too-large"
|
|
6
|
+
| "surface-too-deep" | "surface-too-large" | "surface-not-json" | "surface-cyclic";
|
|
7
|
+
|
|
8
|
+
const MAX_INSTRUCTIONS_CHARS = 1_000_000;
|
|
9
|
+
const MAX_TOOLS = 512;
|
|
10
|
+
const MAX_INPUT_ITEMS = 4_096;
|
|
11
|
+
const MAX_SURFACE_DEPTH = 32;
|
|
12
|
+
const MAX_SURFACE_NODES = 50_000;
|
|
13
|
+
const MAX_SURFACE_STRING_CHARS = 2_000_000;
|
|
14
|
+
const MAX_CONTAINER_ITEMS = 4_096;
|
|
15
|
+
const PUBLIC_ID = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/;
|
|
16
|
+
const CONTROL_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
|
17
|
+
|
|
18
|
+
export type ProviderWireFingerprint = {
|
|
19
|
+
applicable: boolean;
|
|
20
|
+
state: "known" | "unavailable";
|
|
21
|
+
reasonCode: BoundReason | "not-applicable" | null;
|
|
22
|
+
modelId: string | null;
|
|
23
|
+
instructionsHash: string | null;
|
|
24
|
+
baseInstructionsHash: string | null;
|
|
25
|
+
instructionNormalization: "host-relocation-v1" | "none" | null;
|
|
26
|
+
instructionChars: number | null;
|
|
27
|
+
orderedToolSurfaceHash: string | null;
|
|
28
|
+
toolCount: number | null;
|
|
29
|
+
deferredToolSurfaceHash: string | null;
|
|
30
|
+
deferredToolCount: number | null;
|
|
31
|
+
deferredToolBatchCount: number | null;
|
|
32
|
+
reasoningEffort: string | null;
|
|
33
|
+
textVerbosity: string | null;
|
|
34
|
+
toolChoiceKind: string | null;
|
|
35
|
+
toolChoiceHash: string | null;
|
|
36
|
+
requestPrefixFingerprint: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
class SurfaceBoundError extends Error {
|
|
40
|
+
readonly reasonCode: BoundReason;
|
|
41
|
+
constructor(reasonCode: BoundReason) { super(reasonCode); this.reasonCode = reasonCode; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function record(value: unknown): JsonRecord | null {
|
|
45
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonRecord : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hash(value: string): string {
|
|
49
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function control(value: unknown): string | null {
|
|
53
|
+
return typeof value === "string" && CONTROL_VALUE.test(value) ? value : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function canonicalJson(value: unknown): string {
|
|
57
|
+
const budget = { nodes: 0, stringChars: 0, active: new WeakSet<object>() };
|
|
58
|
+
const visit = (item: unknown, depth: number, inArray: boolean): unknown => {
|
|
59
|
+
budget.nodes += 1;
|
|
60
|
+
if (budget.nodes > MAX_SURFACE_NODES) throw new SurfaceBoundError("surface-too-large");
|
|
61
|
+
if (depth > MAX_SURFACE_DEPTH) throw new SurfaceBoundError("surface-too-deep");
|
|
62
|
+
if (item === null || typeof item === "boolean") return item;
|
|
63
|
+
if (typeof item === "string") {
|
|
64
|
+
budget.stringChars += item.length;
|
|
65
|
+
if (budget.stringChars > MAX_SURFACE_STRING_CHARS) throw new SurfaceBoundError("surface-too-large");
|
|
66
|
+
return item;
|
|
67
|
+
}
|
|
68
|
+
if (typeof item === "number") return Number.isFinite(item) ? item : null;
|
|
69
|
+
if (["undefined", "function", "symbol"].includes(typeof item)) return inArray ? null : undefined;
|
|
70
|
+
if (typeof item === "bigint" || typeof item !== "object") throw new SurfaceBoundError("surface-not-json");
|
|
71
|
+
if (budget.active.has(item)) throw new SurfaceBoundError("surface-cyclic");
|
|
72
|
+
budget.active.add(item);
|
|
73
|
+
try {
|
|
74
|
+
if (Array.isArray(item)) {
|
|
75
|
+
if (item.length > MAX_CONTAINER_ITEMS) throw new SurfaceBoundError("surface-too-large");
|
|
76
|
+
return item.map((entry) => visit(entry, depth + 1, true));
|
|
77
|
+
}
|
|
78
|
+
const entries = Object.entries(item as JsonRecord)
|
|
79
|
+
.filter(([, entry]) => !["undefined", "function", "symbol"].includes(typeof entry));
|
|
80
|
+
if (entries.length > MAX_CONTAINER_ITEMS) throw new SurfaceBoundError("surface-too-large");
|
|
81
|
+
entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
82
|
+
const output: JsonRecord = {};
|
|
83
|
+
for (const [key, entry] of entries) {
|
|
84
|
+
budget.stringChars += key.length;
|
|
85
|
+
if (budget.stringChars > MAX_SURFACE_STRING_CHARS) throw new SurfaceBoundError("surface-too-large");
|
|
86
|
+
output[key] = visit(entry, depth + 1, false);
|
|
87
|
+
}
|
|
88
|
+
return output;
|
|
89
|
+
} finally {
|
|
90
|
+
budget.active.delete(item);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return JSON.stringify(visit(value, 0, false));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unavailable(modelId: string, reasonCode: BoundReason): ProviderWireFingerprint {
|
|
97
|
+
return { applicable: true, state: "unavailable", reasonCode, modelId, instructionsHash: null,
|
|
98
|
+
baseInstructionsHash: null, instructionNormalization: null, instructionChars: null,
|
|
99
|
+
orderedToolSurfaceHash: null, toolCount: null, deferredToolSurfaceHash: null, deferredToolCount: null,
|
|
100
|
+
deferredToolBatchCount: null, reasoningEffort: null, textVerbosity: null, toolChoiceKind: null,
|
|
101
|
+
toolChoiceHash: null, requestPrefixFingerprint: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function xmlEscape(value: string): string {
|
|
105
|
+
return value
|
|
106
|
+
.replace(/&/g, "&")
|
|
107
|
+
.replace(/</g, "<")
|
|
108
|
+
.replace(/>/g, ">")
|
|
109
|
+
.replace(/"/g, """)
|
|
110
|
+
.replace(/'/g, "'");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function boundedHostPaths(value: unknown): string[] {
|
|
114
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 32_768 || value.includes("\0")) return [];
|
|
115
|
+
if (/^[\\/]+$/.test(value) || /^[A-Za-z]:[\\/]?$/.test(value)) return [];
|
|
116
|
+
const native = value.replace(/[\\/]+$/, "");
|
|
117
|
+
const slash = native.replace(/\\/g, "/");
|
|
118
|
+
return [...new Set([native, slash].filter(Boolean))];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeSkillLocations(instructions: string, platformRoot: unknown): string {
|
|
122
|
+
const roots = boundedHostPaths(platformRoot);
|
|
123
|
+
if (roots.length === 0) return instructions;
|
|
124
|
+
const startTag = "<available_skills>";
|
|
125
|
+
const endTag = "</available_skills>";
|
|
126
|
+
let cursor = 0;
|
|
127
|
+
let output = "";
|
|
128
|
+
while (cursor < instructions.length) {
|
|
129
|
+
const start = instructions.indexOf(startTag, cursor);
|
|
130
|
+
if (start < 0) return output + instructions.slice(cursor);
|
|
131
|
+
const endStart = instructions.indexOf(endTag, start + startTag.length);
|
|
132
|
+
if (endStart < 0) return output + instructions.slice(cursor);
|
|
133
|
+
const end = endStart + endTag.length;
|
|
134
|
+
let section = instructions.slice(start, end);
|
|
135
|
+
for (const root of roots) {
|
|
136
|
+
const locationPrefix = `<location>${xmlEscape(root)}`;
|
|
137
|
+
section = section
|
|
138
|
+
.replaceAll(`${locationPrefix}/`, "<location>[PIAGENT_PLATFORM_ROOT]/")
|
|
139
|
+
.replaceAll(`${locationPrefix}\\`, "<location>[PIAGENT_PLATFORM_ROOT]/")
|
|
140
|
+
.replaceAll(`${locationPrefix}</location>`, "<location>[PIAGENT_PLATFORM_ROOT]</location>");
|
|
141
|
+
}
|
|
142
|
+
output += instructions.slice(cursor, start) + section;
|
|
143
|
+
cursor = end;
|
|
144
|
+
}
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeHostOwnedRelocation(instructions: string, workingDirectory: unknown, platformRoot: unknown): {
|
|
149
|
+
value: string;
|
|
150
|
+
mode: "host-relocation-v1" | "none";
|
|
151
|
+
} {
|
|
152
|
+
const cwdVariants = boundedHostPaths(workingDirectory);
|
|
153
|
+
let value = normalizeSkillLocations(instructions, platformRoot);
|
|
154
|
+
for (const cwd of cwdVariants) {
|
|
155
|
+
const cwdLine = `Current working directory: ${cwd}`;
|
|
156
|
+
if (value.endsWith(cwdLine)) {
|
|
157
|
+
value = `${value.slice(0, -cwdLine.length)}Current working directory: [PIAGENT_PROJECT_ROOT]`;
|
|
158
|
+
}
|
|
159
|
+
value = value
|
|
160
|
+
.replaceAll(`<project_instructions path="${cwd}">`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]">')
|
|
161
|
+
.replaceAll(`<project_instructions path="${cwd}/`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]/')
|
|
162
|
+
.replaceAll(`<project_instructions path="${cwd}\\`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]/');
|
|
163
|
+
}
|
|
164
|
+
return value === instructions
|
|
165
|
+
? { value, mode: "none" }
|
|
166
|
+
: { value, mode: "host-relocation-v1" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildOpenAiCodexWireFingerprint(input: {
|
|
170
|
+
payload: unknown;
|
|
171
|
+
provider: unknown;
|
|
172
|
+
modelId: unknown;
|
|
173
|
+
workingDirectory?: unknown;
|
|
174
|
+
platformRoot?: unknown;
|
|
175
|
+
}): ProviderWireFingerprint {
|
|
176
|
+
const payload = record(input.payload);
|
|
177
|
+
const provider = typeof input.provider === "string" ? input.provider : "";
|
|
178
|
+
const modelId = typeof input.modelId === "string" && PUBLIC_ID.test(input.modelId) ? input.modelId : "";
|
|
179
|
+
if (provider !== "openai-codex" || !payload || !modelId || payload.model !== modelId) {
|
|
180
|
+
return { ...unavailable("", "surface-not-json"), applicable: false, state: "unavailable",
|
|
181
|
+
reasonCode: "not-applicable", modelId: null };
|
|
182
|
+
}
|
|
183
|
+
if (typeof payload.instructions !== "string") return unavailable(modelId, "instructions-unavailable");
|
|
184
|
+
if (payload.instructions.length > MAX_INSTRUCTIONS_CHARS) return unavailable(modelId, "instructions-too-large");
|
|
185
|
+
const tools = payload.tools === undefined ? [] : payload.tools;
|
|
186
|
+
if (!Array.isArray(tools)) return unavailable(modelId, "tools-invalid");
|
|
187
|
+
if (tools.length > MAX_TOOLS) return unavailable(modelId, "tools-too-large");
|
|
188
|
+
const requestInput = payload.input === undefined ? [] : payload.input;
|
|
189
|
+
if (!Array.isArray(requestInput)) return unavailable(modelId, "input-invalid");
|
|
190
|
+
if (requestInput.length > MAX_INPUT_ITEMS) return unavailable(modelId, "input-too-large");
|
|
191
|
+
|
|
192
|
+
const deferredBatches: Array<{ inputIndex: number; tools: unknown[] }> = [];
|
|
193
|
+
let deferredToolCount = 0;
|
|
194
|
+
for (let index = 0; index < requestInput.length; index += 1) {
|
|
195
|
+
const item = record(requestInput[index]);
|
|
196
|
+
if (item?.type !== "tool_search_output") continue;
|
|
197
|
+
if (!Array.isArray(item.tools)) return unavailable(modelId, "deferred-tools-invalid");
|
|
198
|
+
deferredToolCount += item.tools.length;
|
|
199
|
+
if (deferredToolCount > MAX_TOOLS) return unavailable(modelId, "deferred-tools-too-large");
|
|
200
|
+
deferredBatches.push({ inputIndex: index, tools: item.tools });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const instructionsHash = hash(payload.instructions);
|
|
205
|
+
const normalizedInstructions = normalizeHostOwnedRelocation(payload.instructions, input.workingDirectory, input.platformRoot);
|
|
206
|
+
const baseInstructionsHash = hash(normalizedInstructions.value);
|
|
207
|
+
// Object keys are canonicalized because their order is semantic noise;
|
|
208
|
+
// arrays are preserved so changing provider-visible tool order is detected.
|
|
209
|
+
const orderedToolSurfaceHash = hash(canonicalJson(tools));
|
|
210
|
+
const deferredToolSurfaceHash = hash(canonicalJson(deferredBatches));
|
|
211
|
+
const toolChoice = payload.tool_choice ?? null;
|
|
212
|
+
const toolChoiceHash = hash(canonicalJson(toolChoice));
|
|
213
|
+
const reasoningEffort = control(record(payload.reasoning)?.effort);
|
|
214
|
+
const textVerbosity = control(record(payload.text)?.verbosity);
|
|
215
|
+
const toolChoiceKind = typeof toolChoice === "string" ? control(toolChoice) : record(toolChoice) ? "structured" : "none";
|
|
216
|
+
const requestPrefixFingerprint = hash(JSON.stringify({
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
modelId,
|
|
219
|
+
instructionsHash,
|
|
220
|
+
baseInstructionsHash,
|
|
221
|
+
orderedToolSurfaceHash,
|
|
222
|
+
toolCount: tools.length,
|
|
223
|
+
deferredToolSurfaceHash,
|
|
224
|
+
deferredToolCount,
|
|
225
|
+
deferredToolBatchCount: deferredBatches.length,
|
|
226
|
+
reasoningEffort,
|
|
227
|
+
textVerbosity,
|
|
228
|
+
toolChoiceHash
|
|
229
|
+
}));
|
|
230
|
+
return { applicable: true, state: "known", reasonCode: null, modelId, instructionsHash,
|
|
231
|
+
baseInstructionsHash, instructionNormalization: normalizedInstructions.mode,
|
|
232
|
+
instructionChars: payload.instructions.length, orderedToolSurfaceHash, toolCount: tools.length,
|
|
233
|
+
deferredToolSurfaceHash, deferredToolCount, deferredToolBatchCount: deferredBatches.length,
|
|
234
|
+
reasoningEffort, textVerbosity, toolChoiceKind, toolChoiceHash, requestPrefixFingerprint };
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return unavailable(modelId, error instanceof SurfaceBoundError ? error.reasonCode : "surface-not-json");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -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
|
+
}
|