@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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export type EmissionSuppressionReason =
|
|
4
|
+
| "empty"
|
|
5
|
+
| "filler"
|
|
6
|
+
| "duplicate-exact"
|
|
7
|
+
| "duplicate-normalized"
|
|
8
|
+
| "update-budget";
|
|
9
|
+
|
|
10
|
+
export type BoundedEmissionCandidate = {
|
|
11
|
+
channel: string;
|
|
12
|
+
identityKey: string;
|
|
13
|
+
updateKey: string;
|
|
14
|
+
text: string;
|
|
15
|
+
actionable: boolean;
|
|
16
|
+
reference: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type EmissionDecision = {
|
|
20
|
+
accepted: boolean;
|
|
21
|
+
reason: EmissionSuppressionReason | null;
|
|
22
|
+
priorReference: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type BoundedEmissionTelemetry = {
|
|
26
|
+
schemaVersion: 1;
|
|
27
|
+
capacity: number;
|
|
28
|
+
retained: number;
|
|
29
|
+
primed: number;
|
|
30
|
+
accepted: number;
|
|
31
|
+
suppressed: number;
|
|
32
|
+
suppressedByReason: Record<EmissionSuppressionReason, number>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type RetainedEmission = {
|
|
36
|
+
exactKey: string;
|
|
37
|
+
normalizedKey: string;
|
|
38
|
+
updateKey: string | null;
|
|
39
|
+
reference: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g;
|
|
43
|
+
const DEFAULT_CAPACITY = 512;
|
|
44
|
+
const MIN_CAPACITY = 1;
|
|
45
|
+
const MAX_CAPACITY = 16_384;
|
|
46
|
+
|
|
47
|
+
const FILLER = new Set([
|
|
48
|
+
"abort", "all good", "carry on", "complete", "continue", "done", "finished", "halt", "lgtm",
|
|
49
|
+
"looks good", "no action", "no action needed", "no concern", "no concerns", "no further advice",
|
|
50
|
+
"no further advice needed", "no further input", "no further input needed", "no issue", "no issues",
|
|
51
|
+
"no issue continue", "no notes", "nothing to add", "nothing to flag", "nothing to report", "ok", "okay",
|
|
52
|
+
"stop", "stop here", "stop now", "task complete", "task done",
|
|
53
|
+
"da xong", "đã xong", "khong can lam gi", "không cần làm gì", "khong co gi de bao cao", "không có gì để báo cáo",
|
|
54
|
+
"khong co loi", "không có lỗi", "khong co van de", "không có vấn đề", "on", "ổn", "tiep tuc", "tiếp tục", "xong"
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function digest(value: string): string {
|
|
58
|
+
return createHash("sha256").update(value).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function exactEmissionText(value: string): string {
|
|
62
|
+
return String(value ?? "")
|
|
63
|
+
.replace(ANSI, "")
|
|
64
|
+
.normalize("NFKC")
|
|
65
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
|
|
66
|
+
.replace(/\s+/g, " ")
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Stable comparison form for reviewer notes and compact runtime event summaries. */
|
|
71
|
+
export function normalizeEmissionText(value: string): string {
|
|
72
|
+
return exactEmissionText(value)
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
75
|
+
.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function canonical(value: unknown): unknown {
|
|
79
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
80
|
+
if (!value || typeof value !== "object") return value;
|
|
81
|
+
const prototype = Object.getPrototypeOf(value);
|
|
82
|
+
if (prototype !== Object.prototype && prototype !== null) return String(value);
|
|
83
|
+
const record = value as Record<string, unknown>;
|
|
84
|
+
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonical(record[key])]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function canonicalText(value: unknown): string {
|
|
88
|
+
return JSON.stringify(canonical(value));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function textField(payload: Record<string, unknown>): string | null {
|
|
92
|
+
for (const field of ["note", "advice", "finding", "message", "summary"]) {
|
|
93
|
+
if (typeof payload[field] === "string" && payload[field].trim()) return payload[field];
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reviewerRole(payload: Record<string, unknown>): string | null {
|
|
99
|
+
for (const field of ["role", "agentRole", "actorRole", "sourceRole"]) {
|
|
100
|
+
const value = String(payload[field] ?? "").toLowerCase();
|
|
101
|
+
if (value === "reviewer" || value === "piagent-reviewer") return value;
|
|
102
|
+
}
|
|
103
|
+
const emissionType = String(payload.emissionType ?? "").toLowerCase();
|
|
104
|
+
return emissionType === "reviewer-note" || emissionType === "review-note" ? "reviewer" : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type RuntimeEmissionLike = {
|
|
108
|
+
kind: string;
|
|
109
|
+
sessionRef: string;
|
|
110
|
+
taskRunId: string | null;
|
|
111
|
+
agentOperationId: string | null;
|
|
112
|
+
turnIndex: number | null;
|
|
113
|
+
toolCallId: string | null;
|
|
114
|
+
revision: object;
|
|
115
|
+
payload: Record<string, unknown>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Select only bounded activity, reviewer-note and operation emissions. Message
|
|
120
|
+
* deltas/completions are deliberately outside this boundary, so a final user-
|
|
121
|
+
* visible response can never be suppressed by this guard.
|
|
122
|
+
*/
|
|
123
|
+
export function runtimeEventEmissionCandidate(event: RuntimeEmissionLike, reference: string): BoundedEmissionCandidate | null {
|
|
124
|
+
const payload = event.payload ?? {};
|
|
125
|
+
const revision = canonicalText(event.revision);
|
|
126
|
+
// Lifecycle state is authoritative. Classify it before reviewer metadata so
|
|
127
|
+
// a note attached to a started/finished event can never hide that state.
|
|
128
|
+
if (event.kind.startsWith("activity.")) {
|
|
129
|
+
const state = payload.state ?? event.kind.slice("activity.".length);
|
|
130
|
+
return {
|
|
131
|
+
channel: "activity",
|
|
132
|
+
identityKey: canonicalText([event.agentOperationId, event.toolCallId, payload.activityRef, event.kind, state]),
|
|
133
|
+
updateKey: canonicalText([revision, event.agentOperationId, event.toolCallId, event.kind, state]),
|
|
134
|
+
text: canonicalText(payload),
|
|
135
|
+
actionable: false,
|
|
136
|
+
reference
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (event.kind.startsWith("agent-operation.")) {
|
|
140
|
+
const state = payload.state ?? payload.settlement ?? payload.status ?? event.kind.slice("agent-operation.".length);
|
|
141
|
+
return {
|
|
142
|
+
channel: "operation",
|
|
143
|
+
identityKey: canonicalText([event.agentOperationId, event.kind, state]),
|
|
144
|
+
updateKey: canonicalText([revision, event.agentOperationId, event.kind, state]),
|
|
145
|
+
text: canonicalText(payload),
|
|
146
|
+
actionable: false,
|
|
147
|
+
reference
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const role = reviewerRole(payload), note = role ? textField(payload) : null;
|
|
151
|
+
if (role && note) {
|
|
152
|
+
const explicitUpdate = payload.updateRef ?? payload.reviewerRevision ?? payload.revision;
|
|
153
|
+
return {
|
|
154
|
+
channel: "reviewer",
|
|
155
|
+
// A session may host many unrelated tasks. Dedupe reviewer prose across
|
|
156
|
+
// retries/operations of one durable task, but never across task runs.
|
|
157
|
+
identityKey: canonicalText([event.sessionRef, event.taskRunId, role]),
|
|
158
|
+
updateKey: canonicalText([event.sessionRef, event.agentOperationId, event.turnIndex, explicitUpdate ?? revision]),
|
|
159
|
+
text: note,
|
|
160
|
+
actionable: true,
|
|
161
|
+
reference
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A small in-memory policy gate. It stores only hashes, never emitted prose. */
|
|
168
|
+
export class BoundedEmissionGuard {
|
|
169
|
+
readonly capacity: number;
|
|
170
|
+
readonly #retained: RetainedEmission[] = [];
|
|
171
|
+
readonly #exact = new Map<string, string>();
|
|
172
|
+
readonly #normalized = new Map<string, string>();
|
|
173
|
+
readonly #updates = new Map<string, string>();
|
|
174
|
+
#primed = 0;
|
|
175
|
+
#accepted = 0;
|
|
176
|
+
readonly #suppressed: Record<EmissionSuppressionReason, number> = {
|
|
177
|
+
empty: 0,
|
|
178
|
+
filler: 0,
|
|
179
|
+
"duplicate-exact": 0,
|
|
180
|
+
"duplicate-normalized": 0,
|
|
181
|
+
"update-budget": 0
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
constructor(options: { capacity?: number } = {}) {
|
|
185
|
+
const candidate = Number.isFinite(options.capacity) ? Math.floor(Number(options.capacity)) : DEFAULT_CAPACITY;
|
|
186
|
+
this.capacity = Math.max(MIN_CAPACITY, Math.min(MAX_CAPACITY, candidate));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#keys(candidate: BoundedEmissionCandidate): { exactKey: string; normalizedKey: string; updateKey: string | null; exact: string; normalized: string } {
|
|
190
|
+
const exact = exactEmissionText(candidate.text), normalized = normalizeEmissionText(exact);
|
|
191
|
+
const scope = `${candidate.channel}\0${candidate.identityKey}`;
|
|
192
|
+
return {
|
|
193
|
+
exact,
|
|
194
|
+
normalized,
|
|
195
|
+
exactKey: digest(`${scope}\0exact\0${exact}`),
|
|
196
|
+
normalizedKey: digest(`${scope}\0normalized\0${normalized}`),
|
|
197
|
+
updateKey: candidate.actionable ? digest(`${candidate.channel}\0update\0${candidate.updateKey}`) : null
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#reject(reason: EmissionSuppressionReason, priorReference: string | null = null, count = true): EmissionDecision {
|
|
202
|
+
if (count) this.#suppressed[reason] += 1;
|
|
203
|
+
return { accepted: false, reason, priorReference };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#inspect(candidate: BoundedEmissionCandidate, countSuppression: boolean): EmissionDecision {
|
|
207
|
+
const keys = this.#keys(candidate);
|
|
208
|
+
if (candidate.actionable && !keys.exact) return this.#reject("empty", null, countSuppression);
|
|
209
|
+
if (candidate.actionable && FILLER.has(keys.normalized)) return this.#reject("filler", null, countSuppression);
|
|
210
|
+
const exact = this.#exact.get(keys.exactKey);
|
|
211
|
+
if (exact) return this.#reject("duplicate-exact", exact, countSuppression);
|
|
212
|
+
const normalized = this.#normalized.get(keys.normalizedKey);
|
|
213
|
+
if (normalized) return this.#reject("duplicate-normalized", normalized, countSuppression);
|
|
214
|
+
const update = keys.updateKey ? this.#updates.get(keys.updateKey) : undefined;
|
|
215
|
+
if (update) return this.#reject("update-budget", update, countSuppression);
|
|
216
|
+
return { accepted: true, reason: null, priorReference: null };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
inspect(candidate: BoundedEmissionCandidate): EmissionDecision {
|
|
220
|
+
return this.#inspect(candidate, true);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Seed retained history without counting old suppression as live telemetry. */
|
|
224
|
+
prime(candidate: BoundedEmissionCandidate): boolean {
|
|
225
|
+
if (!this.#inspect(candidate, false).accepted) return false;
|
|
226
|
+
this.recordAccepted(candidate, { primed: true });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
recordAccepted(candidate: BoundedEmissionCandidate, options: { primed?: boolean } = {}): void {
|
|
231
|
+
const keys = this.#keys(candidate);
|
|
232
|
+
const record = { exactKey: keys.exactKey, normalizedKey: keys.normalizedKey, updateKey: keys.updateKey, reference: candidate.reference };
|
|
233
|
+
this.#retained.push(record);
|
|
234
|
+
this.#exact.set(record.exactKey, record.reference);
|
|
235
|
+
this.#normalized.set(record.normalizedKey, record.reference);
|
|
236
|
+
if (record.updateKey) this.#updates.set(record.updateKey, record.reference);
|
|
237
|
+
if (options.primed) this.#primed += 1;
|
|
238
|
+
else this.#accepted += 1;
|
|
239
|
+
while (this.#retained.length > this.capacity) {
|
|
240
|
+
const stale = this.#retained.shift() as RetainedEmission;
|
|
241
|
+
if (this.#exact.get(stale.exactKey) === stale.reference) this.#exact.delete(stale.exactKey);
|
|
242
|
+
if (this.#normalized.get(stale.normalizedKey) === stale.reference) this.#normalized.delete(stale.normalizedKey);
|
|
243
|
+
if (stale.updateKey && this.#updates.get(stale.updateKey) === stale.reference) this.#updates.delete(stale.updateKey);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Remove fingerprints when their durable event leaves the retention window. */
|
|
248
|
+
forget(reference: string): boolean {
|
|
249
|
+
const kept = this.#retained.filter((record) => record.reference !== reference);
|
|
250
|
+
if (kept.length === this.#retained.length) return false;
|
|
251
|
+
this.#retained.length = 0;
|
|
252
|
+
this.#retained.push(...kept);
|
|
253
|
+
this.#exact.clear();
|
|
254
|
+
this.#normalized.clear();
|
|
255
|
+
this.#updates.clear();
|
|
256
|
+
for (const record of this.#retained) {
|
|
257
|
+
this.#exact.set(record.exactKey, record.reference);
|
|
258
|
+
this.#normalized.set(record.normalizedKey, record.reference);
|
|
259
|
+
if (record.updateKey) this.#updates.set(record.updateKey, record.reference);
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
reset(): void {
|
|
265
|
+
this.#retained.length = 0;
|
|
266
|
+
this.#exact.clear();
|
|
267
|
+
this.#normalized.clear();
|
|
268
|
+
this.#updates.clear();
|
|
269
|
+
this.#primed = 0;
|
|
270
|
+
this.#accepted = 0;
|
|
271
|
+
for (const reason of Object.keys(this.#suppressed) as EmissionSuppressionReason[]) this.#suppressed[reason] = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
telemetry(): BoundedEmissionTelemetry {
|
|
275
|
+
const suppressedByReason = { ...this.#suppressed };
|
|
276
|
+
return {
|
|
277
|
+
schemaVersion: 1,
|
|
278
|
+
capacity: this.capacity,
|
|
279
|
+
retained: this.#retained.length,
|
|
280
|
+
primed: this.#primed,
|
|
281
|
+
accepted: this.#accepted,
|
|
282
|
+
suppressed: Object.values(suppressedByReason).reduce((sum, value) => sum + value, 0),
|
|
283
|
+
suppressedByReason
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -3,6 +3,12 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "../../extensions/local-state-path.js";
|
|
6
|
+
import {
|
|
7
|
+
BoundedEmissionGuard,
|
|
8
|
+
type BoundedEmissionTelemetry,
|
|
9
|
+
type EmissionSuppressionReason,
|
|
10
|
+
runtimeEventEmissionCandidate
|
|
11
|
+
} from "./bounded-emission-guard.ts";
|
|
6
12
|
|
|
7
13
|
export const RUNTIME_EVENT_SCHEMA_VERSION = 2 as const;
|
|
8
14
|
export type RuntimeEventRevision = {
|
|
@@ -57,6 +63,9 @@ export type RuntimeEventReplay = {
|
|
|
57
63
|
lastAvailableSequence: number | null;
|
|
58
64
|
reasonCode: string | null;
|
|
59
65
|
};
|
|
66
|
+
export type RuntimeEventAppendResult =
|
|
67
|
+
| { event: RuntimeEventV2; appended: true; suppressionReason: null }
|
|
68
|
+
| { event: RuntimeEventV2 | null; appended: false; suppressionReason: EmissionSuppressionReason | "event-id-duplicate" };
|
|
60
69
|
|
|
61
70
|
const EVENT_KINDS = new Set([
|
|
62
71
|
"runtime.started", "runtime.health-changed", "runtime.disconnected", "runtime.resync-required", "runtime.resynced", "runtime.phase-changed",
|
|
@@ -158,15 +167,17 @@ function readSegment(root: string, file: string): RuntimeEventV2[] {
|
|
|
158
167
|
export class RuntimeEventStore {
|
|
159
168
|
readonly directory: string;
|
|
160
169
|
readonly corruptions: string[] = [];
|
|
161
|
-
readonly options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number };
|
|
170
|
+
readonly options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number; emissionGuardCapacity?: number };
|
|
162
171
|
private events: RuntimeEventV2[] = [];
|
|
163
172
|
private segments: Array<{ file: string; start: number; count: number }> = [];
|
|
164
173
|
private eventById = new Map<string, RuntimeEventV2>();
|
|
174
|
+
private readonly emissionGuard: BoundedEmissionGuard;
|
|
165
175
|
private readonly maxEventsPerSegment: number;
|
|
166
176
|
private readonly maxSegments: number;
|
|
167
177
|
|
|
168
|
-
constructor(options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number }) {
|
|
178
|
+
constructor(options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number; emissionGuardCapacity?: number }) {
|
|
169
179
|
this.options = options;
|
|
180
|
+
this.emissionGuard = new BoundedEmissionGuard({ capacity: options.emissionGuardCapacity });
|
|
170
181
|
this.maxEventsPerSegment = Math.max(2, Math.min(1_000, options.maxEventsPerSegment ?? 500));
|
|
171
182
|
this.maxSegments = Math.max(2, Math.min(20, options.maxSegments ?? 10));
|
|
172
183
|
this.directory = storeDirectory(options.projectRoot, options.runtimeInstanceId, options.sessionRef);
|
|
@@ -195,11 +206,13 @@ export class RuntimeEventStore {
|
|
|
195
206
|
if (previous && record.writerSequence !== previous + 1) throw new Error("event sequence gap");
|
|
196
207
|
if (this.eventById.has(record.eventId)) throw new Error("duplicate event identity");
|
|
197
208
|
previous = record.writerSequence; this.events.push(record); this.eventById.set(record.eventId, record);
|
|
209
|
+
const candidate = runtimeEventEmissionCandidate(record, record.eventId);
|
|
210
|
+
if (candidate) this.emissionGuard.prime(candidate);
|
|
198
211
|
}
|
|
199
212
|
this.segments.push({ file: name, start, count: records.length });
|
|
200
213
|
}
|
|
201
214
|
} catch {
|
|
202
|
-
this.events = []; this.segments = []; this.eventById.clear(); this.corruptions.push("event-store-corrupt");
|
|
215
|
+
this.events = []; this.segments = []; this.eventById.clear(); this.emissionGuard.reset(); this.corruptions.push("event-store-corrupt");
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
218
|
|
|
@@ -208,14 +221,27 @@ export class RuntimeEventStore {
|
|
|
208
221
|
retention(): { eventRetentionCount: number; eventRetentionSeconds: number } {
|
|
209
222
|
return { eventRetentionCount: this.maxEventsPerSegment * this.maxSegments, eventRetentionSeconds: 0 };
|
|
210
223
|
}
|
|
224
|
+
emissionTelemetry(): BoundedEmissionTelemetry { return this.emissionGuard.telemetry(); }
|
|
211
225
|
|
|
212
|
-
append(draft: RuntimeEventDraft, recordedAt = new Date().toISOString()):
|
|
226
|
+
append(draft: RuntimeEventDraft, recordedAt = new Date().toISOString()): RuntimeEventAppendResult {
|
|
213
227
|
if (this.resyncRequired()) throw new Error("WebUI event store requires resync");
|
|
214
228
|
if (draft.projectRef !== this.options.projectRef || draft.runtimeInstanceId !== this.options.runtimeInstanceId || draft.sessionRef !== this.options.sessionRef) throw new Error("Runtime event identity does not match its store");
|
|
215
229
|
const id = eventId(draft), duplicate = this.eventById.get(id);
|
|
216
|
-
|
|
230
|
+
const candidate = runtimeEventEmissionCandidate(draft, id);
|
|
231
|
+
if (duplicate) {
|
|
232
|
+
const decision = candidate ? this.emissionGuard.inspect(candidate) : null;
|
|
233
|
+
return { event: structuredClone(duplicate), appended: false,
|
|
234
|
+
suppressionReason: decision?.reason ?? "event-id-duplicate" };
|
|
235
|
+
}
|
|
217
236
|
const sequence = (this.events.at(-1)?.writerSequence ?? 0) + 1;
|
|
218
237
|
const event = validateRuntimeEventEnvelope({ schemaVersion: 2, eventId: id, eventCursor: eventCursor(sequence, id), writerSequence: sequence, recordedAt, ...structuredClone(draft) });
|
|
238
|
+
if (candidate) {
|
|
239
|
+
const decision = this.emissionGuard.inspect(candidate);
|
|
240
|
+
if (!decision.accepted) {
|
|
241
|
+
const prior = decision.priorReference ? this.eventById.get(decision.priorReference) ?? null : null;
|
|
242
|
+
return { event: prior ? structuredClone(prior) : null, appended: false, suppressionReason: decision.reason as EmissionSuppressionReason };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
219
245
|
let segment = this.segments.at(-1);
|
|
220
246
|
if (!segment || segment.count >= this.maxEventsPerSegment) {
|
|
221
247
|
segment = { file: segmentName(sequence), start: sequence, count: 0 }; this.segments.push(segment);
|
|
@@ -228,8 +254,9 @@ export class RuntimeEventStore {
|
|
|
228
254
|
const descriptor = fs.openSync(target, flags, 0o600);
|
|
229
255
|
try { fs.writeFileSync(descriptor, `${JSON.stringify(event)}\n`); fs.fsyncSync(descriptor); fs.fchmodSync(descriptor, 0o600); } finally { fs.closeSync(descriptor); }
|
|
230
256
|
segment.count += 1; this.events.push(event); this.eventById.set(event.eventId, event);
|
|
257
|
+
if (candidate) this.emissionGuard.recordAccepted(candidate);
|
|
231
258
|
try { this.enforceRetention(); } catch { this.corruptions.push("event-retention-failed"); }
|
|
232
|
-
return { event: structuredClone(event), appended: true };
|
|
259
|
+
return { event: structuredClone(event), appended: true, suppressionReason: null };
|
|
233
260
|
}
|
|
234
261
|
|
|
235
262
|
private enforceRetention(): void {
|
|
@@ -240,7 +267,10 @@ export class RuntimeEventStore {
|
|
|
240
267
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("event retention target is unsafe");
|
|
241
268
|
fs.unlinkSync(target);
|
|
242
269
|
const discarded = this.events.splice(0, removed.count);
|
|
243
|
-
for (const event of discarded)
|
|
270
|
+
for (const event of discarded) {
|
|
271
|
+
this.eventById.delete(event.eventId);
|
|
272
|
+
this.emissionGuard.forget(event.eventId);
|
|
273
|
+
}
|
|
244
274
|
}
|
|
245
275
|
}
|
|
246
276
|
|
|
@@ -96,7 +96,7 @@ export function projectTaskCompactionHistory(input: {
|
|
|
96
96
|
let malformed = inspected.corruptions;
|
|
97
97
|
const matching = inspected.records.flatMap((record: any, ordinal: number) => {
|
|
98
98
|
if (record?.taskId !== input.task.taskId || record?.taskRunId !== input.task.taskRunId || record?.sessionId !== input.task.sessionId) return [];
|
|
99
|
-
if (record.schemaVersion
|
|
99
|
+
if (![1, 2].includes(record.schemaVersion) || !timestamp(record.recordedAt)) { malformed += 1; return []; }
|
|
100
100
|
if (record.event === "session_compact") {
|
|
101
101
|
if (typeof record.willRetry !== "boolean" || typeof record.fromExtension !== "boolean") { malformed += 1; return []; }
|
|
102
102
|
return [{ ordinal, recordedAt: record.recordedAt, kind: "context-compaction", title: "Context đã được compact",
|
|
@@ -60,7 +60,7 @@ export function projectTaskHandoffHistory(input: { cwd: string; task: Task; iden
|
|
|
60
60
|
const matching = inspected.records.flatMap((record: any, ordinal: number) => {
|
|
61
61
|
if (record?.event !== "handoff_projection_written" || record.taskId !== input.task.taskId
|
|
62
62
|
|| record.taskRunId !== input.task.taskRunId || record.sessionId !== input.task.sessionId) return [];
|
|
63
|
-
if (record.schemaVersion
|
|
63
|
+
if (![1, 2].includes(record.schemaVersion) || !timestamp(record.recordedAt) || typeof record.completionApproved !== "boolean") { malformed += 1; return []; }
|
|
64
64
|
const action = display(record.recoveryAction, 80) || "unknown", phase = display(record.phase, 64) || null;
|
|
65
65
|
return [{ ordinal, recordedAt: record.recordedAt, phase, completionApproved: record.completionApproved, projectedAction: action }];
|
|
66
66
|
});
|
|
@@ -5,7 +5,7 @@ import { inspectOwnedWorkBudget, type OwnedWorkReservation } from "../orchestrat
|
|
|
5
5
|
import { taskRunOpaqueRef } from "./task-run-index.ts";
|
|
6
6
|
|
|
7
7
|
type Task = { taskId: string; taskRunId: string; trace?: { outcome?: unknown }; orchestration?: Record<string, unknown>;
|
|
8
|
-
acceptanceReceipt?: { helperUsage?: { used?: unknown; helpers?: unknown[] } }; [key: string]: any };
|
|
8
|
+
acceptanceReceipt?: { helperUsage?: { used?: unknown; decision?: unknown; projectedSavingsRatio?: unknown; reasonCodes?: unknown; helpers?: unknown[] } }; [key: string]: any };
|
|
9
9
|
type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string; taskRunId: string;
|
|
10
10
|
agentOperationId: null; toolCallId: null };
|
|
11
11
|
|
|
@@ -53,9 +53,32 @@ function orchestration(task: Task) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function delegation(task: Task) {
|
|
57
|
+
const usage = task.acceptanceReceipt?.helperUsage;
|
|
58
|
+
const explicitDecision = usage?.decision;
|
|
59
|
+
const action = explicitDecision === "dispatch" || explicitDecision === "skip"
|
|
60
|
+
? explicitDecision
|
|
61
|
+
: usage?.used === true ? "dispatch" : "skip";
|
|
62
|
+
const reasonCodes = Array.isArray(usage?.reasonCodes)
|
|
63
|
+
? [...new Set(usage.reasonCodes.map((item) => String(item ?? "").trim()).filter((item) => /^[a-z0-9_.:-]+$/.test(item)).slice(0, 16))]
|
|
64
|
+
: action === "dispatch" ? ["helper-dispatched"] : ["parent-direct-default", "delegation-evidence-missing"];
|
|
65
|
+
const projected = usage?.projectedSavingsRatio;
|
|
66
|
+
return {
|
|
67
|
+
action,
|
|
68
|
+
reasonCodes,
|
|
69
|
+
projectedNetSavingsRatio: typeof projected === "number" && Number.isFinite(projected)
|
|
70
|
+
? Math.max(-10, Math.min(1, projected))
|
|
71
|
+
: null,
|
|
72
|
+
minimumRequiredNetSavingsRatio: 0.3,
|
|
73
|
+
budget: { maxConcurrent: 1, maxTotal: 1, maxRetries: 0, maxWriters: 0 }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
function unavailable(identity: Identity, runRef: string, generatedAt: string, reasonCode: string) {
|
|
57
78
|
return { schemaVersion: 1, version: "piagent-webui-subagent-tree-v1", generatedAt, identity: structuredClone(identity), runRef,
|
|
58
79
|
state: "unavailable", treeRevision: null, evidenceState: "unknown", orchestration: { mode: "unknown", subagents: "unknown" },
|
|
80
|
+
delegation: { action: "unknown", reasonCodes: [reasonCode], projectedNetSavingsRatio: null,
|
|
81
|
+
minimumRequiredNetSavingsRatio: 0.3, budget: { maxConcurrent: 1, maxTotal: 1, maxRetries: 0, maxWriters: 0 } },
|
|
59
82
|
parent: null, children: [], writer: { state: "unknown", ownerNodeRef: null },
|
|
60
83
|
nestedLineage: { state: "unavailable", reasonCode: "no-durable-nested-lineage" },
|
|
61
84
|
summary: { total: 0, active: 0, completed: 0, staleResults: 0, readOnly: 0, singleWriter: 0 }, warnings: [],
|
|
@@ -66,7 +89,7 @@ export function projectTaskSubagentTree(input: { cwd: string; task: Task; identi
|
|
|
66
89
|
const generatedAt = input.generatedAt ?? new Date().toISOString(), runRef = taskRunOpaqueRef(input.task.taskRunId);
|
|
67
90
|
const inspected = inspectOwnedWorkBudget(input.cwd, input.task.taskId, input.task.taskRunId, generatedAt);
|
|
68
91
|
if (inspected.state === "corrupt") return unavailable(input.identity, runRef, generatedAt, inspected.reasonCode ?? "helper-budget-corrupt");
|
|
69
|
-
const policy = orchestration(input.task), parentRef = opaque("parent", input.task.taskRunId), receipts = helperReceipts(input.task);
|
|
92
|
+
const policy = orchestration(input.task), delegationDecision = delegation(input.task), parentRef = opaque("parent", input.task.taskRunId), receipts = helperReceipts(input.task);
|
|
70
93
|
const children = inspected.reservations.map((item) => child(input.task.taskRunId, parentRef, item, receipts.get(item.deduplicationKey)));
|
|
71
94
|
const matchedReceipts = new Set(inspected.reservations.map((item) => item.deduplicationKey).filter((key) => receipts.has(key)));
|
|
72
95
|
const unmatchedReceipts = Math.max(0, receipts.size - matchedReceipts.size);
|
|
@@ -90,10 +113,10 @@ export function projectTaskSubagentTree(input: { cwd: string; task: Task; identi
|
|
|
90
113
|
staleResults: children.filter((item) => item.result.state === "stale-result").length,
|
|
91
114
|
readOnly: children.filter((item) => item.authority === "read-only").length,
|
|
92
115
|
singleWriter: children.filter((item) => item.authority === "single-writer").length };
|
|
93
|
-
const treeRevision = `subagent-tree.${createHash("sha256").update(JSON.stringify({ policy, parentState, terminal: inspected.terminal,
|
|
116
|
+
const treeRevision = `subagent-tree.${createHash("sha256").update(JSON.stringify({ policy, delegationDecision, parentState, terminal: inspected.terminal,
|
|
94
117
|
children, writer, evidenceState })).digest("hex")}`;
|
|
95
118
|
return { schemaVersion: 1, version: "piagent-webui-subagent-tree-v1", generatedAt, identity: structuredClone(input.identity), runRef,
|
|
96
|
-
state: "ready", treeRevision, evidenceState, orchestration: policy,
|
|
119
|
+
state: "ready", treeRevision, evidenceState, orchestration: policy, delegation: delegationDecision,
|
|
97
120
|
parent: { nodeRef: parentRef, lifecycleState: parentState, budgetTerminal: inspected.terminal, mergeOwner: "parent" }, children, writer,
|
|
98
121
|
nestedLineage: { state: "unavailable", reasonCode: "no-durable-nested-lineage" }, summary, warnings,
|
|
99
122
|
health: warnings.length ? { state: "degraded", reasonCode: "subagent-tree-incomplete", message: "Some helper/subagent evidence is incomplete." }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type ToolFailureReasonCode =
|
|
2
|
+
| "target-not-found"
|
|
3
|
+
| "search-target-missing"
|
|
4
|
+
| "edit-anchor-not-unique"
|
|
5
|
+
| "edit-anchor-stale"
|
|
6
|
+
| "helper-dispatch-rejected"
|
|
7
|
+
| "helper-insufficient-evidence"
|
|
8
|
+
| "tool-result-failed";
|
|
9
|
+
|
|
10
|
+
function boundedText(content: unknown, depth = 0): string {
|
|
11
|
+
if (depth > 2) return "";
|
|
12
|
+
const nested = content && typeof content === "object" && !Array.isArray(content)
|
|
13
|
+
? (content as { content?: unknown }).content
|
|
14
|
+
: undefined;
|
|
15
|
+
const text = typeof content === "string" ? content : Array.isArray(content)
|
|
16
|
+
? content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
|
|
17
|
+
.map((item) => String((item as { text?: unknown }).text ?? "")).join("\n")
|
|
18
|
+
: nested !== undefined ? boundedText(nested, depth + 1) : "";
|
|
19
|
+
return text.slice(0, 8_192);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function commandFromInput(input: unknown): string {
|
|
23
|
+
if (!input || typeof input !== "object") return "";
|
|
24
|
+
const direct = (input as { command?: unknown }).command;
|
|
25
|
+
if (typeof direct === "string") return direct;
|
|
26
|
+
const args = (input as { args?: unknown }).args;
|
|
27
|
+
return args && typeof args === "object" && typeof (args as { command?: unknown }).command === "string"
|
|
28
|
+
? (args as { command: string }).command : "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function classifyToolFailure(toolName: string, isError: boolean, content: unknown, input?: unknown): ToolFailureReasonCode | null {
|
|
32
|
+
const text = boundedText(content);
|
|
33
|
+
const subagent = /(?:^|[._-])subagents?(?:$|[._-])/i.test(toolName);
|
|
34
|
+
// Some subagent providers return a successful tool envelope even though no
|
|
35
|
+
// child was launched, or a zero-exit child explicitly reports that it could
|
|
36
|
+
// inspect no evidence. Canonical replay must not turn either outcome into a
|
|
37
|
+
// green "passed" Activity row merely because `isError` is false.
|
|
38
|
+
if (subagent && /spawn limit reached|no children were started|declared run cannot fit/i.test(text)) {
|
|
39
|
+
return "helper-dispatch-rejected";
|
|
40
|
+
}
|
|
41
|
+
if (subagent && /scope inspected:[^\n]{0,16}\bnone\b|insufficient-evidence rule|no source (?:content|evidence)[^\n]{0,80}(?:accessible|collected)/i.test(text)) {
|
|
42
|
+
return "helper-insufficient-evidence";
|
|
43
|
+
}
|
|
44
|
+
if (!isError) return null;
|
|
45
|
+
// Only the registered, policy-governed `edit` tool may receive an automatic
|
|
46
|
+
// current-file recovery snapshot. A similarly named third-party `replace`
|
|
47
|
+
// tool is not necessarily covered by Piagent's mutation guard.
|
|
48
|
+
if (/^edit$/i.test(toolName)) {
|
|
49
|
+
if (/\b(?:oldtext|old text)\b[^\n]{0,240}\b(?:unique|occurrences?)\b|\bfound\s+\d+\s+occurrences?\b/i.test(text)) {
|
|
50
|
+
return "edit-anchor-not-unique";
|
|
51
|
+
}
|
|
52
|
+
if (/could not find (?:the )?exact text|\b(?:oldtext|old text)\b[^\n]{0,240}\b(?:match|mismatch|stale|changed|find)\b|\b(?:generation|content) drift\b/i.test(text)) {
|
|
53
|
+
return "edit-anchor-stale";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const readLike = /(?:^|[._-])(?:read|document[_-]?read)(?:$|[._-])/i.test(toolName);
|
|
57
|
+
if (readLike && /\bENOENT\b|no such file or directory|file (?:does not exist|not found)|cannot find the (?:file|path)/i.test(text)) {
|
|
58
|
+
return "target-not-found";
|
|
59
|
+
}
|
|
60
|
+
const shellLike = /^(?:bash|shell|exec|command)$/i.test(toolName);
|
|
61
|
+
const searchCommand = /^\s*(?:rg|grep|git\s+grep)\b/.test(commandFromInput(input));
|
|
62
|
+
const missingTarget = /no such file or directory|cannot find the (?:file|path)|\bos error 2\b/i.test(text);
|
|
63
|
+
const usefulMatches = text.split(/\r?\n/).some((line) => !/^\s*(?:rg|grep):/i.test(line) && /^.+?:\d+:/.test(line));
|
|
64
|
+
if (shellLike && searchCommand && missingTarget && usefulMatches) return "search-target-missing";
|
|
65
|
+
return "tool-result-failed";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function handledToolFailure(reasonCode: unknown): boolean {
|
|
69
|
+
return reasonCode === "search-target-missing";
|
|
70
|
+
}
|