pi-ui-extend 1.0.41 → 1.0.42
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/README.md +10 -2
- package/dist/app/app.d.ts +3 -0
- package/dist/app/app.js +26 -0
- package/dist/app/commands/command-host.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +6 -1
- package/dist/app/commands/command-registry.js +1 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +59 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +6 -0
- package/dist/app/session/tabs-controller.d.ts +1 -0
- package/dist/app/session/tabs-controller.js +19 -1
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/types.d.ts +5 -0
- package/dist/default-pix-config.js +10 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +80 -58
- package/dist/schemas/pi-tools-suite-schema.js +43 -78
- package/dist/schemas/pix-schema.d.ts +7 -0
- package/dist/schemas/pix-schema.js +14 -0
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +6 -0
- package/docs/desktop-mvp.md +12 -1
- package/docs/desktop-task-manager.md +93 -82
- package/external/pi-tools-suite/README.md +37 -50
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +19 -0
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +40 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +38 -27
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/commands.ts +14 -70
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -3
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +122 -312
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +15 -2
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +7 -8
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -4
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/config.ts +41 -1
- package/external/pi-tools-suite/src/context-gateway/config.ts +11 -2
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +102 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +73 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +3 -3
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +9 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +96 -44
- package/external/pi-tools-suite/src/dcp/commands.ts +18 -0
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +170 -45
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/config.ts +56 -13
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +1 -1
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +13 -9
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/resource-registry/index.ts +2290 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +3 -4
- package/package.json +4 -2
- package/schemas/pi-tools-suite.json +427 -280
- package/schemas/pix.json +24 -0
- package/schemas/tasks.json +132 -0
- package/skills/spec-wiki/SKILL.md +55 -0
- package/skills/spec-wiki/evals/evals.json +132 -0
- package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +14 -0
- package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +4 -0
- package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +9 -0
- package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +17 -0
- package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +12 -0
- package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +11 -0
- package/skills/spec-wiki/evals/files/focused/src/session.ts +5 -0
- package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +17 -0
- package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +2 -0
- package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +3 -0
- package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +18 -0
- package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +5 -0
- package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +24 -0
- package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +7 -0
- package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +7 -0
- package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +17 -0
- package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +3 -0
- package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +3 -0
- package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +4 -0
- package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +9 -0
- package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +15 -0
- package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +6 -0
- package/skills/spec-wiki/evals/maintenance_regression.py +311 -0
- package/skills/spec-wiki/evals/retrieval-eval.md +37 -0
- package/skills/spec-wiki/evals/retrieval-evals.json +37 -0
- package/skills/spec-wiki/evals/trigger-evals.json +94 -0
- package/skills/spec-wiki/references/bootstrap.md +100 -0
- package/skills/spec-wiki/references/focused-spec.md +117 -0
- package/skills/spec-wiki/references/index-schema.md +330 -0
- package/skills/spec-wiki/references/maintenance.md +134 -0
- package/skills/spec-wiki/references/query.md +74 -0
- package/skills/spec-wiki/scripts/spec_wiki.py +2076 -0
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/spec-lite/SKILL.md +0 -140
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { classifyShellCommand, type ShellCommandClassification } from "../shell-command-policy.js";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
+
contextGatewayBudgetForClass,
|
|
5
6
|
loadContextGatewayConfig,
|
|
6
7
|
} from "./config.js";
|
|
7
|
-
import {
|
|
8
|
+
import { planContextGatewayEnforcement } from "./enforcement.js";
|
|
9
|
+
import { classifyContextGatewayTool, ContextGatewayTelemetry } from "./telemetry.js";
|
|
8
10
|
import type {
|
|
9
11
|
ContextGatewayEffectiveMode,
|
|
10
12
|
ContextGatewayMode,
|
|
11
13
|
ContextGatewayResolvedConfig,
|
|
12
14
|
ContextGatewayRuntimeState,
|
|
15
|
+
ContextGatewayToolClass,
|
|
13
16
|
} from "./types.js";
|
|
14
17
|
|
|
15
18
|
export interface RegisterContextGatewayOptions {
|
|
@@ -25,23 +28,23 @@ function formatStatus(runtime: RegisteredContextGateway): string {
|
|
|
25
28
|
const telemetry = runtime.telemetry.snapshot();
|
|
26
29
|
return [
|
|
27
30
|
`Context Gateway requested=${runtime.requestedMode}, effective=${runtime.effectiveMode}.`,
|
|
28
|
-
`Observed results=${telemetry.results}, errors=${telemetry.errors}, upstreamTruncated=${telemetry.upstreamTruncatedResults}, overBudget=${telemetry.overBudgetResults}, unbound results=${telemetry.unboundResults}.`,
|
|
29
|
-
`Repeat
|
|
31
|
+
`Observed results=${telemetry.results}, errors=${telemetry.errors}, upstreamTruncated=${telemetry.upstreamTruncatedResults}, overBudget=${telemetry.overBudgetResults}, enforced=${telemetry.enforcedResults}, unbound results=${telemetry.unboundResults}.`,
|
|
32
|
+
`Repeat exact reads=${telemetry.repeatCandidateCount}, same-source different-range reads=${telemetry.sameSourceDifferentRangeCount}, retrieval calls=${telemetry.retrievalCalls}.`,
|
|
30
33
|
`Native policy results=${telemetry.nativePolicy.results}, refusals=${telemetry.nativePolicy.refusals}, full overrides=${telemetry.nativePolicy.fullOverrides}.`,
|
|
31
|
-
`
|
|
34
|
+
`Source contentBytes=${telemetry.contentBytes}, deliveredContentBytes=${telemetry.deliveredContentBytes}, actualBytesSaved=${telemetry.actualBytesSaved}, potentialBytesOverBudget=${telemetry.potentialBytesOverBudget}, detailsBytes=${telemetry.detailsBytes}.`,
|
|
32
35
|
].join("\n");
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function formatDoctor(runtime: RegisteredContextGateway): string {
|
|
36
39
|
const lines = [
|
|
37
40
|
`Context Gateway doctor: requested=${runtime.requestedMode}, effective=${runtime.effectiveMode}.`,
|
|
38
|
-
"
|
|
41
|
+
"Budgets: read=maxExactReadBytes; repo search/AST/structure=maxSearchBytes; other results=maxResultBytes; compact inline view=maxInlineBytes.",
|
|
42
|
+
"Enforce: recognised complete simple test/build output=bounded parser compact; partial/unknown/compound/upstream-truncated/non-text=passthrough.",
|
|
43
|
+
"Read: full passthrough even when over budget; exact recovery/archive is not wired, so irreversible read truncation is disabled.",
|
|
44
|
+
"Repo tools: producer-native compact is supported with repoDiscovery.profile=native-compact; Gateway never post-hoc slices repo output.",
|
|
39
45
|
"External paths: MCP=current Pix adapter unsupported; direct parent browser capture=unsupported.",
|
|
40
46
|
"Storage: disabled; no durable archive, index, quota reservation, or retrieval tools are active.",
|
|
41
47
|
];
|
|
42
|
-
if (runtime.requestedMode === "enforce") {
|
|
43
|
-
lines.push("BLOCKED: enforce is unavailable in P01; effective mode is off.");
|
|
44
|
-
}
|
|
45
48
|
for (const issue of runtime.config.issues) lines.push(`Config issue: ${issue}`);
|
|
46
49
|
return lines.join("\n");
|
|
47
50
|
}
|
|
@@ -57,8 +60,9 @@ export function registerContextGateway(
|
|
|
57
60
|
const config = (options.loadConfig ?? (() => loadContextGatewayConfig()))();
|
|
58
61
|
const telemetry = options.telemetry ?? new ContextGatewayTelemetry();
|
|
59
62
|
let requestedMode = config.mode;
|
|
60
|
-
let effectiveMode: ContextGatewayEffectiveMode = requestedMode
|
|
61
|
-
|
|
63
|
+
let effectiveMode: ContextGatewayEffectiveMode = requestedMode;
|
|
64
|
+
type InFlight = { toolClass: ContextGatewayToolClass; shell: ShellCommandClassification };
|
|
65
|
+
const inFlightToolCalls = new Map<string, InFlight>();
|
|
62
66
|
|
|
63
67
|
const resetRuntimeTelemetry = (): void => {
|
|
64
68
|
inFlightToolCalls.clear();
|
|
@@ -71,9 +75,6 @@ export function registerContextGateway(
|
|
|
71
75
|
config,
|
|
72
76
|
telemetry,
|
|
73
77
|
setRuntimeMode(mode) {
|
|
74
|
-
if (mode === "enforce") {
|
|
75
|
-
return { ok: false, message: CONTEXT_GATEWAY_ENFORCE_UNAVAILABLE };
|
|
76
|
-
}
|
|
77
78
|
if (mode !== effectiveMode && inFlightToolCalls.size > 0) {
|
|
78
79
|
return {
|
|
79
80
|
ok: false,
|
|
@@ -88,14 +89,67 @@ export function registerContextGateway(
|
|
|
88
89
|
};
|
|
89
90
|
|
|
90
91
|
pi.on("tool_call", async (event) => {
|
|
91
|
-
if (typeof event.toolCallId === "string"
|
|
92
|
-
|
|
92
|
+
if (typeof event.toolCallId === "string" && typeof event.toolName === "string") {
|
|
93
|
+
const toolClass = classifyContextGatewayTool(event.toolName);
|
|
94
|
+
inFlightToolCalls.set(event.toolCallId, {
|
|
95
|
+
toolClass,
|
|
96
|
+
shell: toolClass === "shell"
|
|
97
|
+
? classifyShellCommand(event.input)
|
|
98
|
+
: { scope: "unknown", kind: "unknown" },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (effectiveMode === "observe" || effectiveMode === "enforce") telemetry.recordToolCall(event);
|
|
93
102
|
return undefined;
|
|
94
103
|
});
|
|
95
104
|
|
|
96
105
|
pi.on("tool_result", async (event) => {
|
|
97
106
|
try {
|
|
98
|
-
if (effectiveMode === "
|
|
107
|
+
if (effectiveMode === "off") return undefined;
|
|
108
|
+
const binding = typeof event.toolCallId === "string"
|
|
109
|
+
? inFlightToolCalls.get(event.toolCallId)
|
|
110
|
+
: undefined;
|
|
111
|
+
const classBudget = binding
|
|
112
|
+
? contextGatewayBudgetForClass(binding.toolClass, config.budgets)
|
|
113
|
+
: config.budgets.maxResultBytes;
|
|
114
|
+
const enforcement = effectiveMode === "enforce" && binding
|
|
115
|
+
? planContextGatewayEnforcement({
|
|
116
|
+
event,
|
|
117
|
+
toolClass: binding.toolClass,
|
|
118
|
+
shell: binding.shell,
|
|
119
|
+
budgetBytes: classBudget,
|
|
120
|
+
maxInlineBytes: Math.min(
|
|
121
|
+
config.budgets.maxInlineBytes,
|
|
122
|
+
classBudget,
|
|
123
|
+
),
|
|
124
|
+
})
|
|
125
|
+
: undefined;
|
|
126
|
+
telemetry.recordToolResult(event, config.budgets, {
|
|
127
|
+
maxInlineBytes: config.budgets.maxInlineBytes,
|
|
128
|
+
...(enforcement ? {
|
|
129
|
+
delivery: {
|
|
130
|
+
representation: enforcement.representation,
|
|
131
|
+
contentBytes: enforcement.contentBytes,
|
|
132
|
+
textBytes: enforcement.textBytes,
|
|
133
|
+
},
|
|
134
|
+
} : {}),
|
|
135
|
+
});
|
|
136
|
+
if (enforcement?.representation === "test-build-compact") {
|
|
137
|
+
const originalDetails = event.details && typeof event.details === "object" && !Array.isArray(event.details)
|
|
138
|
+
? event.details as Record<string, unknown>
|
|
139
|
+
: {};
|
|
140
|
+
return {
|
|
141
|
+
content: enforcement.content as any,
|
|
142
|
+
details: {
|
|
143
|
+
...originalDetails,
|
|
144
|
+
contextGateway: {
|
|
145
|
+
version: 1,
|
|
146
|
+
representation: enforcement.representation,
|
|
147
|
+
sourceContentBytes: enforcement.sourceContentBytes,
|
|
148
|
+
deliveredContentBytes: enforcement.contentBytes,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
99
153
|
} finally {
|
|
100
154
|
if (typeof event.toolCallId === "string") inFlightToolCalls.delete(event.toolCallId);
|
|
101
155
|
}
|
|
@@ -118,7 +172,7 @@ export function registerContextGateway(
|
|
|
118
172
|
});
|
|
119
173
|
|
|
120
174
|
pi.registerCommand("context-gateway", {
|
|
121
|
-
description: "Show Context Gateway status/doctor output or change
|
|
175
|
+
description: "Show Context Gateway status/doctor output or change runtime-only off/observe/enforce mode.",
|
|
122
176
|
handler: async (rawArgs, ctx) => {
|
|
123
177
|
const args = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
124
178
|
const action = (args[0] ?? "status").toLowerCase();
|
|
@@ -127,7 +181,7 @@ export function registerContextGateway(
|
|
|
127
181
|
return;
|
|
128
182
|
}
|
|
129
183
|
if (action === "doctor") {
|
|
130
|
-
notify(ctx, formatDoctor(runtime), runtime.
|
|
184
|
+
notify(ctx, formatDoctor(runtime), runtime.config.issues.length > 0 ? "warning" : "info");
|
|
131
185
|
return;
|
|
132
186
|
}
|
|
133
187
|
if (action === "mode") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type StorelessCapabilityStatus = "supported" | "limited" | "unsupported";
|
|
2
|
-
export type StorelessCapabilityStrategy = "native-passthrough" | "metadata-only" | "pure-parser-candidate" | "none";
|
|
2
|
+
export type StorelessCapabilityStrategy = "native-passthrough" | "metadata-only" | "pure-parser-candidate" | "pure-parser-compact" | "none";
|
|
3
3
|
|
|
4
4
|
export interface StorelessCapabilityEntry {
|
|
5
5
|
surface:
|
|
@@ -27,9 +27,9 @@ export const STORELESS_CAPABILITIES: readonly StorelessCapabilityEntry[] = Objec
|
|
|
27
27
|
{
|
|
28
28
|
surface: "test-build",
|
|
29
29
|
status: "limited",
|
|
30
|
-
strategy: "pure-parser-
|
|
30
|
+
strategy: "pure-parser-compact",
|
|
31
31
|
lifetime: "current-result",
|
|
32
|
-
reason: "Bun/TAP/bounded TypeScript
|
|
32
|
+
reason: "Enforce compacts only recognised complete simple Bun/TAP/bounded TypeScript output; partial, compound, truncated, or unknown results remain passthrough.",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
surface: "mutation-lsp",
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { classifyShellCommand } from "../shell-command-policy.js";
|
|
2
3
|
|
|
3
4
|
import { accountContextGatewayParts } from "./accounting.js";
|
|
5
|
+
import { contextGatewayBudgetForClass } from "./config.js";
|
|
4
6
|
import { parseTestBuildOutput, planProspectiveTestOutputDelivery } from "./test-output-parser.js";
|
|
5
7
|
import type {
|
|
8
|
+
ContextGatewayBudgets,
|
|
6
9
|
ContextGatewayClassTelemetry,
|
|
7
10
|
ContextGatewayNativePolicyReason,
|
|
8
11
|
ContextGatewayNativePolicyTelemetry,
|
|
@@ -40,6 +43,7 @@ const NATIVE_POLICY_REASONS = new Set<ContextGatewayNativePolicyReason>([
|
|
|
40
43
|
type CallBinding = {
|
|
41
44
|
toolClass: ContextGatewayToolClass;
|
|
42
45
|
inputFingerprint?: string;
|
|
46
|
+
readSourceFingerprint?: string;
|
|
43
47
|
shellCommandScope?: "simple" | "compound" | "unknown";
|
|
44
48
|
};
|
|
45
49
|
|
|
@@ -60,12 +64,15 @@ function emptyCounters(): ContextGatewayClassTelemetry {
|
|
|
60
64
|
results: 0,
|
|
61
65
|
errors: 0,
|
|
62
66
|
contentBytes: 0,
|
|
67
|
+
deliveredContentBytes: 0,
|
|
63
68
|
textBytes: 0,
|
|
64
69
|
imageBytes: 0,
|
|
65
70
|
detailsBytes: 0,
|
|
66
71
|
upstreamTruncatedResults: 0,
|
|
67
72
|
overBudgetResults: 0,
|
|
68
73
|
potentialBytesOverBudget: 0,
|
|
74
|
+
enforcedResults: 0,
|
|
75
|
+
actualBytesSaved: 0,
|
|
69
76
|
};
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -92,13 +99,46 @@ function inputFingerprint(toolName: string, input: unknown): string {
|
|
|
92
99
|
return createHash("sha256").update(`${toolName}\u0000${encoded}`).digest("hex");
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
function
|
|
96
|
-
if (
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
function normalizedReadPath(value: unknown): string | undefined {
|
|
103
|
+
if (typeof value !== "string") return undefined;
|
|
104
|
+
const raw = value.trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
105
|
+
if (!raw) return undefined;
|
|
106
|
+
const prefix = raw.startsWith("/") ? "/" : /^[A-Za-z]:\//.test(raw) ? raw.slice(0, 3).toLowerCase() : "";
|
|
107
|
+
const body = prefix === "/" ? raw.slice(1) : prefix ? raw.slice(3) : raw;
|
|
108
|
+
const stack: string[] = [];
|
|
109
|
+
for (const part of body.split("/")) {
|
|
110
|
+
if (!part || part === ".") continue;
|
|
111
|
+
if (part === "..") {
|
|
112
|
+
if (stack.length > 0 && stack.at(-1) !== "..") stack.pop();
|
|
113
|
+
else if (!prefix) stack.push(part);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
stack.push(part);
|
|
117
|
+
}
|
|
118
|
+
return `${prefix}${stack.join("/")}` || prefix || ".";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizedReadRangeValue(value: unknown): number | undefined {
|
|
122
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
|
|
123
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
124
|
+
const parsed = Number(value);
|
|
125
|
+
if (Number.isSafeInteger(parsed)) return parsed;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function contextGatewayReadIdentity(input: unknown): { exact?: string; source?: string } {
|
|
131
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
132
|
+
const record = input as Record<string, unknown>;
|
|
133
|
+
const path = normalizedReadPath(record.path ?? record.file ?? record.filePath);
|
|
134
|
+
if (!path) return {};
|
|
135
|
+
const source = inputFingerprint("read-source", { path });
|
|
136
|
+
const exact = inputFingerprint("read-exact", {
|
|
137
|
+
path,
|
|
138
|
+
offset: normalizedReadRangeValue(record.offset ?? record.start ?? record.line),
|
|
139
|
+
limit: normalizedReadRangeValue(record.limit ?? record.lines ?? record.count),
|
|
140
|
+
});
|
|
141
|
+
return { source, exact };
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
function byteLengthJson(value: unknown): number {
|
|
@@ -209,19 +249,31 @@ export function classifyContextGatewayTool(toolName: string): ContextGatewayTool
|
|
|
209
249
|
|
|
210
250
|
export function observeToolResult(
|
|
211
251
|
event: ToolResultLike,
|
|
212
|
-
|
|
213
|
-
options: {
|
|
252
|
+
budget: number | ContextGatewayBudgets,
|
|
253
|
+
options: {
|
|
254
|
+
toolClass?: ContextGatewayToolClass;
|
|
255
|
+
shellCommandScope?: "simple" | "compound" | "unknown";
|
|
256
|
+
maxInlineBytes?: number;
|
|
257
|
+
delivery?: { representation: "passthrough" | "test-build-compact"; contentBytes: number; textBytes: number };
|
|
258
|
+
} = {},
|
|
214
259
|
): ContextGatewayObservation {
|
|
215
260
|
const toolName = typeof event.toolName === "string" ? event.toolName : "unknown";
|
|
216
261
|
const sizes = contentSizes(event.content);
|
|
217
262
|
const detailsBytes = byteLengthJson(event.details);
|
|
218
|
-
const potentialBytesOverBudget = Math.max(0, sizes.contentBytes - maxResultBytes);
|
|
219
263
|
const toolClass = options.toolClass ?? classifyContextGatewayTool(toolName);
|
|
264
|
+
const budgetBytes = typeof budget === "number" ? budget : contextGatewayBudgetForClass(toolClass, budget);
|
|
265
|
+
const potentialBytesOverBudget = Math.max(0, sizes.contentBytes - budgetBytes);
|
|
220
266
|
const completeness = isTruncated(event.details) ? "upstream-truncated" : "unknown";
|
|
267
|
+
const delivery = options.delivery ?? {
|
|
268
|
+
representation: "passthrough" as const,
|
|
269
|
+
contentBytes: sizes.contentBytes,
|
|
270
|
+
textBytes: sizes.textBytes,
|
|
271
|
+
};
|
|
221
272
|
const observation: ContextGatewayObservation = {
|
|
222
273
|
version: 1,
|
|
223
274
|
toolClass,
|
|
224
275
|
outcome: event.isError === true ? "error" : "success",
|
|
276
|
+
budgetBytes,
|
|
225
277
|
source: {
|
|
226
278
|
kind: "tool-result-boundary",
|
|
227
279
|
completeness,
|
|
@@ -231,13 +283,10 @@ export function observeToolResult(
|
|
|
231
283
|
detailsBytes,
|
|
232
284
|
},
|
|
233
285
|
view: { kind: "text-content", textBytes: sizes.textBytes },
|
|
234
|
-
delivery
|
|
235
|
-
|
|
236
|
-
contentBytes: sizes.contentBytes,
|
|
237
|
-
textBytes: sizes.textBytes,
|
|
238
|
-
},
|
|
239
|
-
overBudget: sizes.contentBytes > maxResultBytes,
|
|
286
|
+
delivery,
|
|
287
|
+
overBudget: sizes.contentBytes > budgetBytes,
|
|
240
288
|
potentialBytesOverBudget,
|
|
289
|
+
actualBytesSaved: Math.max(0, sizes.contentBytes - delivery.contentBytes),
|
|
241
290
|
};
|
|
242
291
|
if (toolClass === "shell") {
|
|
243
292
|
const parsed = parseTestBuildOutput({
|
|
@@ -246,7 +295,11 @@ export function observeToolResult(
|
|
|
246
295
|
upstreamTruncated: completeness === "upstream-truncated",
|
|
247
296
|
});
|
|
248
297
|
const commandScope = options.shellCommandScope ?? "unknown";
|
|
249
|
-
const prospective = planProspectiveTestOutputDelivery(
|
|
298
|
+
const prospective = planProspectiveTestOutputDelivery(
|
|
299
|
+
parsed,
|
|
300
|
+
options.maxInlineBytes ?? budgetBytes,
|
|
301
|
+
{ commandScope },
|
|
302
|
+
);
|
|
250
303
|
observation.testOutput = {
|
|
251
304
|
parserVersion: 1,
|
|
252
305
|
classification: parsed.classification,
|
|
@@ -268,8 +321,10 @@ export class ContextGatewayTelemetry {
|
|
|
268
321
|
private byClass: Partial<Record<ContextGatewayToolClass, ContextGatewayClassTelemetry>> = {};
|
|
269
322
|
private bindings = new Map<string, CallBinding>();
|
|
270
323
|
private seenInputFingerprints = new Set<string>();
|
|
324
|
+
private seenReadSourceFingerprints = new Set<string>();
|
|
271
325
|
private unboundResults = 0;
|
|
272
326
|
private repeatCandidateCount = 0;
|
|
327
|
+
private sameSourceDifferentRangeCount = 0;
|
|
273
328
|
private retrievalCalls = 0;
|
|
274
329
|
private nativePolicy: ContextGatewayNativePolicyTelemetry = {
|
|
275
330
|
results: 0,
|
|
@@ -286,27 +341,39 @@ export class ContextGatewayTelemetry {
|
|
|
286
341
|
if (normalizedToolName === "artifact_read" || normalizedToolName === "artifact_search") {
|
|
287
342
|
this.retrievalCalls += 1;
|
|
288
343
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
: undefined;
|
|
344
|
+
const readFingerprint = normalizedToolName === "read" ? contextGatewayReadIdentity(event.input) : {};
|
|
345
|
+
const fingerprint = readFingerprint.exact;
|
|
292
346
|
if (fingerprint) {
|
|
293
347
|
if (this.seenInputFingerprints.has(fingerprint)) this.repeatCandidateCount += 1;
|
|
294
|
-
else
|
|
348
|
+
else {
|
|
349
|
+
if (readFingerprint.source && this.seenReadSourceFingerprints.has(readFingerprint.source)) {
|
|
350
|
+
this.sameSourceDifferentRangeCount += 1;
|
|
351
|
+
}
|
|
352
|
+
this.seenInputFingerprints.add(fingerprint);
|
|
353
|
+
}
|
|
295
354
|
}
|
|
355
|
+
if (readFingerprint.source) this.seenReadSourceFingerprints.add(readFingerprint.source);
|
|
356
|
+
const toolClass = classifyContextGatewayTool(event.toolName);
|
|
296
357
|
this.bindings.set(event.toolCallId, {
|
|
297
|
-
toolClass
|
|
358
|
+
toolClass,
|
|
298
359
|
inputFingerprint: fingerprint,
|
|
299
|
-
|
|
300
|
-
|
|
360
|
+
readSourceFingerprint: readFingerprint.source,
|
|
361
|
+
...(toolClass === "shell"
|
|
362
|
+
? { shellCommandScope: classifyShellCommand(event.input).scope }
|
|
301
363
|
: {}),
|
|
302
364
|
});
|
|
303
365
|
}
|
|
304
366
|
|
|
305
|
-
recordToolResult(
|
|
367
|
+
recordToolResult(
|
|
368
|
+
event: ToolResultLike,
|
|
369
|
+
budget: number | ContextGatewayBudgets,
|
|
370
|
+
options: { delivery?: { representation: "passthrough" | "test-build-compact"; contentBytes: number; textBytes: number }; maxInlineBytes?: number } = {},
|
|
371
|
+
): ContextGatewayObservation {
|
|
306
372
|
const binding = typeof event.toolCallId === "string" ? this.bindings.get(event.toolCallId) : undefined;
|
|
307
|
-
const observation = observeToolResult(event,
|
|
373
|
+
const observation = observeToolResult(event, budget, {
|
|
308
374
|
...(binding ? { toolClass: binding.toolClass } : {}),
|
|
309
375
|
...(binding?.shellCommandScope ? { shellCommandScope: binding.shellCommandScope } : {}),
|
|
376
|
+
...options,
|
|
310
377
|
});
|
|
311
378
|
if (typeof event.toolCallId !== "string" || !binding) {
|
|
312
379
|
this.unboundResults += 1;
|
|
@@ -357,8 +424,8 @@ export class ContextGatewayTelemetry {
|
|
|
357
424
|
}
|
|
358
425
|
|
|
359
426
|
/** Backward-compatible convenience for observation-only callers without a tool_call event. */
|
|
360
|
-
record(event: ToolResultLike,
|
|
361
|
-
const observation = observeToolResult(event,
|
|
427
|
+
record(event: ToolResultLike, budget: number | ContextGatewayBudgets): ContextGatewayObservation {
|
|
428
|
+
const observation = observeToolResult(event, budget);
|
|
362
429
|
this.aggregateObservation(event, observation);
|
|
363
430
|
return observation;
|
|
364
431
|
}
|
|
@@ -368,8 +435,10 @@ export class ContextGatewayTelemetry {
|
|
|
368
435
|
this.byClass = {};
|
|
369
436
|
this.bindings.clear();
|
|
370
437
|
this.seenInputFingerprints.clear();
|
|
438
|
+
this.seenReadSourceFingerprints.clear();
|
|
371
439
|
this.unboundResults = 0;
|
|
372
440
|
this.repeatCandidateCount = 0;
|
|
441
|
+
this.sameSourceDifferentRangeCount = 0;
|
|
373
442
|
this.retrievalCalls = 0;
|
|
374
443
|
this.nativePolicy = { results: 0, refusals: 0, fullOverrides: 0, byReason: {} };
|
|
375
444
|
this.testOutput = emptyTestOutputTelemetry();
|
|
@@ -383,6 +452,7 @@ export class ContextGatewayTelemetry {
|
|
|
383
452
|
pendingCalls: this.bindings.size,
|
|
384
453
|
unboundResults: this.unboundResults,
|
|
385
454
|
repeatCandidateCount: this.repeatCandidateCount,
|
|
455
|
+
sameSourceDifferentRangeCount: this.sameSourceDifferentRangeCount,
|
|
386
456
|
retrievalCalls: this.retrievalCalls,
|
|
387
457
|
nativePolicy: {
|
|
388
458
|
...this.nativePolicy,
|
|
@@ -404,12 +474,15 @@ export class ContextGatewayTelemetry {
|
|
|
404
474
|
target.results += 1;
|
|
405
475
|
if (observation.outcome === "error") target.errors += 1;
|
|
406
476
|
target.contentBytes += observation.source.contentBytes;
|
|
477
|
+
target.deliveredContentBytes += observation.delivery.contentBytes;
|
|
407
478
|
target.textBytes += observation.source.textBytes;
|
|
408
479
|
target.imageBytes += observation.source.imageBytes;
|
|
409
480
|
target.detailsBytes += observation.source.detailsBytes;
|
|
410
481
|
if (observation.source.completeness === "upstream-truncated") target.upstreamTruncatedResults += 1;
|
|
411
482
|
if (observation.overBudget) target.overBudgetResults += 1;
|
|
412
483
|
target.potentialBytesOverBudget += observation.potentialBytesOverBudget;
|
|
484
|
+
if (observation.delivery.representation !== "passthrough") target.enforcedResults += 1;
|
|
485
|
+
target.actualBytesSaved += observation.actualBytesSaved;
|
|
413
486
|
}
|
|
414
487
|
}
|
|
415
488
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ContextGatewayMode = "off" | "observe" | "enforce";
|
|
2
2
|
|
|
3
|
-
export type ContextGatewayEffectiveMode =
|
|
3
|
+
export type ContextGatewayEffectiveMode = ContextGatewayMode;
|
|
4
4
|
|
|
5
5
|
export interface ContextGatewayBudgets {
|
|
6
6
|
maxInlineBytes: number;
|
|
@@ -10,7 +10,7 @@ export interface ContextGatewayBudgets {
|
|
|
10
10
|
maxSearchMatches: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** Parsed layered config. `enforce`
|
|
13
|
+
/** Parsed layered config. `enforce` is selective and fail-open for unsupported result classes. */
|
|
14
14
|
export interface ContextGatewayResolvedConfig {
|
|
15
15
|
mode: ContextGatewayMode;
|
|
16
16
|
budgets: ContextGatewayBudgets;
|
|
@@ -48,7 +48,7 @@ export interface ContextGatewayObservedView {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface ContextGatewayObservedDelivery {
|
|
51
|
-
representation: "passthrough";
|
|
51
|
+
representation: "passthrough" | "test-build-compact";
|
|
52
52
|
contentBytes: number;
|
|
53
53
|
textBytes: number;
|
|
54
54
|
}
|
|
@@ -81,11 +81,13 @@ export interface ContextGatewayObservation {
|
|
|
81
81
|
version: 1;
|
|
82
82
|
toolClass: ContextGatewayToolClass;
|
|
83
83
|
outcome: ContextGatewayExecutionOutcome;
|
|
84
|
+
budgetBytes: number;
|
|
84
85
|
source: ContextGatewayObservedSource;
|
|
85
86
|
view: ContextGatewayObservedView;
|
|
86
87
|
delivery: ContextGatewayObservedDelivery;
|
|
87
88
|
overBudget: boolean;
|
|
88
89
|
potentialBytesOverBudget: number;
|
|
90
|
+
actualBytesSaved: number;
|
|
89
91
|
testOutput?: ContextGatewayTestOutputObservation;
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -93,12 +95,15 @@ export interface ContextGatewayClassTelemetry {
|
|
|
93
95
|
results: number;
|
|
94
96
|
errors: number;
|
|
95
97
|
contentBytes: number;
|
|
98
|
+
deliveredContentBytes: number;
|
|
96
99
|
textBytes: number;
|
|
97
100
|
imageBytes: number;
|
|
98
101
|
detailsBytes: number;
|
|
99
102
|
upstreamTruncatedResults: number;
|
|
100
103
|
overBudgetResults: number;
|
|
101
104
|
potentialBytesOverBudget: number;
|
|
105
|
+
enforcedResults: number;
|
|
106
|
+
actualBytesSaved: number;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export type ContextGatewayNativePolicyReason =
|
|
@@ -134,6 +139,7 @@ export interface ContextGatewayTelemetrySnapshot extends ContextGatewayClassTele
|
|
|
134
139
|
pendingCalls: number;
|
|
135
140
|
unboundResults: number;
|
|
136
141
|
repeatCandidateCount: number;
|
|
142
|
+
sameSourceDifferentRangeCount: number;
|
|
137
143
|
retrievalCalls: number;
|
|
138
144
|
nativePolicy: ContextGatewayNativePolicyTelemetry;
|
|
139
145
|
testOutput: ContextGatewayTestOutputTelemetry;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
currentModelRef,
|
|
4
|
+
filterSubagentConfigForParentModel,
|
|
5
|
+
loadSubagentConfig,
|
|
6
|
+
} from "./async-subagents/lib.js";
|
|
7
|
+
import { publishRpcSessionState } from "./lib/rpc-session-state.js";
|
|
8
|
+
|
|
9
|
+
export const CONTEXT_INVENTORY_EVENT = "pi-tools-suite:context-inventory";
|
|
10
|
+
|
|
11
|
+
export interface ContextInventoryState {
|
|
12
|
+
version: 1;
|
|
13
|
+
reason?: "startup" | "reload" | "new" | "resume" | "fork" | "model_select";
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
sessionFile?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
thinking?: string;
|
|
18
|
+
tools: string[];
|
|
19
|
+
skills: string[];
|
|
20
|
+
agents?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createContextInventoryState(
|
|
24
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "getCommands">,
|
|
25
|
+
ctx: Pick<ExtensionContext, "cwd" | "model" | "sessionManager">,
|
|
26
|
+
reason?: ContextInventoryState["reason"],
|
|
27
|
+
): ContextInventoryState {
|
|
28
|
+
const tools = unique(pi.getActiveTools());
|
|
29
|
+
const skillsReadable = tools.includes("read") || tools.includes("bash");
|
|
30
|
+
const skills = skillsReadable
|
|
31
|
+
? unique(pi.getCommands()
|
|
32
|
+
.filter((command) => command.source === "skill")
|
|
33
|
+
.map((command) => command.name.replace(/^skill:/u, "")))
|
|
34
|
+
.sort(compareText)
|
|
35
|
+
: [];
|
|
36
|
+
const model = currentModelRef(ctx.model);
|
|
37
|
+
const thinking = typeof (ctx as { thinkingLevel?: unknown }).thinkingLevel === "string"
|
|
38
|
+
? (ctx as { thinkingLevel?: string }).thinkingLevel?.trim() || undefined
|
|
39
|
+
: undefined;
|
|
40
|
+
const agents = tools.includes("subagents") ? effectiveAgentTypes(ctx.cwd, model) : [];
|
|
41
|
+
const sessionId = safeSessionId(ctx.sessionManager);
|
|
42
|
+
const sessionFile = safeSessionFile(ctx.sessionManager);
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
...(reason ? { reason } : {}),
|
|
46
|
+
...(sessionId ? { sessionId } : {}),
|
|
47
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
48
|
+
...(model ? { model } : {}),
|
|
49
|
+
...(thinking ? { thinking } : {}),
|
|
50
|
+
tools,
|
|
51
|
+
skills,
|
|
52
|
+
...(agents === undefined ? {} : { agents }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function publishContextInventoryState(
|
|
57
|
+
pi: ExtensionAPI,
|
|
58
|
+
ctx: ExtensionContext,
|
|
59
|
+
reason?: ContextInventoryState["reason"],
|
|
60
|
+
): void {
|
|
61
|
+
const state = createContextInventoryState(pi, ctx, reason);
|
|
62
|
+
pi.events?.emit?.(CONTEXT_INVENTORY_EVENT, state);
|
|
63
|
+
publishRpcSessionState(ctx, CONTEXT_INVENTORY_EVENT, state);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function effectiveAgentTypes(cwd: string, model: string | undefined): string[] | undefined {
|
|
67
|
+
try {
|
|
68
|
+
const config = filterSubagentConfigForParentModel(loadSubagentConfig(cwd), model);
|
|
69
|
+
return Object.keys(config.types).sort(compareText);
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function safeSessionId(sessionManager: ExtensionContext["sessionManager"]): string | undefined {
|
|
76
|
+
try {
|
|
77
|
+
const sessionId = sessionManager.getSessionId();
|
|
78
|
+
return sessionId?.trim() || undefined;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function safeSessionFile(sessionManager: ExtensionContext["sessionManager"]): string | undefined {
|
|
85
|
+
try {
|
|
86
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
87
|
+
return sessionFile?.trim() || undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function unique(values: readonly string[]): string[] {
|
|
94
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compareText(left: string, right: string): number {
|
|
98
|
+
return left.localeCompare(right);
|
|
99
|
+
}
|