@piagent/platform 1.5.5 → 1.6.0
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 +117 -0
- package/README.md +9 -8
- package/README.vi.md +9 -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 +68 -0
- package/benchmarks/production-v1/suite.json +42 -3
- package/catalog/capabilities.json +6 -6
- package/docs/command-reference-vietnamese.md +6 -6
- package/docs/context-engine.md +7 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +1 -0
- package/docs/languages.json +4 -4
- package/docs/model-options.md +8 -5
- package/docs/operator-manual-vietnamese.md +13 -11
- package/docs/project-adapters.md +2 -2
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +271 -62
- package/docs/release-install-policy.md +10 -9
- 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/task-lifecycle-vietnamese.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +7 -2
- package/docs/vi/architecture.md +1 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.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/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v4.json +6 -5
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/package.json +3 -1
- 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.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
- package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
- package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
- package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -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 +3 -1
- package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
- package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
- package/packages/piagent-core/benchmark/benchmark-report.js +116 -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-stage-diagnostic.js +782 -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 +128 -2
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
- package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
- package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -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 +34 -11
- 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 +148 -145
- 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-telemetry-integrity.js +65 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
- package/packages/piagent-core/extensions/criterion-graph.js +78 -8
- package/packages/piagent-core/extensions/guard-types.ts +2 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
- package/packages/piagent-core/extensions/task-contract-view.js +17 -3
- package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
- package/packages/piagent-core/extensions/task-state.js +40 -134
- package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +2 -0
- 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 +5 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
- package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
- 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/task-compaction-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
- 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/role-policy.ts +1 -1
- package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
- package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
- 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/semantic-repair-handshake.ts +55 -2
- package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
- package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
- package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
- package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
- package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
- package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
- package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
- package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
- 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-worker.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
- package/packages/piagent-webui/dist/client/index.html +1 -1
- 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 +93 -7
- 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-start.ts +5 -1
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
- package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
- package/packages/piagent-webui/server/loopback-server.ts +8 -0
- package/packages/piagent-webui/server/transcript-projection.ts +22 -4
- package/packages/piagent-webui/shared/text-visibility.ts +10 -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/session-live-state-v1.schema.json +76 -0
- package/schemas/task-contract.schema.json +12 -1
- package/scripts/benchmark-runner-core.mjs +355 -359
- package/scripts/benchmark-runner-finalization.mjs +309 -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/helper-evaluation.mjs +1 -1
- package/scripts/install-global.sh +2 -2
- package/scripts/pi-usage-history.mjs +37 -10
- package/scripts/setup.sh +2 -2
- package/scripts/verify-local.sh +5 -1
- package/templates/global/settings.json +2 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
|
@@ -11,6 +11,7 @@ import { buildHandoffProjection, writeHandoffProjection } from "../recovery/hand
|
|
|
11
11
|
import { buildContextPreflight, buildUsageSnapshot } from "../session/usage.ts";
|
|
12
12
|
import { activeTaskToolGroups, toolGroupsForPrompt } from "../tools/tool-groups.ts";
|
|
13
13
|
import type { PiagentToolGroup } from "../tools/tool-groups.ts";
|
|
14
|
+
import type { RuntimeSessionState } from "../session/runtime-state.ts";
|
|
14
15
|
import {
|
|
15
16
|
buildFreshCommand,
|
|
16
17
|
chooseFreshWorkflow,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
} from "../workflows/task-intake.ts";
|
|
27
28
|
|
|
28
29
|
type InputHookDependencies = {
|
|
30
|
+
state: RuntimeSessionState;
|
|
29
31
|
boilerplateCollapseChars: number;
|
|
30
32
|
activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
|
|
31
33
|
authorityPolicy: (ctx: ExtensionContext, task: TaskContract) => AuthorityResumeDecision;
|
|
@@ -44,7 +46,14 @@ export function registerInputHook(pi: ExtensionAPI, dependencies: InputHookDepen
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const taskSignal = classifyContextTask(text);
|
|
47
|
-
const
|
|
49
|
+
const sessionTask = dependencies.activeTask(ctx);
|
|
50
|
+
if (sessionTask && sessionTask.trace.outcome !== "pending") dependencies.state.clearTaskBoundary(ctx, sessionTask.taskRunId);
|
|
51
|
+
else if (!sessionTask) {
|
|
52
|
+
const cachedTask = dependencies.state.taskIdentity(ctx);
|
|
53
|
+
if (cachedTask) dependencies.state.clearTaskBoundary(ctx, cachedTask.taskRunId);
|
|
54
|
+
}
|
|
55
|
+
const activeTask = sessionTask?.trace.outcome === "pending" ? sessionTask : undefined;
|
|
56
|
+
const turn = dependencies.state.beginTurn(ctx, taskSignal.promptHash);
|
|
48
57
|
const authorityPolicy = activeTask?.trace.outcome === "pending" ? dependencies.authorityPolicy(ctx, activeTask) : undefined;
|
|
49
58
|
let authorityHandoffReady = false;
|
|
50
59
|
if (activeTask && authorityPolicy?.disposition === "new-attempt-required") {
|
|
@@ -79,6 +88,7 @@ export function registerInputHook(pi: ExtensionAPI, dependencies: InputHookDepen
|
|
|
79
88
|
: promptGroups);
|
|
80
89
|
dependencies.telemetry(ctx, {
|
|
81
90
|
event: "user_input",
|
|
91
|
+
turnId: turn.turnId,
|
|
82
92
|
source: event.source,
|
|
83
93
|
promptHash: taskSignal.promptHash,
|
|
84
94
|
promptChars: taskSignal.promptChars,
|
|
@@ -4,6 +4,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
4
4
|
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
5
5
|
import { RuntimeSessionState } from "../session/runtime-state.ts";
|
|
6
6
|
import type { ObservedTaskContext } from "../session/runtime-state.ts";
|
|
7
|
+
import { confirmContextDelivery, contextDeliveryIdFromDetails } from "../context/context-delivery.ts";
|
|
7
8
|
|
|
8
9
|
type TelemetryWriter = (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
|
|
9
10
|
|
|
@@ -39,6 +40,20 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
|
|
|
39
40
|
flushObservedTaskContext
|
|
40
41
|
} = dependencies;
|
|
41
42
|
|
|
43
|
+
pi.on("message_start", async (event, ctx) => {
|
|
44
|
+
const message = event.message as unknown as { role?: unknown; details?: unknown };
|
|
45
|
+
if (message.role !== "custom") return;
|
|
46
|
+
const deliveryId = contextDeliveryIdFromDetails(message.details);
|
|
47
|
+
if (!deliveryId) return;
|
|
48
|
+
confirmContextDelivery(pi, ctx, deliveryId, {
|
|
49
|
+
state,
|
|
50
|
+
maxManifestFiles,
|
|
51
|
+
activeTask: (deliveryContext) => activeTask(deliveryContext.cwd, deliveryContext.sessionManager.getSessionId()),
|
|
52
|
+
flushObservedTaskContext,
|
|
53
|
+
telemetry
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
42
57
|
pi.on("session_info_changed", async (event, ctx) => {
|
|
43
58
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
44
59
|
const task = activeTask(ctx.cwd, sessionId);
|
|
@@ -58,6 +73,7 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
|
|
|
58
73
|
|
|
59
74
|
pi.on("turn_end", async (event, ctx) => {
|
|
60
75
|
const message = event.message as unknown as { usage?: unknown; stopReason?: unknown; role?: unknown };
|
|
76
|
+
const task = activeTask(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
61
77
|
telemetry(ctx, {
|
|
62
78
|
event: "turn_end",
|
|
63
79
|
turnIndex: event.turnIndex,
|
|
@@ -65,9 +81,13 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
|
|
|
65
81
|
role: message.role,
|
|
66
82
|
stopReason: message.stopReason,
|
|
67
83
|
usage: message.usage,
|
|
68
|
-
contextUsage: ctx.getContextUsage()
|
|
84
|
+
contextUsage: ctx.getContextUsage(),
|
|
85
|
+
taskId: task?.taskId,
|
|
86
|
+
taskRunId: task?.taskRunId,
|
|
87
|
+
taskOutcome: task?.trace.outcome
|
|
69
88
|
});
|
|
70
89
|
dependencies.onTurnEnd?.(ctx);
|
|
90
|
+
if (task && task.trace.outcome !== "pending") state.clearTaskBoundary(ctx, task.taskRunId);
|
|
71
91
|
});
|
|
72
92
|
|
|
73
93
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
@@ -96,7 +116,7 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
|
|
|
96
116
|
flushObservedTaskContext(
|
|
97
117
|
pi,
|
|
98
118
|
ctx,
|
|
99
|
-
state.
|
|
119
|
+
state.qualifiedTaskContext(ctx),
|
|
100
120
|
maxManifestFiles,
|
|
101
121
|
"context_observed_before_shutdown"
|
|
102
122
|
);
|
|
@@ -139,8 +139,9 @@ export function registerSessionStartHook(pi: ExtensionAPI, dependencies: Session
|
|
|
139
139
|
dependencies.bindTask(ctx.cwd, sessionId, sessionName, resumedTask);
|
|
140
140
|
}
|
|
141
141
|
if (resumedTask?.workingTreeDigestMigration && resumedTask.workingTreeDigestMigration.status !== "refreshed") dependencies.state.clearDigestMigrationState(ctx, resumedTask.taskRunId, resumedTask.taskId);
|
|
142
|
-
dependencies.state.cacheTaskIdentity(ctx, resumedTask);
|
|
143
|
-
|
|
142
|
+
if (resumedTask?.trace.outcome === "pending") dependencies.state.cacheTaskIdentity(ctx, resumedTask);
|
|
143
|
+
else dependencies.state.clearSession(ctx);
|
|
144
|
+
const resumeState = resumedTask?.trace.outcome === "pending" ? dependencies.inspectResume(ctx.cwd, resumedTask, sessionId) : undefined;
|
|
144
145
|
if (resumeState) dependencies.state.rememberResumeState(resumeState);
|
|
145
146
|
if (resumedTask && resumeState?.enforcementSafe) observeTrajectorySync(ctx, dependencies.syncTrajectory?.(ctx, resumedTask), dependencies.telemetry);
|
|
146
147
|
let taskRecovery: Record<string, unknown> | undefined;
|
|
@@ -217,6 +218,10 @@ export function registerSessionStartHook(pi: ExtensionAPI, dependencies: Session
|
|
|
217
218
|
}
|
|
218
219
|
dependencies.telemetry(ctx, {
|
|
219
220
|
event: "session_start",
|
|
221
|
+
// Benchmark and efficiency projections must be able to distinguish a
|
|
222
|
+
// genuine zero-recovery session from telemetry produced before this
|
|
223
|
+
// receipt/result protocol existed.
|
|
224
|
+
editRecoveryContextTelemetryVersion: 1,
|
|
220
225
|
activeTools: pi.getActiveTools().length,
|
|
221
226
|
index: engineStatus,
|
|
222
227
|
taskMigration: migration,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { matchesProtectedPath } from "../../extensions/policy-core.js";
|
|
5
|
+
|
|
6
|
+
function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
|
|
7
|
+
if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
|
|
8
|
+
let raw = candidate.trim();
|
|
9
|
+
try {
|
|
10
|
+
raw = decodeURIComponent(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
|
|
15
|
+
if (!raw.toLowerCase().startsWith("file://")) return undefined;
|
|
16
|
+
try {
|
|
17
|
+
raw = fileURLToPath(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
23
|
+
return path.relative(cwd, absolute).split(path.sep).join("/");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function filterTextBlocks(
|
|
27
|
+
content: unknown,
|
|
28
|
+
blocked: (line: string) => boolean,
|
|
29
|
+
emptyText: string,
|
|
30
|
+
noticeText: (count: number) => string
|
|
31
|
+
): { changed: boolean; content?: unknown; redactedLines: number } {
|
|
32
|
+
if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
|
|
33
|
+
let changed = false;
|
|
34
|
+
let redactedLines = 0;
|
|
35
|
+
const filtered = content.map((block) => {
|
|
36
|
+
if (!block || typeof block !== "object") return block;
|
|
37
|
+
const text = (block as { type?: unknown; text?: unknown }).text;
|
|
38
|
+
if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
|
|
39
|
+
const kept: string[] = [];
|
|
40
|
+
let blockRedactedLines = 0;
|
|
41
|
+
for (const line of text.split(/\r?\n/)) {
|
|
42
|
+
if (blocked(line)) {
|
|
43
|
+
changed = true;
|
|
44
|
+
redactedLines += 1;
|
|
45
|
+
blockRedactedLines += 1;
|
|
46
|
+
} else {
|
|
47
|
+
kept.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (blockRedactedLines === 0) return block;
|
|
51
|
+
const notice = noticeText(blockRedactedLines);
|
|
52
|
+
const nextText = kept.join("\n").trim().length > 0
|
|
53
|
+
? `${kept.join("\n")}\n${notice}`
|
|
54
|
+
: `${emptyText}\n${notice}`;
|
|
55
|
+
return { ...block, text: nextText };
|
|
56
|
+
});
|
|
57
|
+
return { changed, content: filtered, redactedLines };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function filterGrepProtectedContent(content: unknown, protectedPatterns: string[]) {
|
|
61
|
+
return filterTextBlocks(
|
|
62
|
+
content,
|
|
63
|
+
(line) => {
|
|
64
|
+
const linePath = line.match(/^(.+?)(?::\d+:|-\d+-)/)?.[1];
|
|
65
|
+
return Boolean(linePath && matchesProtectedPath(linePath, protectedPatterns));
|
|
66
|
+
},
|
|
67
|
+
"No matches found in non-protected paths.",
|
|
68
|
+
(count) => `[Piagent Pi guard redacted ${count} protected grep line${count === 1 ? "" : "s"}.]`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function filterProtectedPathListContent(
|
|
73
|
+
cwd: string,
|
|
74
|
+
content: unknown,
|
|
75
|
+
protectedPatterns: string[],
|
|
76
|
+
basePath: string,
|
|
77
|
+
toolName: string
|
|
78
|
+
) {
|
|
79
|
+
return filterTextBlocks(
|
|
80
|
+
content,
|
|
81
|
+
(line) => {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("No ") || trimmed === "(empty directory)") return false;
|
|
84
|
+
const entry = trimmed.replace(/[\\/]+$/, "");
|
|
85
|
+
const candidates = new Set<string>();
|
|
86
|
+
const direct = normalizeRelative(cwd, entry);
|
|
87
|
+
const underBase = normalizeRelative(cwd, path.posix.join(basePath || ".", entry));
|
|
88
|
+
if (direct !== undefined) candidates.add(direct);
|
|
89
|
+
if (underBase !== undefined) candidates.add(underBase);
|
|
90
|
+
return [...candidates].some((candidate) => matchesProtectedPath(candidate, protectedPatterns));
|
|
91
|
+
},
|
|
92
|
+
"No entries found in non-protected paths.",
|
|
93
|
+
(count) => `[Piagent Pi guard redacted ${count} protected ${toolName} line${count === 1 ? "" : "s"}.]`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
2
|
|
|
5
|
-
import { toolResultFingerprint } from "../../extensions/context-engine.js";
|
|
6
|
-
import type { TaskContract } from "../../extensions/guard-types.ts";
|
|
3
|
+
import { estimateContextTokens, toolResultFingerprint } from "../../extensions/context-engine.js";
|
|
7
4
|
import { matchesProtectedPath } from "../../extensions/policy-core.js";
|
|
8
5
|
import { changedSnapshotFiles, taskDeltaFilesFromSnapshot } from "../../extensions/task-contract-view.js";
|
|
9
6
|
import { classifyVerificationFailure } from "../../extensions/verification-intelligence.js";
|
|
@@ -11,25 +8,29 @@ import { redactForStorage, redactSensitiveText } from "../../extensions/redactio
|
|
|
11
8
|
import { appendObservedBashResult, hashEvidenceCommand, observedBashResultFromToolResultEvent } from "../../extensions/runtime-evidence.js";
|
|
12
9
|
import { workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
|
|
13
10
|
import { workingTreeObservation } from "../../extensions/working-tree-digest.js";
|
|
11
|
+
import { recordObservedContextEvidence } from "../context/context-evidence-qualification.ts";
|
|
12
|
+
import { confirmContextDeliveryFromToolResult, type ContextDeliveryConfirmationDependencies } from "../context/context-delivery.ts";
|
|
14
13
|
import { recordMutationResult } from "../inspection/mutation-provenance-recorder.ts";
|
|
14
|
+
import { classifyToolFailure } from "../inspection/tool-failure-classification.ts";
|
|
15
15
|
import { boundedGitDiffReview } from "../quality/performance-assurance.ts";
|
|
16
16
|
import { currentFileContentDigests } from "../quality/model-mutation-proof.ts";
|
|
17
17
|
import { boundedPerformanceReviewResultText } from "../quality/performance-review-evidence.ts";
|
|
18
|
+
import { buildEditRecoveryContext, type EditRecoveryContext } from "../recovery/edit-recovery-context.ts";
|
|
19
|
+
import { EditRecoveryDeliveryState } from "../recovery/edit-recovery-delivery.ts";
|
|
18
20
|
import { attachToolResultCompactionDetails, compactToolResultDetails, compactToolResultTextContent, type ToolResultCaptureSummary } from "../session/tool-result-compaction.ts";
|
|
19
|
-
import {
|
|
21
|
+
import type { ObservedTaskContext } from "../session/runtime-state.ts";
|
|
20
22
|
import { observeTrajectorySync } from "../trajectory/trajectory-observability.ts";
|
|
21
23
|
import type { TrajectorySyncResult } from "../trajectory/trajectory-runtime.ts";
|
|
24
|
+
import { filterGrepProtectedContent, filterProtectedPathListContent } from "./tool-result-content-guards.ts";
|
|
22
25
|
import { patchLineStats } from "./tool-result-metadata.ts";
|
|
23
26
|
|
|
27
|
+
export { filterGrepProtectedContent, filterProtectedPathListContent };
|
|
24
28
|
type ToolResultEvent = { toolCallId?: string; toolName: string; input?: unknown; content?: unknown; details?: unknown; isError?: boolean; usage?: unknown };
|
|
25
29
|
type ObservedBashResult = NonNullable<ReturnType<typeof observedBashResultFromToolResultEvent>>;
|
|
26
30
|
type ObservedVerificationResult = ObservedBashResult & { outputText?: string };
|
|
27
31
|
type WorkingTreeObservation = ReturnType<typeof workingTreeObservation>;
|
|
28
32
|
|
|
29
|
-
type ToolResultHookDependencies = {
|
|
30
|
-
state: RuntimeSessionState;
|
|
31
|
-
activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
|
|
32
|
-
maxManifestFiles: number;
|
|
33
|
+
type ToolResultHookDependencies = ContextDeliveryConfirmationDependencies & {
|
|
33
34
|
readProtectedPaths: (ctx: ExtensionContext) => string[];
|
|
34
35
|
recordObservedBash: (observed: ObservedBashResult) => void;
|
|
35
36
|
observedBashLedgerPath: (cwd: string) => string;
|
|
@@ -69,33 +70,13 @@ type ToolResultHookDependencies = {
|
|
|
69
70
|
success: boolean;
|
|
70
71
|
exitCode: number;
|
|
71
72
|
currentWorkingTreeDigest: string;
|
|
72
|
-
changedPaths: string[];
|
|
73
|
+
changedPaths: string[]; authoredChangedPaths: string[];
|
|
73
74
|
retryableFailure: boolean; correctiveFailure: boolean;
|
|
74
75
|
}
|
|
75
76
|
) => TrajectorySyncResult | undefined;
|
|
76
77
|
syncTrajectory?: (ctx: ExtensionContext, contextObserved: boolean) => TrajectorySyncResult | undefined;
|
|
77
78
|
};
|
|
78
79
|
|
|
79
|
-
function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
|
|
80
|
-
if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
|
|
81
|
-
let raw = candidate.trim();
|
|
82
|
-
try {
|
|
83
|
-
raw = decodeURIComponent(raw);
|
|
84
|
-
} catch {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
|
|
88
|
-
if (!raw.toLowerCase().startsWith("file://")) return undefined;
|
|
89
|
-
try {
|
|
90
|
-
raw = fileURLToPath(raw);
|
|
91
|
-
} catch {
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
96
|
-
return path.relative(cwd, absolute).split(path.sep).join("/");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
80
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
100
81
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
101
82
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -154,79 +135,19 @@ function redactToolResultTextContent(content: unknown): { content: unknown; reda
|
|
|
154
135
|
return { content: safeContent, redacted };
|
|
155
136
|
}
|
|
156
137
|
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
): { changed: boolean; content?: unknown; redactedLines: number } {
|
|
163
|
-
if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
|
|
164
|
-
let changed = false;
|
|
165
|
-
let redactedLines = 0;
|
|
166
|
-
const filtered = content.map((block) => {
|
|
167
|
-
if (!block || typeof block !== "object") return block;
|
|
168
|
-
const text = (block as { type?: unknown; text?: unknown }).text;
|
|
169
|
-
if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
|
|
170
|
-
const kept: string[] = [];
|
|
171
|
-
let blockRedactedLines = 0;
|
|
172
|
-
for (const line of text.split(/\r?\n/)) {
|
|
173
|
-
if (blocked(line)) {
|
|
174
|
-
changed = true;
|
|
175
|
-
redactedLines += 1;
|
|
176
|
-
blockRedactedLines += 1;
|
|
177
|
-
} else {
|
|
178
|
-
kept.push(line);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (blockRedactedLines === 0) return block;
|
|
182
|
-
const notice = noticeText(blockRedactedLines);
|
|
183
|
-
const nextText = kept.join("\n").trim().length > 0
|
|
184
|
-
? `${kept.join("\n")}\n${notice}`
|
|
185
|
-
: `${emptyText}\n${notice}`;
|
|
186
|
-
return { ...block, text: nextText };
|
|
187
|
-
});
|
|
188
|
-
return { changed, content: filtered, redactedLines };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function filterGrepProtectedContent(content: unknown, protectedPatterns: string[]) {
|
|
192
|
-
return filterTextBlocks(
|
|
193
|
-
content,
|
|
194
|
-
(line) => {
|
|
195
|
-
const linePath = line.match(/^(.+?)(?::\d+:|-\d+-)/)?.[1];
|
|
196
|
-
return Boolean(linePath && matchesProtectedPath(linePath, protectedPatterns));
|
|
197
|
-
},
|
|
198
|
-
"No matches found in non-protected paths.",
|
|
199
|
-
(count) => `[Piagent Pi guard redacted ${count} protected grep line${count === 1 ? "" : "s"}.]`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function filterProtectedPathListContent(
|
|
204
|
-
cwd: string,
|
|
205
|
-
content: unknown,
|
|
206
|
-
protectedPatterns: string[],
|
|
207
|
-
basePath: string,
|
|
208
|
-
toolName: string
|
|
209
|
-
) {
|
|
210
|
-
return filterTextBlocks(
|
|
211
|
-
content,
|
|
212
|
-
(line) => {
|
|
213
|
-
const trimmed = line.trim();
|
|
214
|
-
if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("No ") || trimmed === "(empty directory)") return false;
|
|
215
|
-
const entry = trimmed.replace(/[\\/]+$/, "");
|
|
216
|
-
const candidates = new Set<string>();
|
|
217
|
-
const direct = normalizeRelative(cwd, entry);
|
|
218
|
-
const underBase = normalizeRelative(cwd, path.posix.join(basePath || ".", entry));
|
|
219
|
-
if (direct !== undefined) candidates.add(direct);
|
|
220
|
-
if (underBase !== undefined) candidates.add(underBase);
|
|
221
|
-
return [...candidates].some((candidate) => matchesProtectedPath(candidate, protectedPatterns));
|
|
222
|
-
},
|
|
223
|
-
"No entries found in non-protected paths.",
|
|
224
|
-
(count) => `[Piagent Pi guard redacted ${count} protected ${toolName} line${count === 1 ? "" : "s"}.]`
|
|
225
|
-
);
|
|
138
|
+
function appendToolResultText(content: unknown, text: string): unknown[] {
|
|
139
|
+
const block = { type: "text", text };
|
|
140
|
+
if (Array.isArray(content)) return [...content, block];
|
|
141
|
+
if (typeof content === "string" && content) return [{ type: "text", text: content }, block];
|
|
142
|
+
return [block];
|
|
226
143
|
}
|
|
227
144
|
|
|
228
145
|
export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResultHookDependencies): void {
|
|
146
|
+
const editRecoveryDelivery = new EditRecoveryDeliveryState();
|
|
147
|
+
pi.on("session_compact", async (_event, ctx) => editRecoveryDelivery.advanceEpoch(ctx));
|
|
148
|
+
pi.on("session_shutdown", async (_event, ctx) => editRecoveryDelivery.clearSession(ctx));
|
|
229
149
|
pi.on("tool_result", async (event, ctx) => {
|
|
150
|
+
confirmContextDeliveryFromToolResult(pi, ctx, event, dependencies);
|
|
230
151
|
const taskIdentity = dependencies.state.taskIdentity(ctx);
|
|
231
152
|
const resultToolCallId = event.toolCallId ?? toolResultFingerprint(event.toolName, event.input, []).key;
|
|
232
153
|
const readProtectedPaths = dependencies.readProtectedPaths(ctx);
|
|
@@ -245,8 +166,8 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
245
166
|
}
|
|
246
167
|
|
|
247
168
|
const observedContextEntry = dependencies.observedTaskContext(ctx.cwd, event, readProtectedPaths);
|
|
248
|
-
|
|
249
|
-
const pendingContext = dependencies.state.
|
|
169
|
+
recordObservedContextEvidence(ctx, observedContextEntry, taskIdentity, dependencies);
|
|
170
|
+
const pendingContext = dependencies.state.qualifiedTaskContext(ctx);
|
|
250
171
|
const shellSnapshotBefore = dependencies.isShellTool(event.toolName)
|
|
251
172
|
? dependencies.state.consumeShellMutationSnapshot(ctx, event.toolName, event.input)
|
|
252
173
|
: undefined;
|
|
@@ -303,6 +224,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
303
224
|
}
|
|
304
225
|
}
|
|
305
226
|
const outputText = boundedToolResultText(event.content);
|
|
227
|
+
const failureReasonCode = classifyToolFailure(event.toolName, event.isError === true, event.content, event.input);
|
|
306
228
|
const performanceReviewOutputText = boundedPerformanceReviewResultText(event.content);
|
|
307
229
|
const observedExitCode = observed?.exitCode
|
|
308
230
|
?? numericExitCode(isPlainRecord(event.details) ? event.details.exitCode ?? event.details.status : undefined)
|
|
@@ -326,6 +248,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
326
248
|
exitCode: observedExitCode,
|
|
327
249
|
currentWorkingTreeDigest: currentTreeDigest,
|
|
328
250
|
changedPaths: [...new Set([...directMutationResult.changedPaths, ...shellChangedPaths])].sort(),
|
|
251
|
+
authoredChangedPaths: Object.keys(directMutationResult.recordedDigests).sort(),
|
|
329
252
|
retryableFailure: failure?.retryable === true, correctiveFailure: failure?.sourceMutationPermission === "eligible-in-scope" && failure?.confidence === "high"
|
|
330
253
|
})
|
|
331
254
|
: undefined;
|
|
@@ -389,6 +312,8 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
389
312
|
let resultContent: unknown = event.content;
|
|
390
313
|
let resultDetails: unknown = event.details;
|
|
391
314
|
let resultChanged = false;
|
|
315
|
+
let editRecovery: EditRecoveryContext | undefined;
|
|
316
|
+
const resultTarget = dependencies.extractLikelyPath(ctx.cwd, isPlainRecord(event.input) ? event.input : {});
|
|
392
317
|
if (event.toolName === "grep") {
|
|
393
318
|
const filtered = filterGrepProtectedContent(resultContent, readProtectedPaths);
|
|
394
319
|
if (filtered.changed) {
|
|
@@ -412,6 +337,44 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
412
337
|
}
|
|
413
338
|
}
|
|
414
339
|
|
|
340
|
+
const recovery = buildEditRecoveryContext({
|
|
341
|
+
cwd: ctx.cwd,
|
|
342
|
+
targetPath: resultTarget,
|
|
343
|
+
reasonCode: failureReasonCode,
|
|
344
|
+
protectedPaths: readProtectedPaths
|
|
345
|
+
});
|
|
346
|
+
if (recovery) {
|
|
347
|
+
if (editRecoveryDelivery.reserve(ctx, taskIdentity?.taskRunId ?? "", recovery.key)) {
|
|
348
|
+
editRecovery = recovery;
|
|
349
|
+
const injectedChars = recovery.text.length;
|
|
350
|
+
const injectedEstimatedTokens = estimateContextTokens(recovery.text);
|
|
351
|
+
const recoveryDetails = {
|
|
352
|
+
schemaVersion: 1,
|
|
353
|
+
targetPath: dependencies.redactText(recovery.targetPath),
|
|
354
|
+
contentHash: recovery.contentHash,
|
|
355
|
+
originalChars: recovery.originalChars,
|
|
356
|
+
injectedChars,
|
|
357
|
+
injectedEstimatedTokens,
|
|
358
|
+
sensitiveContentRedacted: recovery.redacted
|
|
359
|
+
};
|
|
360
|
+
resultDetails = isPlainRecord(resultDetails)
|
|
361
|
+
? { ...resultDetails, piagentEditRecovery: recoveryDetails }
|
|
362
|
+
: { ...(resultDetails === undefined ? {} : { value: resultDetails }), piagentEditRecovery: recoveryDetails };
|
|
363
|
+
resultChanged = true;
|
|
364
|
+
dependencies.telemetry(ctx, {
|
|
365
|
+
event: "edit_recovery_context",
|
|
366
|
+
toolCallId: resultToolCallId,
|
|
367
|
+
taskRunId: taskIdentity?.taskRunId,
|
|
368
|
+
targetPath: dependencies.redactText(recovery.targetPath),
|
|
369
|
+
contentHash: recovery.contentHash,
|
|
370
|
+
originalChars: recovery.originalChars,
|
|
371
|
+
injectedChars,
|
|
372
|
+
injectedEstimatedTokens,
|
|
373
|
+
sensitiveContentRedacted: recovery.redacted
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
415
378
|
const safeContent = redactToolResultTextContent(resultContent);
|
|
416
379
|
const safeDetails = redactForStorage(resultDetails);
|
|
417
380
|
const sensitiveValuesRedacted = safeContent.redacted + countChangedStringLeaves(resultDetails, safeDetails);
|
|
@@ -463,11 +426,22 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
463
426
|
if (compactionCaptures.length > 0) {
|
|
464
427
|
resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
|
|
465
428
|
}
|
|
429
|
+
// Compact the original error independently, then append the already bounded
|
|
430
|
+
// and sanitized recovery snapshot. Otherwise a small but line-dense file can
|
|
431
|
+
// be compacted into a preview and cease to be a complete one-shot recovery.
|
|
432
|
+
if (editRecovery) {
|
|
433
|
+
resultContent = appendToolResultText(resultContent, editRecovery.text);
|
|
434
|
+
resultChanged = true;
|
|
435
|
+
}
|
|
466
436
|
|
|
467
437
|
const resultFingerprintId = event.toolCallId ?? fingerprint.key;
|
|
468
438
|
const record = dependencies.activity ?? dependencies.telemetry;
|
|
469
|
-
const
|
|
439
|
+
const changedPaths = [...new Set([...directMutationResult.changedPaths, ...shellChangedPaths])]
|
|
440
|
+
.filter((filePath) => !matchesProtectedPath(filePath, readProtectedPaths))
|
|
441
|
+
.map((filePath) => dependencies.redactText(filePath))
|
|
442
|
+
.sort();
|
|
470
443
|
const lineStats = patchLineStats(event.details);
|
|
444
|
+
const editRecoveryFailure = ["edit-anchor-not-unique", "edit-anchor-stale"].includes(String(failureReasonCode));
|
|
471
445
|
record(ctx, {
|
|
472
446
|
activityId: `result:${resultFingerprintId}`,
|
|
473
447
|
event: "tool_result",
|
|
@@ -475,6 +449,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
475
449
|
toolCallId: resultFingerprintId,
|
|
476
450
|
toolName: event.toolName,
|
|
477
451
|
targetPath: resultTarget ? dependencies.redactText(resultTarget) : undefined,
|
|
452
|
+
changedPaths: changedPaths.length > 0 ? changedPaths : undefined,
|
|
478
453
|
inputHash: fingerprint.inputHash,
|
|
479
454
|
outputHash: fingerprint.outputHash,
|
|
480
455
|
outputChars: fingerprint.outputChars,
|
|
@@ -484,6 +459,13 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
|
|
|
484
459
|
compactedCaptures: compactionCaptures.length,
|
|
485
460
|
sensitiveValuesRedacted,
|
|
486
461
|
isError: event.isError,
|
|
462
|
+
reasonCode: failureReasonCode,
|
|
463
|
+
// An explicit false is evidence too: it proves that every classified
|
|
464
|
+
// edit-anchor failure passed through the recovery policy even when the
|
|
465
|
+
// target was too large, private, protected, or otherwise ineligible.
|
|
466
|
+
editRecoveryContext: editRecoveryFailure ? Boolean(editRecovery) : undefined,
|
|
467
|
+
editRecoveryInjectedChars: editRecovery?.text.length,
|
|
468
|
+
editRecoveryEstimatedTokens: editRecovery ? estimateContextTokens(editRecovery.text) : undefined,
|
|
487
469
|
exitCode: observed?.exitCode ?? (dependencies.isShellTool(event.toolName) ? event.isError ? 1 : 0 : undefined),
|
|
488
470
|
exitCodeExact: observed?.exitCode !== undefined,
|
|
489
471
|
...lineStats,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
4
4
|
import type { ActivityInspectorEvent } from "../product/activity-inspector.ts";
|
|
5
|
+
import { handledToolFailure } from "./tool-failure-classification.ts";
|
|
5
6
|
import type { RuntimeEventDraft, RuntimeEventRevision } from "./runtime-event-store.ts";
|
|
6
7
|
|
|
7
8
|
export type RuntimeEventIdentity = {
|
|
@@ -52,7 +53,8 @@ export function adaptActivityTelemetryEvent(input: ActivityEventAdapterInput): R
|
|
|
52
53
|
const operationRef = hasOperation ? opaque("operation", input.identity.agentOperationId) : null;
|
|
53
54
|
const toolCallRef = hasOperation ? opaque("tool", rawToolCall) : null;
|
|
54
55
|
const activityType = hasOperation ? "tool" : ["bash", "shell", "exec"].includes(toolName) ? "command" : "other";
|
|
55
|
-
const
|
|
56
|
+
const resultFailure = eventName === "tool_result" && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0);
|
|
57
|
+
const failed = resultFailure && !handledToolFailure(event.reasonCode);
|
|
56
58
|
const blocked = eventName === "tool_decision";
|
|
57
59
|
// `tool_call` is emitted before policy authorization. Calling it "started"
|
|
58
60
|
// made the Dashboard claim a command was running while it was still waiting
|
|
@@ -64,7 +66,7 @@ export function adaptActivityTelemetryEvent(input: ActivityEventAdapterInput): R
|
|
|
64
66
|
const preview = safeText(previewValue, 4000);
|
|
65
67
|
const storedRedactions = typeof event.sensitiveValuesRedacted === "number" && Number.isInteger(event.sensitiveValuesRedacted)
|
|
66
68
|
? Math.max(0, Math.min(10_000, event.sensitiveValuesRedacted)) : 0;
|
|
67
|
-
const reasonCode = blocked ? "policy-blocked" :
|
|
69
|
+
const reasonCode = blocked ? "policy-blocked" : resultFailure ? safeText(event.reasonCode ?? "tool-result-failed", 80).text || "tool-result-failed" : null;
|
|
68
70
|
return {
|
|
69
71
|
...base(input.identity, input.revision, observedAt), agentOperationId: operationRef, toolCallId: toolCallRef,
|
|
70
72
|
kind, evidence: "derived",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ActivityInspectorEvent } from "../product/activity-inspector.ts";
|
|
2
|
+
|
|
3
|
+
const RECOVERY_WINDOW_MS = 15 * 60 * 1_000;
|
|
4
|
+
const SHELL_TOOLS = new Set(["bash", "shell", "exec", "command"]);
|
|
5
|
+
const MUTATION_TOOLS = /(?:^|[._-])(?:apply[_-]?patch|edit|write|create[_-]?file|replace)(?:$|[._-])/i;
|
|
6
|
+
|
|
7
|
+
export type ActivityRecovery = {
|
|
8
|
+
recoveredAt: string;
|
|
9
|
+
recoveryToolCallId: string;
|
|
10
|
+
recoveryToolName: string;
|
|
11
|
+
exitCode: number | null;
|
|
12
|
+
exitCodeExact: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function resultFailed(event: ActivityInspectorEvent | undefined): boolean {
|
|
16
|
+
return Boolean(event && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function time(value: unknown): number {
|
|
20
|
+
const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
21
|
+
return Number.isFinite(parsed) ? parsed : Number.NaN;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalized(value: unknown): string {
|
|
25
|
+
return typeof value === "string" ? value.replaceAll("\\", "/").replace(/\s+/g, " ").trim() : "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function family(toolName: unknown): "read" | "mutation" | "shell" | "other" {
|
|
29
|
+
const value = String(toolName ?? "").toLowerCase();
|
|
30
|
+
if (SHELL_TOOLS.has(value)) return "shell";
|
|
31
|
+
if (MUTATION_TOOLS.test(value)) return "mutation";
|
|
32
|
+
if (/(?:^|[._-])(?:read|document[_-]?read)(?:$|[._-])/.test(value)) return "read";
|
|
33
|
+
return "other";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function likelyCorrectedReadTarget(failed: ActivityInspectorEvent, candidate: ActivityInspectorEvent): boolean {
|
|
37
|
+
if (family(failed.toolName) !== "read" || family(candidate.toolName) !== "read") return false;
|
|
38
|
+
const failedPath = normalized(failed.targetPath).toLowerCase();
|
|
39
|
+
const candidatePath = normalized(candidate.targetPath).toLowerCase();
|
|
40
|
+
const failedSlash = failedPath.lastIndexOf("/"), candidateSlash = candidatePath.lastIndexOf("/");
|
|
41
|
+
if (!failedPath || !candidatePath || failedPath.slice(0, failedSlash) !== candidatePath.slice(0, candidateSlash)) return false;
|
|
42
|
+
const failedName = failedPath.slice(failedSlash + 1), candidateName = candidatePath.slice(candidateSlash + 1);
|
|
43
|
+
const failedExtension = failedName.includes(".") ? failedName.slice(failedName.lastIndexOf(".")) : "";
|
|
44
|
+
const candidateExtension = candidateName.includes(".") ? candidateName.slice(candidateName.lastIndexOf(".")) : "";
|
|
45
|
+
if (failedExtension !== candidateExtension) return false;
|
|
46
|
+
const tokens = (value: string) => new Set(value.slice(0, value.length - failedExtension.length).split(/[^a-z0-9]+/).filter(Boolean));
|
|
47
|
+
const failedTokens = tokens(failedName), candidateTokens = tokens(candidateName);
|
|
48
|
+
const overlap = [...failedTokens].filter((token) => candidateTokens.has(token)).length;
|
|
49
|
+
return overlap >= 2 && overlap === Math.min(failedTokens.size, candidateTokens.size);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameLogicalTarget(failed: ActivityInspectorEvent, candidate: ActivityInspectorEvent): boolean {
|
|
53
|
+
const failedPath = normalized(failed.targetPath), candidatePath = normalized(candidate.targetPath);
|
|
54
|
+
if (failedPath && candidatePath && failedPath === candidatePath) {
|
|
55
|
+
const failedFamily = family(failed.toolName), candidateFamily = family(candidate.toolName);
|
|
56
|
+
return failedFamily === candidateFamily || failedFamily === "read" && candidateFamily === "mutation";
|
|
57
|
+
}
|
|
58
|
+
const failedCommand = normalized(failed.command), candidateCommand = normalized(candidate.command);
|
|
59
|
+
return Boolean(failedCommand && candidateCommand && failedCommand === candidateCommand
|
|
60
|
+
&& family(failed.toolName) === "shell" && family(candidate.toolName) === "shell");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function recoveredToolCalls(events: readonly ActivityInspectorEvent[]): ReadonlyMap<string, ActivityRecovery> {
|
|
64
|
+
const results = new Map<string, { event: ActivityInspectorEvent; order: number }>();
|
|
65
|
+
for (let order = 0; order < events.length; order += 1) {
|
|
66
|
+
const event = events[order];
|
|
67
|
+
if (event?.event === "tool_result" && event.toolCallId) results.set(event.toolCallId, { event, order });
|
|
68
|
+
}
|
|
69
|
+
const segments = new Map<number, number>();
|
|
70
|
+
let segment = 0;
|
|
71
|
+
for (let order = 0; order < events.length; order += 1) {
|
|
72
|
+
if (events[order]?.event === "user_input") segment += 1;
|
|
73
|
+
segments.set(order, segment);
|
|
74
|
+
}
|
|
75
|
+
const calls = events.map((event, order) => ({ event, order }))
|
|
76
|
+
.filter((entry) => entry.event?.event === "tool_call" && Boolean(entry.event.toolCallId));
|
|
77
|
+
const recovered = new Map<string, ActivityRecovery>();
|
|
78
|
+
for (let failedCallIndex = 0; failedCallIndex < calls.length; failedCallIndex += 1) {
|
|
79
|
+
const failedCall = calls[failedCallIndex];
|
|
80
|
+
const failedResult = results.get(failedCall.event.toolCallId!);
|
|
81
|
+
if (!failedResult || !resultFailed(failedResult.event)) continue;
|
|
82
|
+
const failedAt = time(failedResult.event.recordedAt ?? failedCall.event.recordedAt);
|
|
83
|
+
const match = calls.find((candidate, candidateIndex) => {
|
|
84
|
+
if (candidate.order <= failedResult.order || segments.get(candidate.order) !== segments.get(failedCall.order)) return false;
|
|
85
|
+
const candidateResult = results.get(candidate.event.toolCallId!);
|
|
86
|
+
if (!candidateResult || resultFailed(candidateResult.event)) return false;
|
|
87
|
+
const recoveredAt = time(candidateResult.event.recordedAt ?? candidate.event.recordedAt);
|
|
88
|
+
return Number.isFinite(failedAt) && Number.isFinite(recoveredAt) && recoveredAt >= failedAt
|
|
89
|
+
&& recoveredAt - failedAt <= RECOVERY_WINDOW_MS
|
|
90
|
+
&& (sameLogicalTarget(failedCall.event, candidate.event)
|
|
91
|
+
|| candidateIndex === failedCallIndex + 1 && likelyCorrectedReadTarget(failedCall.event, candidate.event));
|
|
92
|
+
});
|
|
93
|
+
if (!match) continue;
|
|
94
|
+
const recoveryResult = results.get(match.event.toolCallId!)!.event;
|
|
95
|
+
recovered.set(failedCall.event.toolCallId!, {
|
|
96
|
+
recoveredAt: String(recoveryResult.recordedAt ?? match.event.recordedAt),
|
|
97
|
+
recoveryToolCallId: match.event.toolCallId!,
|
|
98
|
+
recoveryToolName: String(match.event.toolName ?? "unknown"),
|
|
99
|
+
exitCode: typeof recoveryResult.exitCode === "number" ? recoveryResult.exitCode : null,
|
|
100
|
+
exitCodeExact: recoveryResult.exitCodeExact === true
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return recovered;
|
|
104
|
+
}
|
|
@@ -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",
|