@piagent/platform 1.2.13 → 1.2.14
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 +26 -0
- package/README.md +11 -3
- package/README.vi.md +90 -0
- package/architecture/layers.json +117 -0
- package/docs/README.md +23 -0
- package/docs/architecture.md +7 -61
- package/docs/command-reference-vietnamese.md +6 -6
- package/docs/distribution-standard.md +7 -7
- package/docs/en/README.md +13 -0
- package/docs/en/architecture.md +81 -0
- package/docs/en/maintainer-guide.md +121 -0
- package/docs/languages.json +40 -0
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/package-architecture-notes.md +9 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/README.md +13 -0
- package/docs/vi/architecture.md +81 -0
- package/docs/vi/maintainer-guide.md +121 -0
- package/docs-site/architecture.html +38 -14
- package/docs-site/benchmark.html +3 -1
- package/docs-site/commands.html +3 -1
- package/docs-site/content/architecture.html +35 -13
- package/docs-site/context-engine.html +3 -1
- package/docs-site/index.html +3 -1
- package/docs-site/mcp-approval.html +3 -1
- package/docs-site/mcp-auth.html +3 -1
- package/docs-site/mcp-servers.html +3 -1
- package/docs-site/mcp.html +3 -1
- package/docs-site/permissions.html +3 -1
- package/docs-site/profiles.html +3 -1
- package/docs-site/quickstart.html +3 -1
- package/docs-site/runtime.html +3 -1
- package/docs-site/scope.html +3 -1
- package/docs-site/security.html +3 -1
- package/docs-site/team.html +3 -1
- package/docs-site/workflows.html +3 -1
- package/package.json +5 -1
- package/packages/piagent-core/README.md +6 -2
- package/packages/piagent-core/capabilities/capability-core.js +3 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +18 -0
- package/packages/piagent-core/extensions/guard-types.ts +0 -37
- package/packages/piagent-core/extensions/piagent-guard.ts +170 -2099
- package/packages/piagent-core/extensions/task-state.js +22 -3
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +213 -0
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +203 -0
- package/packages/piagent-core/runtime/hooks/input-hook.ts +122 -0
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +104 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +140 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +316 -0
- package/packages/piagent-core/runtime/input/chat-images.ts +299 -0
- package/packages/piagent-core/runtime/runtime-limits.ts +21 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +64 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +175 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +52 -0
- package/packages/piagent-core/runtime/session/tool-result-compaction.ts +352 -0
- package/packages/piagent-core/runtime/session/usage.ts +178 -0
- package/packages/piagent-core/runtime/tools/tool-groups.ts +86 -0
- package/packages/piagent-core/runtime/workflows/input-routing.ts +113 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +73 -0
- package/scripts/build-docs-site.mjs +4 -1
- package/scripts/check-architecture.mjs +132 -0
- package/scripts/check-doc-languages.mjs +67 -0
- package/scripts/verify-local.sh +31 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { redactForStorage, redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
8
|
+
import { appendJsonlBounded, pruneCaptureFiles, readJsonlTail } from "../../extensions/state-retention.js";
|
|
9
|
+
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "../../extensions/local-state-path.js";
|
|
10
|
+
import {
|
|
11
|
+
CAPTURE_INDEX_MAX_BYTES,
|
|
12
|
+
CAPTURE_RETENTION_MAX_AGE_MS,
|
|
13
|
+
CAPTURE_RETENTION_MAX_BYTES,
|
|
14
|
+
CAPTURE_RETENTION_MAX_FILES,
|
|
15
|
+
TOOL_RESULT_CAPTURE_MAX_CHARS,
|
|
16
|
+
TOOL_RESULT_COMPACT_CHAR_THRESHOLD,
|
|
17
|
+
TOOL_RESULT_COMPACT_LINE_THRESHOLD,
|
|
18
|
+
TOOL_RESULT_PREVIEW_HEAD_LINES,
|
|
19
|
+
TOOL_RESULT_PREVIEW_INTERESTING_LINES,
|
|
20
|
+
TOOL_RESULT_PREVIEW_MAX_CHARS,
|
|
21
|
+
TOOL_RESULT_PREVIEW_TAIL_LINES
|
|
22
|
+
} from "../runtime-limits.ts";
|
|
23
|
+
import { formatCount } from "./usage.ts";
|
|
24
|
+
|
|
25
|
+
export type ToolResultCaptureSummary = {
|
|
26
|
+
path?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
source: string;
|
|
29
|
+
toolName: string;
|
|
30
|
+
originalChars: number;
|
|
31
|
+
originalLines: number;
|
|
32
|
+
previewChars?: number;
|
|
33
|
+
storedChars?: number;
|
|
34
|
+
storedTruncated?: boolean;
|
|
35
|
+
sha256: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const captureWritesSincePrune = new Map<string, number>();
|
|
39
|
+
|
|
40
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function slugify(input: string): string {
|
|
45
|
+
return input
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
49
|
+
.replace(/^-+|-+$/g, "")
|
|
50
|
+
.slice(0, 80) || "tool";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nowIso(): string {
|
|
54
|
+
return new Date().toISOString();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function redactText(input: string): string {
|
|
58
|
+
return redactSensitiveText(input).text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function toolResultCaptureRoot(cwd: string): string {
|
|
62
|
+
return path.join(cwd, ".pi", "piagent-state", "tool-results");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toolResultCaptureIndexPath(cwd: string): string {
|
|
66
|
+
return path.join(toolResultCaptureRoot(cwd), "index.jsonl");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeToolResultText(text: string): string {
|
|
70
|
+
return text.replace(/\r\n/g, "\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toolResultLineCount(text: string): number {
|
|
74
|
+
if (!text) return 0;
|
|
75
|
+
return normalizeToolResultText(text).split("\n").length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function shouldCompactToolResultText(text: string): boolean {
|
|
79
|
+
return text.length > TOOL_RESULT_COMPACT_CHAR_THRESHOLD
|
|
80
|
+
|| toolResultLineCount(text) > TOOL_RESULT_COMPACT_LINE_THRESHOLD;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clipPreviewLine(line: string, maxChars = 260): string {
|
|
84
|
+
if (line.length <= maxChars) return line;
|
|
85
|
+
return `${line.slice(0, maxChars).trimEnd()} ... [line clipped]`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function boundedPreview(text: string): string {
|
|
89
|
+
if (text.length <= TOOL_RESULT_PREVIEW_MAX_CHARS) return text;
|
|
90
|
+
const marker = "\n[Piagent preview shortened further to stay light.]\n";
|
|
91
|
+
const edge = Math.floor((TOOL_RESULT_PREVIEW_MAX_CHARS - marker.length) / 2);
|
|
92
|
+
return `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function interestingToolResultLines(lines: string[]): string[] {
|
|
96
|
+
const interesting = /\b(?:error|errors|err!|failed?|failure|warning|warn|exception|traceback|assert(?:ion)?|panic|fatal|timeout|timed out|denied|not found|cannot|eacces|enoent|ts\d{3,5}|err_[a-z0-9_]+)\b/i;
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
const result: string[] = [];
|
|
99
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
100
|
+
const line = lines[index];
|
|
101
|
+
if (!interesting.test(line)) continue;
|
|
102
|
+
const rendered = `${index + 1}: ${clipPreviewLine(line)}`;
|
|
103
|
+
if (seen.has(rendered)) continue;
|
|
104
|
+
seen.add(rendered);
|
|
105
|
+
result.push(rendered);
|
|
106
|
+
if (result.length >= TOOL_RESULT_PREVIEW_INTERESTING_LINES) break;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function storedToolResultText(text: string): { text: string; truncated: boolean } {
|
|
112
|
+
if (text.length <= TOOL_RESULT_CAPTURE_MAX_CHARS) return { text, truncated: false };
|
|
113
|
+
const marker = `\n\n[Piagent capture truncated: omitted ${formatCount(text.length - TOOL_RESULT_CAPTURE_MAX_CHARS)} chars from the middle.]\n\n`;
|
|
114
|
+
const edge = Math.floor((TOOL_RESULT_CAPTURE_MAX_CHARS - marker.length) / 2);
|
|
115
|
+
return {
|
|
116
|
+
text: `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`,
|
|
117
|
+
truncated: true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function compactToolResultPreview(toolName: string, source: string, text: string, capture: ToolResultCaptureSummary): string {
|
|
122
|
+
const normalized = normalizeToolResultText(text);
|
|
123
|
+
const lines = normalized.split("\n");
|
|
124
|
+
const head = lines.slice(0, TOOL_RESULT_PREVIEW_HEAD_LINES).map((line) => clipPreviewLine(line));
|
|
125
|
+
const tailStart = Math.max(TOOL_RESULT_PREVIEW_HEAD_LINES, lines.length - TOOL_RESULT_PREVIEW_TAIL_LINES);
|
|
126
|
+
const tail = lines.slice(tailStart).map((line) => clipPreviewLine(line));
|
|
127
|
+
const omitted = Math.max(0, lines.length - head.length - tail.length);
|
|
128
|
+
const interesting = interestingToolResultLines(lines);
|
|
129
|
+
const captureLine = capture.path
|
|
130
|
+
? `capture: ${capture.path}${capture.storedTruncated ? " (stored head/tail sample; original too large)" : ""}`
|
|
131
|
+
: `capture: unavailable (${capture.error ?? "write failed"})`;
|
|
132
|
+
const sections = [
|
|
133
|
+
`[Piagent compacted large ${toolName} output from ${source}: ${formatCount(capture.originalChars)} chars / ${formatCount(capture.originalLines)} lines.]`,
|
|
134
|
+
`[${captureLine}]`,
|
|
135
|
+
"[Preview keeps head, notable lines, and tail.]",
|
|
136
|
+
"",
|
|
137
|
+
"head:",
|
|
138
|
+
...head
|
|
139
|
+
];
|
|
140
|
+
if (interesting.length > 0) sections.push("", "notable:", ...interesting);
|
|
141
|
+
if (omitted > 0) sections.push("", `... ${formatCount(omitted)} middle line${omitted === 1 ? "" : "s"} omitted ...`);
|
|
142
|
+
sections.push("", "tail:", ...tail);
|
|
143
|
+
return boundedPreview(sections.join("\n"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function maybeNumber(value: unknown): number | undefined {
|
|
147
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
148
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number.parseInt(value, 10);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function currentSessionField(ctx: ExtensionContext, field: "getSessionFile" | "getSessionId" | "getSessionName"): string | undefined {
|
|
153
|
+
try {
|
|
154
|
+
const value = ctx.sessionManager[field]?.();
|
|
155
|
+
return value === undefined || value === null ? undefined : redactText(String(value));
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function writeToolResultCapture(
|
|
162
|
+
cwd: string,
|
|
163
|
+
event: any,
|
|
164
|
+
ctx: ExtensionContext,
|
|
165
|
+
source: string,
|
|
166
|
+
text: string,
|
|
167
|
+
cache: Map<string, ToolResultCaptureSummary>
|
|
168
|
+
): ToolResultCaptureSummary {
|
|
169
|
+
const normalized = normalizeToolResultText(text);
|
|
170
|
+
const sha256 = crypto.createHash("sha256").update(normalized).digest("hex");
|
|
171
|
+
const originalLines = toolResultLineCount(normalized);
|
|
172
|
+
const cached = cache.get(sha256);
|
|
173
|
+
if (cached?.path) return { ...cached, source };
|
|
174
|
+
|
|
175
|
+
const toolName = String(event?.toolName ?? "tool");
|
|
176
|
+
const recordedAt = nowIso();
|
|
177
|
+
const date = recordedAt.slice(0, 10);
|
|
178
|
+
const filename = `${recordedAt.replace(/[:.]/g, "-")}-${slugify(toolName)}-${sha256.slice(0, 12)}.log`;
|
|
179
|
+
const relativePath = [".pi", "piagent-state", "tool-results", date, filename].join("/");
|
|
180
|
+
const absolutePath = path.join(cwd, ".pi", "piagent-state", "tool-results", date, filename);
|
|
181
|
+
const capture = storedToolResultText(normalized);
|
|
182
|
+
const details = event?.details && typeof event.details === "object" ? event.details : {};
|
|
183
|
+
const exitCode = maybeNumber(details.exitCode ?? details.status ?? event?.exitCode);
|
|
184
|
+
const metadata = {
|
|
185
|
+
schemaVersion: 1,
|
|
186
|
+
recordedAt,
|
|
187
|
+
cwd: redactText(cwd),
|
|
188
|
+
sessionId: currentSessionField(ctx, "getSessionId"),
|
|
189
|
+
sessionName: currentSessionField(ctx, "getSessionName"),
|
|
190
|
+
sessionFile: currentSessionField(ctx, "getSessionFile"),
|
|
191
|
+
toolName,
|
|
192
|
+
source,
|
|
193
|
+
isError: event?.isError === true,
|
|
194
|
+
exitCode,
|
|
195
|
+
input: redactForStorage(event?.input),
|
|
196
|
+
path: relativePath,
|
|
197
|
+
originalChars: normalized.length,
|
|
198
|
+
originalLines,
|
|
199
|
+
storedChars: capture.text.length,
|
|
200
|
+
storedTruncated: capture.truncated,
|
|
201
|
+
sha256
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
ensurePrivateStateDirectory(cwd, path.dirname(absolutePath), "Tool-result capture directory");
|
|
206
|
+
const capturePath = resolveLocalStatePath(cwd, absolutePath, { label: "Tool-result capture" });
|
|
207
|
+
fs.writeFileSync(capturePath, [
|
|
208
|
+
"# Piagent compacted tool result capture",
|
|
209
|
+
JSON.stringify(metadata),
|
|
210
|
+
"---",
|
|
211
|
+
capture.text
|
|
212
|
+
].join("\n"), { mode: 0o600 });
|
|
213
|
+
appendJsonlBounded(toolResultCaptureIndexPath(cwd), metadata, { maxBytes: CAPTURE_INDEX_MAX_BYTES, mode: 0o600, projectRoot: cwd });
|
|
214
|
+
const writes = (captureWritesSincePrune.get(cwd) ?? 0) + 1;
|
|
215
|
+
if (writes >= 25) {
|
|
216
|
+
pruneCaptureFiles(toolResultCaptureRoot(cwd), {
|
|
217
|
+
maxFiles: CAPTURE_RETENTION_MAX_FILES,
|
|
218
|
+
maxBytes: CAPTURE_RETENTION_MAX_BYTES,
|
|
219
|
+
maxAgeMs: CAPTURE_RETENTION_MAX_AGE_MS,
|
|
220
|
+
projectRoot: cwd
|
|
221
|
+
});
|
|
222
|
+
captureWritesSincePrune.set(cwd, 0);
|
|
223
|
+
} else {
|
|
224
|
+
captureWritesSincePrune.set(cwd, writes);
|
|
225
|
+
}
|
|
226
|
+
const summary = {
|
|
227
|
+
path: relativePath,
|
|
228
|
+
source,
|
|
229
|
+
toolName,
|
|
230
|
+
originalChars: normalized.length,
|
|
231
|
+
originalLines,
|
|
232
|
+
storedChars: capture.text.length,
|
|
233
|
+
storedTruncated: capture.truncated,
|
|
234
|
+
sha256
|
|
235
|
+
};
|
|
236
|
+
cache.set(sha256, summary);
|
|
237
|
+
return summary;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const summary = {
|
|
240
|
+
error: error instanceof Error ? error.message : String(error),
|
|
241
|
+
source,
|
|
242
|
+
toolName,
|
|
243
|
+
originalChars: normalized.length,
|
|
244
|
+
originalLines,
|
|
245
|
+
sha256
|
|
246
|
+
};
|
|
247
|
+
cache.set(sha256, summary);
|
|
248
|
+
return summary;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function compactToolResultTextContent(
|
|
253
|
+
cwd: string,
|
|
254
|
+
event: any,
|
|
255
|
+
ctx: ExtensionContext,
|
|
256
|
+
content: unknown,
|
|
257
|
+
cache: Map<string, ToolResultCaptureSummary>
|
|
258
|
+
): { content: unknown; captures: ToolResultCaptureSummary[] } {
|
|
259
|
+
if (!Array.isArray(content)) return { content, captures: [] };
|
|
260
|
+
const captures: ToolResultCaptureSummary[] = [];
|
|
261
|
+
const compacted = content.map((block, index) => {
|
|
262
|
+
if (!block || typeof block !== "object") return block;
|
|
263
|
+
const typed = block as { type?: unknown; text?: unknown };
|
|
264
|
+
if (typed.type !== "text" || typeof typed.text !== "string" || !shouldCompactToolResultText(typed.text)) return block;
|
|
265
|
+
const source = `content[${index}].text`;
|
|
266
|
+
const capture = writeToolResultCapture(cwd, event, ctx, source, typed.text, cache);
|
|
267
|
+
captures.push(capture);
|
|
268
|
+
return {
|
|
269
|
+
...block,
|
|
270
|
+
text: compactToolResultPreview(String(event?.toolName ?? "tool"), source, typed.text, capture)
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
return { content: compacted, captures };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function compactToolResultDetails(
|
|
277
|
+
cwd: string,
|
|
278
|
+
event: any,
|
|
279
|
+
ctx: ExtensionContext,
|
|
280
|
+
value: unknown,
|
|
281
|
+
cache: Map<string, ToolResultCaptureSummary>,
|
|
282
|
+
captures: ToolResultCaptureSummary[],
|
|
283
|
+
source = "details",
|
|
284
|
+
depth = 0
|
|
285
|
+
): unknown {
|
|
286
|
+
if (typeof value === "string") {
|
|
287
|
+
if (!shouldCompactToolResultText(value)) return value;
|
|
288
|
+
const capture = writeToolResultCapture(cwd, event, ctx, source, value, cache);
|
|
289
|
+
captures.push(capture);
|
|
290
|
+
return compactToolResultPreview(String(event?.toolName ?? "tool"), source, value, capture);
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
if (depth >= 6) return value;
|
|
294
|
+
return value.map((item, index) => compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}[${index}]`, depth + 1));
|
|
295
|
+
}
|
|
296
|
+
if (isPlainRecord(value)) {
|
|
297
|
+
if (depth >= 6) return value;
|
|
298
|
+
return Object.fromEntries(
|
|
299
|
+
Object.entries(value).map(([key, item]) => [
|
|
300
|
+
key,
|
|
301
|
+
compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}.${key}`, depth + 1)
|
|
302
|
+
])
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function attachToolResultCompactionDetails(details: unknown, captures: ToolResultCaptureSummary[]): unknown {
|
|
309
|
+
if (captures.length === 0) return details;
|
|
310
|
+
const compacted = captures.map((capture) => ({
|
|
311
|
+
path: capture.path,
|
|
312
|
+
error: capture.error,
|
|
313
|
+
source: capture.source,
|
|
314
|
+
toolName: capture.toolName,
|
|
315
|
+
originalChars: capture.originalChars,
|
|
316
|
+
originalLines: capture.originalLines,
|
|
317
|
+
storedChars: capture.storedChars,
|
|
318
|
+
storedTruncated: capture.storedTruncated,
|
|
319
|
+
sha256: capture.sha256
|
|
320
|
+
}));
|
|
321
|
+
if (isPlainRecord(details)) return { ...details, piagentCompactedToolResults: compacted };
|
|
322
|
+
if (details === undefined) return { piagentCompactedToolResults: compacted };
|
|
323
|
+
return { value: details, piagentCompactedToolResults: compacted };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function readRecentToolResultCaptures(cwd: string, limit = 5): Record<string, unknown>[] {
|
|
327
|
+
const captures: Record<string, unknown>[] = [];
|
|
328
|
+
for (const parsed of readJsonlTail(toolResultCaptureIndexPath(cwd), {
|
|
329
|
+
limit: Math.max(limit * 3, limit),
|
|
330
|
+
maxBytes: CAPTURE_INDEX_MAX_BYTES,
|
|
331
|
+
projectRoot: cwd
|
|
332
|
+
})) {
|
|
333
|
+
if (isPlainRecord(parsed)) captures.push(redactForStorage(parsed) as Record<string, unknown>);
|
|
334
|
+
}
|
|
335
|
+
return captures.slice(-limit);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function formatToolResultCaptureStatus(cwd: string, captures: Record<string, unknown>[]): string {
|
|
339
|
+
const root = path.relative(cwd, toolResultCaptureRoot(cwd)).split(path.sep).join("/") || ".pi/piagent-state/tool-results";
|
|
340
|
+
const lines = [
|
|
341
|
+
`logPolicy: compact above ${formatCount(TOOL_RESULT_COMPACT_CHAR_THRESHOLD)} chars or ${formatCount(TOOL_RESULT_COMPACT_LINE_THRESHOLD)} lines`,
|
|
342
|
+
`captureRoot: ${root}`,
|
|
343
|
+
`recent: ${captures.length || "none"}`
|
|
344
|
+
];
|
|
345
|
+
for (const capture of captures) {
|
|
346
|
+
const originalLines = typeof capture.originalLines === "number" ? formatCount(capture.originalLines) : "unknown";
|
|
347
|
+
const originalChars = typeof capture.originalChars === "number" ? formatCount(capture.originalChars) : "unknown";
|
|
348
|
+
const exit = capture.exitCode === undefined ? "" : ` exit=${capture.exitCode}`;
|
|
349
|
+
lines.push(`- ${capture.recordedAt ?? "unknown"} ${capture.toolName ?? "tool"}${exit}: ${originalLines} lines / ${originalChars} chars -> ${capture.path ?? "no capture path"}`);
|
|
350
|
+
}
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CONTEXT_COMPACT_PERCENT,
|
|
5
|
+
CONTEXT_FRESH_PERCENT,
|
|
6
|
+
CONTEXT_WATCH_PERCENT,
|
|
7
|
+
LONG_INPUT_CHARS
|
|
8
|
+
} from "../runtime-limits.ts";
|
|
9
|
+
import { modelLabel } from "./message-signals.ts";
|
|
10
|
+
|
|
11
|
+
export type UsageSnapshot = {
|
|
12
|
+
sessionFile?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
sessionName?: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
mode: string;
|
|
17
|
+
model: string;
|
|
18
|
+
thinkingLevel: string;
|
|
19
|
+
entries: {
|
|
20
|
+
total: number;
|
|
21
|
+
branch: number;
|
|
22
|
+
};
|
|
23
|
+
contextUsage?: {
|
|
24
|
+
tokens: number | null;
|
|
25
|
+
contextWindow: number;
|
|
26
|
+
percent: number | null;
|
|
27
|
+
};
|
|
28
|
+
exactTotals: {
|
|
29
|
+
availableInCommand: false;
|
|
30
|
+
howToRead: string[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ContextPreflight = {
|
|
35
|
+
workflow: string;
|
|
36
|
+
inputChars: number;
|
|
37
|
+
inputTokenEstimate: number;
|
|
38
|
+
liveContext?: UsageSnapshot["contextUsage"];
|
|
39
|
+
projectedContext?: {
|
|
40
|
+
tokens: number;
|
|
41
|
+
percent: number;
|
|
42
|
+
};
|
|
43
|
+
recommendation: "ok" | "watch" | "compact" | "fresh-session" | "unknown";
|
|
44
|
+
reason: string;
|
|
45
|
+
commands: string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function formatCount(value: number | null | undefined): string {
|
|
49
|
+
if (value === null || value === undefined || Number.isNaN(value)) return "unknown";
|
|
50
|
+
return Math.round(value).toLocaleString("en-US");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatPercent(value: number | null | undefined): string {
|
|
54
|
+
if (value === null || value === undefined || Number.isNaN(value)) return "unknown";
|
|
55
|
+
return `${value.toFixed(1)}%`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildUsageSnapshot(ctx: ExtensionContext, thinkingLevel?: string): UsageSnapshot {
|
|
59
|
+
const contextUsage = ctx.getContextUsage();
|
|
60
|
+
const contextWithThinking = ctx as ExtensionContext & { getThinkingLevel?: () => string };
|
|
61
|
+
return {
|
|
62
|
+
sessionFile: ctx.sessionManager.getSessionFile(),
|
|
63
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
64
|
+
sessionName: ctx.sessionManager.getSessionName(),
|
|
65
|
+
cwd: ctx.cwd,
|
|
66
|
+
mode: ctx.mode,
|
|
67
|
+
model: modelLabel(ctx),
|
|
68
|
+
thinkingLevel: thinkingLevel ?? contextWithThinking.getThinkingLevel?.() ?? "unknown",
|
|
69
|
+
entries: {
|
|
70
|
+
total: ctx.sessionManager.getEntries().length,
|
|
71
|
+
branch: ctx.sessionManager.getBranch().length
|
|
72
|
+
},
|
|
73
|
+
contextUsage: contextUsage
|
|
74
|
+
? {
|
|
75
|
+
tokens: contextUsage.tokens,
|
|
76
|
+
contextWindow: contextUsage.contextWindow,
|
|
77
|
+
percent: contextUsage.percent
|
|
78
|
+
}
|
|
79
|
+
: undefined,
|
|
80
|
+
exactTotals: {
|
|
81
|
+
availableInCommand: false,
|
|
82
|
+
howToRead: [
|
|
83
|
+
"Inside Pi TUI: run /session for exact tokens and cost.",
|
|
84
|
+
"Outside Pi: run piagent-usage /path/to/project or scripts/pi-session-stats.sh /path/to/project.",
|
|
85
|
+
"Historical totals: run piagent-usage --history /path/to/project --days 7."
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatUsageSnapshot(snapshot: UsageSnapshot): string {
|
|
92
|
+
const context = snapshot.contextUsage
|
|
93
|
+
? `${formatCount(snapshot.contextUsage.tokens)} / ${formatCount(snapshot.contextUsage.contextWindow)} tokens (${formatPercent(snapshot.contextUsage.percent)})`
|
|
94
|
+
: "unavailable";
|
|
95
|
+
return [
|
|
96
|
+
`usage: ${context}`,
|
|
97
|
+
`session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
|
|
98
|
+
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
99
|
+
`entries: ${formatCount(snapshot.entries.branch)} active / ${formatCount(snapshot.entries.total)} total`,
|
|
100
|
+
`file: ${snapshot.sessionFile ?? "not persisted"}`,
|
|
101
|
+
"exact: /session | piagent-usage /path/to/project",
|
|
102
|
+
"history: piagent-usage --history /path/to/project --days 7"
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function estimateTokensFromChars(chars: number): number {
|
|
107
|
+
return Math.max(0, Math.ceil(chars / 4));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildContextPreflight(snapshot: UsageSnapshot, workflow = "task", inputChars = 0): ContextPreflight {
|
|
111
|
+
const inputTokenEstimate = estimateTokensFromChars(inputChars);
|
|
112
|
+
const live = snapshot.contextUsage;
|
|
113
|
+
let projectedContext: ContextPreflight["projectedContext"];
|
|
114
|
+
let recommendation: ContextPreflight["recommendation"] = "unknown";
|
|
115
|
+
let reason = "Context usage is unavailable; use /session or /usage if the task is large.";
|
|
116
|
+
|
|
117
|
+
if (live && live.tokens !== null && live.percent !== null) {
|
|
118
|
+
const projectedTokens = live.tokens + inputTokenEstimate;
|
|
119
|
+
const projectedPercent = live.contextWindow > 0 ? (projectedTokens / live.contextWindow) * 100 : live.percent;
|
|
120
|
+
projectedContext = {
|
|
121
|
+
tokens: projectedTokens,
|
|
122
|
+
percent: projectedPercent
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (live.percent >= CONTEXT_FRESH_PERCENT || projectedPercent >= CONTEXT_FRESH_PERCENT || inputChars >= LONG_INPUT_CHARS) {
|
|
126
|
+
recommendation = "fresh-session";
|
|
127
|
+
reason = "Use a fresh governed session before this task to avoid provider context overflow and stale task state.";
|
|
128
|
+
} else if (live.percent >= CONTEXT_COMPACT_PERCENT || projectedPercent >= CONTEXT_COMPACT_PERCENT) {
|
|
129
|
+
recommendation = "compact";
|
|
130
|
+
reason = "Compact before continuing; the current session is close to the high-context zone.";
|
|
131
|
+
} else if (live.percent >= CONTEXT_WATCH_PERCENT || projectedPercent >= CONTEXT_WATCH_PERCENT) {
|
|
132
|
+
recommendation = "watch";
|
|
133
|
+
reason = "Proceed, but keep context targeted and avoid broad file injection.";
|
|
134
|
+
} else {
|
|
135
|
+
recommendation = "ok";
|
|
136
|
+
reason = "Context is within the normal range for a bounded task.";
|
|
137
|
+
}
|
|
138
|
+
} else if (inputChars >= LONG_INPUT_CHARS) {
|
|
139
|
+
recommendation = "fresh-session";
|
|
140
|
+
reason = "The incoming request is large; start a fresh governed session and keep the full intake in a file.";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
workflow,
|
|
145
|
+
inputChars,
|
|
146
|
+
inputTokenEstimate,
|
|
147
|
+
liveContext: live,
|
|
148
|
+
projectedContext,
|
|
149
|
+
recommendation,
|
|
150
|
+
reason,
|
|
151
|
+
commands: [
|
|
152
|
+
"/task-preflight",
|
|
153
|
+
"/task-preflight compact",
|
|
154
|
+
`/fresh ${workflow === "be-to-fe" ? "be-to-fe" : workflow === "scout" ? "scout" : "task"} <request>`,
|
|
155
|
+
"/usage",
|
|
156
|
+
"/session"
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function formatContextPreflight(preflight: ContextPreflight, snapshot: UsageSnapshot): string {
|
|
162
|
+
const live = preflight.liveContext
|
|
163
|
+
? `${formatCount(preflight.liveContext.tokens)} / ${formatCount(preflight.liveContext.contextWindow)} (${formatPercent(preflight.liveContext.percent)})`
|
|
164
|
+
: "unavailable";
|
|
165
|
+
const projected = preflight.projectedContext
|
|
166
|
+
? `${formatCount(preflight.projectedContext.tokens)} (${formatPercent(preflight.projectedContext.percent)})`
|
|
167
|
+
: "unavailable";
|
|
168
|
+
return [
|
|
169
|
+
`preflight: ${preflight.recommendation}`,
|
|
170
|
+
`workflow: ${preflight.workflow}`,
|
|
171
|
+
`session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
|
|
172
|
+
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
173
|
+
`context: ${live}; projected: ${projected}`,
|
|
174
|
+
`input: ~${formatCount(preflight.inputTokenEstimate)} tokens from ${formatCount(preflight.inputChars)} chars`,
|
|
175
|
+
`reason: ${preflight.reason}`,
|
|
176
|
+
`next: ${preflight.commands.join(" | ")}`
|
|
177
|
+
].join("\n");
|
|
178
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { classifyContextTask } from "../../extensions/context-engine.js";
|
|
2
|
+
import { runtimeLifecycleMode } from "../../extensions/task-lifecycle.js";
|
|
3
|
+
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
4
|
+
|
|
5
|
+
export const PIAGENT_TOOL_GROUPS = {
|
|
6
|
+
loader: ["piagent_tools"],
|
|
7
|
+
intake: ["piagent_task_start"],
|
|
8
|
+
governance: [
|
|
9
|
+
"piagent_context",
|
|
10
|
+
"piagent_context_preflight"
|
|
11
|
+
],
|
|
12
|
+
task: ["piagent_task_progress"],
|
|
13
|
+
recovery: [
|
|
14
|
+
"piagent_context_record",
|
|
15
|
+
"piagent_verify_record",
|
|
16
|
+
"piagent_trace_record",
|
|
17
|
+
"piagent_task_gate_check"
|
|
18
|
+
],
|
|
19
|
+
policy: [
|
|
20
|
+
"piagent_permission_status",
|
|
21
|
+
"piagent_exec_policy_check",
|
|
22
|
+
"piagent_tool_policy_check"
|
|
23
|
+
],
|
|
24
|
+
retrieval: [
|
|
25
|
+
"piagent_context_engine",
|
|
26
|
+
"piagent_context_budget",
|
|
27
|
+
"piagent_context_index_status",
|
|
28
|
+
"piagent_context_index_search"
|
|
29
|
+
],
|
|
30
|
+
knowledge: [
|
|
31
|
+
"piagent_memory_status",
|
|
32
|
+
"piagent_memory_search",
|
|
33
|
+
"piagent_memory_citation_record",
|
|
34
|
+
"piagent_document_read",
|
|
35
|
+
"piagent_source_checkout",
|
|
36
|
+
"piagent_orchestration_policy"
|
|
37
|
+
],
|
|
38
|
+
onboarding: [
|
|
39
|
+
"piagent_profile_options",
|
|
40
|
+
"piagent_profile_apply",
|
|
41
|
+
"piagent_profile_tech_options",
|
|
42
|
+
"piagent_profile_tech_apply",
|
|
43
|
+
"piagent_profile_tech_context_record",
|
|
44
|
+
"piagent_project_onboarding_record",
|
|
45
|
+
"piagent_context_index_record",
|
|
46
|
+
"piagent_memory_note"
|
|
47
|
+
],
|
|
48
|
+
usage: ["piagent_usage_snapshot"]
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type PiagentToolGroup = keyof typeof PIAGENT_TOOL_GROUPS;
|
|
52
|
+
|
|
53
|
+
export const PIAGENT_TOOL_ORDER = Object.values(PIAGENT_TOOL_GROUPS).flat();
|
|
54
|
+
export const PIAGENT_TOOL_NAMES = new Set<string>(PIAGENT_TOOL_ORDER);
|
|
55
|
+
|
|
56
|
+
export function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
|
|
57
|
+
const signal = classifyContextTask(prompt);
|
|
58
|
+
const lower = prompt.toLowerCase();
|
|
59
|
+
const groups = new Set<PiagentToolGroup>();
|
|
60
|
+
|
|
61
|
+
if (signal.workflow === "usage") return ["usage"];
|
|
62
|
+
if (signal.workflow === "onboard") {
|
|
63
|
+
return ["governance", "policy", "retrieval", "knowledge", "onboarding"];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
groups.add("intake");
|
|
67
|
+
if (signal.lane !== "tiny") groups.add("task");
|
|
68
|
+
if (/\b(context (?:engine|index|search|diagnostic)|source checkout|vendor checkout)\b/.test(lower)) {
|
|
69
|
+
groups.add("retrieval");
|
|
70
|
+
}
|
|
71
|
+
if (/\b(document intake|project memory|orchestration policy|source checkout|subagent policy|vendor checkout)\b/.test(lower)) {
|
|
72
|
+
groups.add("knowledge");
|
|
73
|
+
}
|
|
74
|
+
if (/\b(permission|capability|exec policy|tool policy)\b/.test(lower)) groups.add("policy");
|
|
75
|
+
if (/\b(profile|tech stack|onboard|project context)\b/.test(lower)) groups.add("onboarding");
|
|
76
|
+
if (/\b(token|usage|cost)\b/.test(lower)) groups.add("usage");
|
|
77
|
+
if (/\b(?:piagent tool loader|load piagent tools?|piagent tool groups?)\b/.test(lower)) groups.add("loader");
|
|
78
|
+
return [...groups];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function activeTaskToolGroups(task: TaskContract): PiagentToolGroup[] {
|
|
82
|
+
const mode = runtimeLifecycleMode(task);
|
|
83
|
+
if (mode.startsWith("automatic")) return [];
|
|
84
|
+
if (mode.startsWith("assisted")) return ["task"];
|
|
85
|
+
return ["task", "recovery"];
|
|
86
|
+
}
|