@piagent/platform 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +17 -14
- package/README.vi.md +26 -1
- package/benchmarks/production-v1/spend-control.v1.json +27 -3
- package/benchmarks/production-v1/suite.json +5 -1
- package/catalog/capabilities.json +16 -16
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +23 -31
- package/docs/context-engine.md +5 -3
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +26 -13
- package/docs/languages.json +4 -4
- package/docs/operator-manual-vietnamese.md +21 -41
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +179 -23
- package/docs/release-install-policy.md +6 -6
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/security-threat-model.md +1 -1
- package/docs/subagent-orchestration-capabilities.md +19 -51
- package/docs/subagents-and-multiagent.md +21 -47
- package/docs/task-implementation-contract.md +3 -3
- package/docs/task-lifecycle-vietnamese.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +26 -13
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +89 -95
- package/docs-site/commands.html +1 -1
- package/docs-site/content/benchmark.html +88 -94
- package/docs-site/content/en/benchmark.html +134 -90
- package/docs-site/content/en/index.html +19 -19
- package/docs-site/content/en/runtime.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -1
- package/docs-site/content/en/workflows.html +2 -2
- package/docs-site/content/index.html +19 -19
- package/docs-site/content/runtime.html +4 -4
- package/docs-site/content/whats-new.html +12 -1
- package/docs-site/content/workflows.html +2 -2
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +135 -91
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +20 -20
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +5 -5
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -2
- package/docs-site/en/workflows.html +3 -3
- package/docs-site/index.html +20 -20
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +5 -5
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -2
- package/docs-site/workflows.html +3 -3
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
- package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
- package/evals/fixtures/helper-request.invalid.json +1 -1
- package/evals/fixtures/helper-request.valid.json +1 -1
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
- package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +3 -3
- package/evals/fs5-pilot-protocol.v2.json +8 -8
- package/evals/fs5-pilot-protocol.v3.json +8 -8
- package/evals/fs5-pilot-protocol.v4.json +6 -6
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/evals/runtime-conformance-v1/README.md +43 -0
- package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
- package/evals/runtime-conformance-v1/lane.json +57 -0
- package/evals/runtime-conformance-v1/paid-canary.json +31 -0
- package/evals/runtime-conformance-v1/runner.mjs +456 -0
- package/package.json +1 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
- package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
- package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
- package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
- package/packages/piagent-core/extensions/context-engine.js +1 -11
- package/packages/piagent-core/extensions/context-index-policy.js +4 -0
- package/packages/piagent-core/extensions/context-query-paths.js +57 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
- package/packages/piagent-core/extensions/criterion-graph.js +4 -1
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +4 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
- package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
- package/packages/piagent-core/extensions/task-state.js +69 -8
- package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +42 -2
- package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
- package/packages/piagent-core/prompts/be-to-fe.md +2 -8
- package/packages/piagent-core/prompts/plan.md +1 -1
- package/packages/piagent-core/prompts/platform-improve.md +2 -6
- package/packages/piagent-core/prompts/review.md +2 -7
- package/packages/piagent-core/prompts/scout.md +1 -1
- package/packages/piagent-core/prompts/task.md +4 -3
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
- package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
- package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
- package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
- package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
- package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
- package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
- package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
- package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
- package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
- package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
- package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
- package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
- package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
- package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
- package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
- package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
- package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
- package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
- package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
- package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
- package/packages/piagent-core/subagents/piagent-planner.md +6 -4
- package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
- package/packages/piagent-core/subagents/piagent-scout.md +4 -2
- package/packages/piagent-core/subagents/piagent-worker.md +6 -4
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
- package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
- package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
- package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
- package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
- package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
- package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
- package/packages/piagent-webui/server/transcript-projection.ts +91 -5
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
- package/schemas/project-profile.schema.json +1 -1
- package/schemas/task-contract.schema.json +15 -0
- package/scripts/benchmark-runner-core.mjs +14 -6
- package/scripts/benchmark-runner-finalization.mjs +4 -0
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/team-doctor.sh +1 -1
- package/scripts/verify-local.sh +5 -0
|
@@ -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
|
|
|
@@ -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." }
|
|
@@ -3,13 +3,19 @@ export type ToolFailureReasonCode =
|
|
|
3
3
|
| "search-target-missing"
|
|
4
4
|
| "edit-anchor-not-unique"
|
|
5
5
|
| "edit-anchor-stale"
|
|
6
|
+
| "helper-dispatch-rejected"
|
|
7
|
+
| "helper-insufficient-evidence"
|
|
6
8
|
| "tool-result-failed";
|
|
7
9
|
|
|
8
|
-
function boundedText(content: unknown): string {
|
|
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;
|
|
9
15
|
const text = typeof content === "string" ? content : Array.isArray(content)
|
|
10
16
|
? content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
|
|
11
17
|
.map((item) => String((item as { text?: unknown }).text ?? "")).join("\n")
|
|
12
|
-
: "";
|
|
18
|
+
: nested !== undefined ? boundedText(nested, depth + 1) : "";
|
|
13
19
|
return text.slice(0, 8_192);
|
|
14
20
|
}
|
|
15
21
|
|
|
@@ -23,8 +29,19 @@ function commandFromInput(input: unknown): string {
|
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function classifyToolFailure(toolName: string, isError: boolean, content: unknown, input?: unknown): ToolFailureReasonCode | null {
|
|
26
|
-
if (!isError) return null;
|
|
27
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;
|
|
28
45
|
// Only the registered, policy-governed `edit` tool may receive an automatic
|
|
29
46
|
// current-file recovery snapshot. A similarly named third-party `replace`
|
|
30
47
|
// tool is not necessarily covered by Piagent's mutation guard.
|
|
@@ -84,8 +84,13 @@ function scopedEvents(events: ActivityInspectorEvent[], sessionId: string, task?
|
|
|
84
84
|
return [...deduped.values()].sort((left, right) => String(left.recordedAt).localeCompare(String(right.recordedAt))).slice(-2_000);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
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>();
|
|
89
94
|
for (const entry of sessionEntries) {
|
|
90
95
|
if (!entry || typeof entry !== "object" || (entry as any).type !== "message") continue;
|
|
91
96
|
const message = (entry as any).message;
|
|
@@ -93,16 +98,44 @@ function enrichedFailureReasons(events: ActivityInspectorEvent[], sessionEntries
|
|
|
93
98
|
for (const block of message.content) if (block?.type === "toolCall" && typeof block.id === "string") inputs.set(block.id, block.arguments);
|
|
94
99
|
}
|
|
95
100
|
if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
|
|
96
|
-
|
|
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 });
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
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);
|
|
102
115
|
if (!raw) return event;
|
|
103
|
-
const reasonCode = classifyToolFailure(raw.toolName, raw.isError, raw.content, inputs.get(event.toolCallId));
|
|
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
|
+
}
|
|
104
125
|
return reasonCode ? { ...event, reasonCode } : event;
|
|
105
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 ?? "")));
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
function sourceSummary(document: SourceChangeDocument | null, view: "task" | "working-tree" | "staged") {
|
|
@@ -302,12 +335,15 @@ export async function buildWebUiInspectionProjection(input: {
|
|
|
302
335
|
const taskRevision = input.task ? webUiTaskRevision(input.task) : null;
|
|
303
336
|
const views = await collectSourceChangeViews({ cwd: input.cwd, identity, generatedAt, taskRevision,
|
|
304
337
|
isProtectedPath: (_root, repoPath) => Boolean(matchesProtectedPath(repoPath, effectiveProtectedPaths)) });
|
|
305
|
-
const
|
|
338
|
+
const canonicalEvidence = canonicalToolEvidence(input.sessionEntries ?? []);
|
|
339
|
+
const events = reconciledToolResults(scopedEvents(input.events ?? [], input.sessionId, input.task), canonicalEvidence);
|
|
306
340
|
const linked = projectCriteriaFileVerifier({ cwd: input.cwd, task: input.task, sourceViews: views, currentSnapshot,
|
|
307
341
|
protectedPaths: input.protectedPaths, events, at: new Date(generatedAt) });
|
|
308
342
|
const sourceChanges = { task: sourceSummary(linked.sourceViews.task, "task"), workingTree: sourceSummary(linked.sourceViews.workingTree, "working-tree"), staged: sourceSummary(linked.sourceViews.staged, "staged") };
|
|
309
343
|
const currentDigest = workingTreeSnapshotUsesCurrentAlgorithm(currentSnapshot) ? workingTreeEvidenceDigest(currentSnapshot) : null;
|
|
310
|
-
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;
|
|
311
347
|
const context = contextProjection(input.contextUsage, generatedAt), usageFacts = usage(input.sessionEntries ?? []);
|
|
312
348
|
const eventCursor = input.eventCursor ?? token("event-cursor", events.map((event) => event.activityId ?? [event.event, event.toolCallId, event.recordedAt]));
|
|
313
349
|
const runtimeRevision = token("runtime-rev", [taskRevision, sourceChanges.workingTree.revision, sourceChanges.staged.revision, eventCursor, current]);
|
|
@@ -328,8 +364,8 @@ export async function buildWebUiInspectionProjection(input: {
|
|
|
328
364
|
capabilities: capabilities(identity, generatedAt, input.resyncRequired === true, input.eventReplay),
|
|
329
365
|
session: {
|
|
330
366
|
connectionState: input.resyncRequired ? "resync-required" : "connected", connectionReason: input.resyncRequired ? "event-replay-gap" : null, displayName: input.task?.sessionName ?? null,
|
|
331
|
-
operation:
|
|
332
|
-
? { 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 }
|
|
333
369
|
: { liveness: "idle", operationRef: null, hostPhase: { state: "known", value: "idle", evidence: "derived", reasonCode: null }, startedAt: null, settledAt: null, reasonCode: null },
|
|
334
370
|
controlState: input.task ? controlState : "active", taskOutcome: input.task?.trace.outcome ?? null, approvalState: approvals.state,
|
|
335
371
|
verificationState: linked.verification.state, permissionProfile: { state: "unavailable", value: null, evidence: null, reasonCode: "permission-profile-not-projected" },
|