@piagent/platform 1.8.0 → 1.9.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/AGENTS.md +7 -0
- package/CHANGELOG.md +97 -1
- package/README.md +45 -70
- package/README.vi.md +34 -61
- package/architecture/layers.json +16 -3
- package/catalog/capabilities.json +14 -7
- package/catalog/claude-model-additions.json +42 -0
- package/docs/auto-delegation-policy.md +2 -0
- package/docs/command-reference-vietnamese.md +68 -90
- package/docs/context-window-policy.md +5 -5
- package/docs/distribution-standard.md +13 -16
- package/docs/en/architecture.md +9 -1
- package/docs/herdr-workflow.md +2 -3
- package/docs/languages.json +4 -4
- package/docs/managed-local-acceptance.md +403 -0
- package/docs/mcp-and-tools.md +5 -7
- package/docs/memory-policy.md +3 -3
- package/docs/model-options.md +2 -2
- package/docs/oauth-providers.md +1 -1
- package/docs/operator-manual-vietnamese.md +82 -127
- package/docs/package-architecture-notes.md +4 -4
- package/docs/piagent-core-improvement-plan.md +3 -5
- package/docs/piagent-core-plan-checkpoint-2026-09-06.json +2 -13
- package/docs/piagent-runtime-benchmark-execution-plan.md +10 -8
- package/docs/project-adapters.md +3 -3
- package/docs/project-onboarding.md +15 -16
- package/docs/publishing-for-teams.md +2 -1
- package/docs/quality-benchmark.md +2 -0
- package/docs/quickstart-vietnamese.md +23 -24
- package/docs/release-install-policy.md +3 -2
- package/docs/research/harness-next-evidence-review.md +7 -7
- package/docs/runtime-harness-standard.md +2 -0
- package/docs/runtime-policy-design.md +2 -0
- package/docs/runtime-quality-baseline.md +2 -0
- package/docs/subagent-orchestration-capabilities.md +1 -1
- package/docs/subagents-and-multiagent.md +10 -8
- package/docs/task-implementation-contract.md +2 -0
- package/docs/task-lifecycle-vietnamese.md +2 -0
- package/docs/team-onboarding.md +28 -59
- package/docs/usage-observability.md +2 -2
- package/docs/vi/architecture.md +9 -1
- package/docs/workflow-recipes.md +6 -10
- package/docs-site/architecture.html +4 -3
- package/docs-site/assets/docs.css +80 -0
- package/docs-site/benchmark.html +7 -1
- package/docs-site/commands.html +37 -23
- package/docs-site/content/architecture.html +2 -2
- package/docs-site/content/benchmark.html +5 -0
- package/docs-site/content/commands.html +35 -22
- package/docs-site/content/ecosystem.html +144 -0
- package/docs-site/content/en/architecture.html +1 -1
- package/docs-site/content/en/benchmark.html +5 -0
- package/docs-site/content/en/commands.html +30 -13
- package/docs-site/content/en/ecosystem.html +144 -0
- package/docs-site/content/en/index.html +24 -9
- package/docs-site/content/en/quickstart.html +10 -12
- package/docs-site/content/en/security.html +1 -1
- package/docs-site/content/en/team.html +2 -2
- package/docs-site/content/en/whats-new.html +77 -1
- package/docs-site/content/en/workflows.html +29 -31
- package/docs-site/content/index.html +24 -9
- package/docs-site/content/quickstart.html +10 -12
- package/docs-site/content/security.html +2 -2
- package/docs-site/content/team.html +2 -2
- package/docs-site/content/whats-new.html +77 -1
- package/docs-site/content/workflows.html +36 -38
- package/docs-site/context-engine.html +2 -1
- package/docs-site/ecosystem.html +283 -0
- package/docs-site/en/architecture.html +3 -2
- package/docs-site/en/benchmark.html +7 -1
- package/docs-site/en/commands.html +32 -14
- package/docs-site/en/context-engine.html +2 -1
- package/docs-site/en/ecosystem.html +283 -0
- package/docs-site/en/index.html +30 -14
- package/docs-site/en/mcp-approval.html +2 -1
- package/docs-site/en/mcp-auth.html +2 -1
- package/docs-site/en/mcp-servers.html +2 -1
- package/docs-site/en/mcp.html +2 -1
- package/docs-site/en/permissions.html +2 -1
- package/docs-site/en/profiles.html +2 -1
- package/docs-site/en/quickstart.html +17 -18
- package/docs-site/en/runtime.html +2 -1
- package/docs-site/en/scope.html +2 -1
- package/docs-site/en/security.html +6 -5
- package/docs-site/en/team.html +4 -3
- package/docs-site/en/whats-new.html +85 -8
- package/docs-site/en/workflows.html +34 -35
- package/docs-site/index.html +30 -14
- package/docs-site/mcp-approval.html +2 -1
- package/docs-site/mcp-auth.html +2 -1
- package/docs-site/mcp-servers.html +2 -1
- package/docs-site/mcp.html +2 -1
- package/docs-site/permissions.html +2 -1
- package/docs-site/profiles.html +2 -1
- package/docs-site/quickstart.html +17 -18
- package/docs-site/runtime.html +2 -1
- package/docs-site/scope.html +2 -1
- package/docs-site/security.html +7 -6
- package/docs-site/team.html +4 -3
- package/docs-site/whats-new.html +85 -8
- package/docs-site/workflows.html +41 -42
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/private-holdout-v1/CURRENT_BOUNDARY.md +17 -0
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +8 -7
- package/packages/piagent-core/README.md +15 -23
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +2 -0
- package/packages/piagent-core/extensions/experiment-loop-policy.ts +75 -0
- package/packages/piagent-core/extensions/local-state-path.js +15 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +108 -960
- package/packages/piagent-core/extensions/policy-core.js +2 -2
- package/packages/piagent-core/extensions/source-checkout-paths.ts +54 -0
- package/packages/piagent-core/managed/broker-client.mjs +49 -0
- package/packages/piagent-core/managed/capabilities.mjs +28 -0
- package/packages/piagent-core/managed/compaction.mjs +63 -0
- package/packages/piagent-core/managed/doctor.mjs +107 -0
- package/packages/piagent-core/managed/native-catalog.mjs +42 -0
- package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
- package/packages/piagent-core/managed/repository-operation.mjs +84 -0
- package/packages/piagent-core/managed/request-stream.mjs +129 -0
- package/packages/piagent-core/managed/resource-loader.mjs +42 -0
- package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
- package/packages/piagent-core/managed/search-fallback.mjs +93 -0
- package/packages/piagent-core/managed/session.mjs +472 -0
- package/packages/piagent-core/managed/store.d.mts +3 -0
- package/packages/piagent-core/managed/store.mjs +64 -0
- package/packages/piagent-core/managed/tool-boundary.mjs +243 -0
- package/packages/piagent-core/managed/tool-worker.mjs +36 -0
- package/packages/piagent-core/managed/toolchain.mjs +120 -0
- package/packages/piagent-core/managed/web-fetch.mjs +105 -0
- package/packages/piagent-core/managed/web-search.mjs +96 -0
- package/packages/piagent-core/managed/workflow.mjs +299 -0
- package/packages/piagent-core/package.json +2 -5
- package/packages/piagent-core/policies/base-policy.json +80 -26
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
- package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
- package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
- package/packages/piagent-core/runtime/managed-failure.mjs +164 -0
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
- package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
- package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
- package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
- package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
- package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
- package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
- package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
- package/packages/piagent-core/security/sensitive-text.js +9 -0
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
- package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
- package/packages/piagent-core/subagents/piagent-planner.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DMU7Xe_O.js → MarkdownMessage-Dd9wPk-v.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-7I-50Q2U.js +228 -0
- package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
- package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
- package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
- package/packages/piagent-webui/gateway/company-relay.ts +308 -0
- package/packages/piagent-webui/gateway/control-socket.ts +39 -12
- package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
- package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
- package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
- package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
- package/packages/piagent-webui/gateway/pi-host.ts +22 -4
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
- package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
- package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
- package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +40 -9
- package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
- package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
- package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
- package/packages/piagent-webui/ownership/profile-state.ts +2 -0
- package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
- package/packages/piagent-webui/server/approval-projection.ts +9 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
- package/packages/piagent-webui/server/loopback-server.ts +37 -1
- package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
- package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
- package/packages/piagent-webui/server/transcript-projection.ts +86 -3
- package/packs/engineering-base/pack.json +4 -0
- package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
- package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
- package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
- package/scripts/build-docs-site.mjs +23 -12
- package/scripts/check-public-wording.mjs +5 -6
- package/scripts/check-runtime-advisories.mjs +23 -6
- package/scripts/claude-model-additions.mjs +33 -0
- package/scripts/configure-model-scope.sh +30 -6
- package/scripts/configure-subagents.sh +1 -1
- package/scripts/explain-command.mjs +5 -6
- package/scripts/model-scope-prune.mjs +75 -0
- package/scripts/phase-tools-evaluation.mjs +9 -4
- package/scripts/piagent-cli.mjs +51 -2
- package/scripts/piagent-dashboard.mjs +27 -4
- package/scripts/piagent-installations.mjs +26 -0
- package/scripts/piagent-studio.mjs +155 -0
- package/scripts/runtime-advisory-exceptions.json +29 -0
- package/scripts/team-doctor.sh +20 -1
- package/scripts/typescript-loader.mjs +6 -0
- package/scripts/verify-local.sh +2 -2
- package/templates/global/settings.json +3 -1
- package/templates/project/.pi/project-context.md +3 -3
- package/templates/project/AGENTS.md +4 -5
- package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
- package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
- package/packages/piagent-webui/dist/client/assets/index-Cui9AAV_.js +0 -176
|
@@ -13,6 +13,9 @@ const MAX_BOOTSTRAP_BODY_BYTES = 4_096;
|
|
|
13
13
|
const MAX_CONTROL_BODY_BYTES = 70_000;
|
|
14
14
|
const MAX_ATTACHMENT_BODY_BYTES = 11_250_000;
|
|
15
15
|
const REQUESTS_PER_MINUTE = 120;
|
|
16
|
+
// An authenticated browser session refreshes its lists on every live event;
|
|
17
|
+
// several busy conversations exceed 120 reads a minute in normal use.
|
|
18
|
+
const SESSION_REQUESTS_PER_MINUTE = 600;
|
|
16
19
|
const BOOTSTRAP_FAILURES_PER_MINUTE = 8;
|
|
17
20
|
const CONTROLS_PER_MINUTE = 60;
|
|
18
21
|
const MAX_JSON_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
@@ -69,6 +72,10 @@ export async function startLoopbackServer(options: {
|
|
|
69
72
|
readProviderAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
|
|
70
73
|
executeProviderAuth?: (command: unknown) => unknown | Promise<unknown>;
|
|
71
74
|
executeProjectImport?: () => unknown | Promise<unknown>;
|
|
75
|
+
readCompanyStatus?: () => unknown | Promise<unknown>;
|
|
76
|
+
executeCompanyConnect?: () => unknown | Promise<unknown>;
|
|
77
|
+
// Reads of a company session, answered by the company Gateway; null: local.
|
|
78
|
+
relaySessionRead?: (sessionRef: string, path: string) => Promise<{ status: number; value: unknown } | null>;
|
|
72
79
|
gatewayProtocol?: GatewayProtocolHandler;
|
|
73
80
|
readModel?: WebUiReadModelProvider;
|
|
74
81
|
executeControl?: (command: unknown) => unknown | Promise<unknown>;
|
|
@@ -113,7 +120,7 @@ export async function startLoopbackServer(options: {
|
|
|
113
120
|
if (now - rate.windowStart >= 60_000) Object.assign(rate, { windowStart: now, requests: 0, bootstrapFailures: 0 });
|
|
114
121
|
rate.requests += 1; rates.set(rateKey, rate);
|
|
115
122
|
while (rates.size > 128) rates.delete(rates.keys().next().value as string);
|
|
116
|
-
if (rate.requests > REQUESTS_PER_MINUTE) return errorResponse(response, 429, "rate-limit");
|
|
123
|
+
if (rate.requests > (browserSession ? SESSION_REQUESTS_PER_MINUTE : REQUESTS_PER_MINUTE)) return errorResponse(response, 429, "rate-limit");
|
|
117
124
|
let url: URL;
|
|
118
125
|
try { url = new URL(request.url ?? "/", origin); }
|
|
119
126
|
catch { return errorResponse(response, 400, "invalid-url"); }
|
|
@@ -176,6 +183,33 @@ export async function startLoopbackServer(options: {
|
|
|
176
183
|
}
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
// A gateway without company sessions (the company WebUI itself, or a
|
|
187
|
+
// dashboard with no relay) answers null: no company section, no failed load.
|
|
188
|
+
if (request.method === "GET" && url.pathname === "/api/v1/managed") {
|
|
189
|
+
if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
|
|
190
|
+
if (url.search) return errorResponse(response, 400, "invalid-managed-query");
|
|
191
|
+
if (!options.readCompanyStatus) return jsonResponse(response, 200, null);
|
|
192
|
+
try { return jsonResponse(response, 200, await options.readCompanyStatus()); }
|
|
193
|
+
catch { return errorResponse(response, 503, "managed-status-unavailable"); }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Starts (or reconnects to) the company Gateway; macOS may ask for Keychain access.
|
|
197
|
+
if (request.method === "POST" && url.pathname === "/api/v1/managed/connect" && options.executeCompanyConnect) {
|
|
198
|
+
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
199
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
200
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
201
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
202
|
+
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
203
|
+
let command: Record<string, unknown>;
|
|
204
|
+
try { command = JSON.parse((await requestBody(request, MAX_BOOTSTRAP_BODY_BYTES)).toString("utf8")); }
|
|
205
|
+
catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-managed-connect"); }
|
|
206
|
+
if (!command || typeof command !== "object" || Array.isArray(command) || Object.keys(command).length !== 1 || command.action !== "managed.connect") {
|
|
207
|
+
return errorResponse(response, 400, "invalid-managed-connect");
|
|
208
|
+
}
|
|
209
|
+
try { return jsonResponse(response, 200, await options.executeCompanyConnect()); }
|
|
210
|
+
catch { return errorResponse(response, 503, "managed-connect-failed"); }
|
|
211
|
+
}
|
|
212
|
+
|
|
179
213
|
if (request.method === "POST" && url.pathname === "/api/v1/provider-auth" && options.executeProviderAuth) {
|
|
180
214
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
181
215
|
const mutationSession = auth.authorizeMutation(request);
|
|
@@ -353,6 +387,8 @@ export async function startLoopbackServer(options: {
|
|
|
353
387
|
return errorResponse(response, 400, "invalid-session-inspection-ref");
|
|
354
388
|
}
|
|
355
389
|
try {
|
|
390
|
+
const relayed = await options.relaySessionRead?.(sessionRef, inspectionPath + url.search);
|
|
391
|
+
if (relayed) return jsonResponse(response, relayed.status, relayed.value);
|
|
356
392
|
if (inspectionPath === "/connections") {
|
|
357
393
|
if (url.search || !options.readSessionConnections) return errorResponse(response, 404, "inspection-route-not-found");
|
|
358
394
|
const value = await options.readSessionConnections(sessionRef);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Some providers end a turn the member stopped with `stopReason: "error"` and
|
|
2
|
+
// the AbortError text instead of `stopReason: "aborted"`. That is a stop, not a
|
|
3
|
+
// model failure: showing "the model returned an error" for it tells the member
|
|
4
|
+
// to resend or change model after they pressed Stop themselves.
|
|
5
|
+
const ABORT_ERROR = /\b(?:this operation|the operation|request) was aborted\b|\bAbortError\b|^aborted$/i;
|
|
6
|
+
|
|
7
|
+
export function effectiveStopReason(message: any): string {
|
|
8
|
+
const stopReason = String(message?.stopReason ?? "");
|
|
9
|
+
if (stopReason === "error" && ABORT_ERROR.test(String(message?.errorMessage ?? "").trim())) return "aborted";
|
|
10
|
+
return stopReason;
|
|
11
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
|
|
5
|
+
|
|
6
|
+
// What a coding-agent timeline shows for one tool call: the kind of action,
|
|
7
|
+
// its target (project-relative path, command, query or URL), an edit's diff
|
|
8
|
+
// and a bounded result preview. Every string is ANSI-stripped and redacted;
|
|
9
|
+
// raw arguments and results never leave the server.
|
|
10
|
+
export type ToolKind = "read" | "write" | "edit" | "command" | "search" | "list" | "web-search" | "web-fetch"
|
|
11
|
+
| "subagent" | "network" | "git" | "plan" | "other";
|
|
12
|
+
export type ToolSummary = { kind: ToolKind; target: string | null; detail: string | null };
|
|
13
|
+
export type ToolChange = { added: number; removed: number; preview: string; truncated: boolean };
|
|
14
|
+
export type ToolResult = { text: string; truncated: boolean; isError: boolean };
|
|
15
|
+
export type TurnUsage = { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; totalTokens: number };
|
|
16
|
+
|
|
17
|
+
const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g;
|
|
18
|
+
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
19
|
+
const TARGET_CHARS = 400, DETAIL_CHARS = 400, PREVIEW_LINES = 80, PREVIEW_CHARS = 6_000, RESULT_LINES = 60, RESULT_CHARS = 4_000;
|
|
20
|
+
|
|
21
|
+
function clean(value: unknown, max: number): string | null {
|
|
22
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
23
|
+
const text = redactedWhole(value);
|
|
24
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
25
|
+
}
|
|
26
|
+
// Redaction needs the whole text: a private key is recognised only while its
|
|
27
|
+
// lines are together, so file contents and edit texts are redacted before they
|
|
28
|
+
// are split into preview rows.
|
|
29
|
+
function redactedWhole(value: unknown): string {
|
|
30
|
+
return redactSensitiveText(String(value ?? "").replace(ANSI, "").replace(CONTROL, " ")).text;
|
|
31
|
+
}
|
|
32
|
+
function record(value: unknown): Record<string, unknown> {
|
|
33
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
34
|
+
}
|
|
35
|
+
function pick(args: Record<string, unknown>, keys: readonly string[]): unknown {
|
|
36
|
+
for (const key of keys) if (args[key] !== undefined && args[key] !== null && args[key] !== "") return args[key];
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
// Paths inside the project are shown relative to it; anything else only by
|
|
40
|
+
// its file name, so folders outside the project never reach the browser.
|
|
41
|
+
export function displayPath(value: unknown, cwd?: string): string | null {
|
|
42
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
43
|
+
const raw = value.trim();
|
|
44
|
+
if (!path.isAbsolute(raw)) return raw.split(/[\\/]/).includes("..") ? clean(path.basename(raw), TARGET_CHARS) : clean(raw, TARGET_CHARS);
|
|
45
|
+
const relative = cwd ? path.relative(cwd, raw) : "";
|
|
46
|
+
return cwd && !relative.startsWith("..") && !path.isAbsolute(relative) ? clean(relative || ".", TARGET_CHARS) : clean(path.basename(raw), TARGET_CHARS);
|
|
47
|
+
}
|
|
48
|
+
// A command as run in the project: the project folder reads "." and the home
|
|
49
|
+
// folder "~".
|
|
50
|
+
function displayCommand(value: unknown, cwd?: string): string | null {
|
|
51
|
+
if (typeof value !== "string") return null;
|
|
52
|
+
let shown = value;
|
|
53
|
+
if (cwd && cwd.length > 1) shown = shown.split(cwd).join(".");
|
|
54
|
+
const home = os.homedir();
|
|
55
|
+
if (home.length > 1) shown = shown.split(home).join("~");
|
|
56
|
+
return clean(shown, TARGET_CHARS);
|
|
57
|
+
}
|
|
58
|
+
// A multi-line command (a heredoc, a script) reads by its first line; the
|
|
59
|
+
// whole command, bounded, opens in the step's body.
|
|
60
|
+
function commandParts(value: unknown, cwd?: string): { target: string | null; script: string | null } {
|
|
61
|
+
const full = displayCommand(value, cwd), rows = full ? full.split("\n").filter((row) => row.trim()) : [];
|
|
62
|
+
return rows.length > 1 ? { target: `${rows[0].trim().slice(0, TARGET_CHARS - 2)} …`, script: full } : { target: full, script: null };
|
|
63
|
+
}
|
|
64
|
+
const lines = (text: string) => text === "" ? [] : text.replace(/\r\n/g, "\n").split("\n");
|
|
65
|
+
|
|
66
|
+
function bounded(rows: string[], removed: number, added: number): ToolChange {
|
|
67
|
+
let preview = "", count = 0, truncated = false;
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const safe = (clean(row, 400) ?? row.slice(0, 1)).replace(/\n/g, " ");
|
|
70
|
+
if (count >= PREVIEW_LINES || preview.length + safe.length + 1 > PREVIEW_CHARS) { truncated = true; break; }
|
|
71
|
+
preview += (count ? "\n" : "") + safe; count += 1;
|
|
72
|
+
}
|
|
73
|
+
return { added, removed, preview, truncated };
|
|
74
|
+
}
|
|
75
|
+
// Old and new text of each edit, trimmed to the changed lines plus two lines
|
|
76
|
+
// of context: enough to read the change without reproducing the file.
|
|
77
|
+
function editChange(edits: Array<{ oldText?: unknown; newText?: unknown }>): ToolChange {
|
|
78
|
+
const rows: string[] = []; let added = 0, removed = 0;
|
|
79
|
+
for (const edit of edits.slice(0, 20)) {
|
|
80
|
+
const before = lines(redactedWhole(edit.oldText)), after = lines(redactedWhole(edit.newText));
|
|
81
|
+
let head = 0; while (head < before.length && head < after.length && before[head] === after[head]) head += 1;
|
|
82
|
+
let tail = 0; while (tail < before.length - head && tail < after.length - head && before[before.length - 1 - tail] === after[after.length - 1 - tail]) tail += 1;
|
|
83
|
+
const gone = before.slice(head, before.length - tail), come = after.slice(head, after.length - tail);
|
|
84
|
+
removed += gone.length; added += come.length;
|
|
85
|
+
if (rows.length) rows.push("@@");
|
|
86
|
+
for (const row of before.slice(Math.max(0, head - 2), head)) rows.push(` ${row}`);
|
|
87
|
+
for (const row of gone) rows.push(`-${row}`);
|
|
88
|
+
for (const row of come) rows.push(`+${row}`);
|
|
89
|
+
for (const row of before.slice(before.length - tail, Math.min(before.length, before.length - tail + 2))) rows.push(` ${row}`);
|
|
90
|
+
}
|
|
91
|
+
return bounded(rows, removed, added);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A patch in the apply_patch format: the files it touches (from its
|
|
95
|
+
// "*** Add/Update/Delete File:" headers) and its +/- body lines, redacted as a
|
|
96
|
+
// whole like an edit. "write" when it only adds files.
|
|
97
|
+
function patchChange(value: unknown, cwd?: string): { summary: ToolSummary; change: ToolChange } {
|
|
98
|
+
const text = redactedWhole(value), rows: string[] = [], files: string[] = []; let added = 0, removed = 0, onlyAdds = true;
|
|
99
|
+
for (const row of lines(text)) {
|
|
100
|
+
const header = row.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
|
|
101
|
+
if (header) { files.push(header[2].trim()); if (header[1] !== "Add") onlyAdds = false; if (rows.length) rows.push("@@"); continue; }
|
|
102
|
+
if (/^\*\*\* (Begin|End) Patch$|^\*\*\* Move to: /.test(row)) continue;
|
|
103
|
+
if (row.startsWith("@@")) { if (rows.length && rows.at(-1) !== "@@") rows.push("@@"); continue; }
|
|
104
|
+
if (row.startsWith("+")) added += 1; else if (row.startsWith("-")) removed += 1;
|
|
105
|
+
rows.push(row);
|
|
106
|
+
}
|
|
107
|
+
const first = displayPath(files[0], cwd), more = files.length - 1;
|
|
108
|
+
return { summary: { kind: files.length && onlyAdds ? "write" : "edit", target: first && more > 0 ? `${first} +${more}` : first, detail: null },
|
|
109
|
+
change: bounded(rows, removed, added) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ALIASES: Record<string, string> = { read_file: "read", view: "read", cat: "read", write_file: "write", create_file: "write",
|
|
113
|
+
edit_file: "edit", str_replace: "edit", str_replace_editor: "edit", shell: "bash", exec: "bash", run: "bash", terminal: "bash", run_experiment: "bash",
|
|
114
|
+
glob: "find", search: "grep", search_files: "grep", list: "ls", list_files: "ls", list_dir: "ls" };
|
|
115
|
+
|
|
116
|
+
export function summarizeToolCall(name: unknown, input: unknown, cwd?: string): { summary: ToolSummary; change?: ToolChange } {
|
|
117
|
+
const raw = String(name ?? "").toLowerCase(), tool = ALIASES[raw] ?? raw, args = record(input);
|
|
118
|
+
const file = pick(args, ["path", "file_path", "filePath", "file"]);
|
|
119
|
+
switch (tool) {
|
|
120
|
+
case "read": {
|
|
121
|
+
const offset = Number(args.offset), limit = Number(args.limit);
|
|
122
|
+
const detail = Number.isInteger(offset) && offset > 0 ? `${offset}${Number.isInteger(limit) && limit > 0 ? `–${offset + limit - 1}` : "+"}` : null;
|
|
123
|
+
return { summary: { kind: "read", target: displayPath(file, cwd), detail } };
|
|
124
|
+
}
|
|
125
|
+
case "write": {
|
|
126
|
+
const content = lines(redactedWhole(args.content));
|
|
127
|
+
return { summary: { kind: "write", target: displayPath(file, cwd), detail: null },
|
|
128
|
+
change: bounded(content.map((row) => `+${row}`), 0, content.length) };
|
|
129
|
+
}
|
|
130
|
+
case "edit": {
|
|
131
|
+
const edits = Array.isArray(args.edits) ? args.edits as Array<{ oldText?: unknown; newText?: unknown }>
|
|
132
|
+
: [{ oldText: args.oldText ?? args.old_string, newText: args.newText ?? args.new_string }];
|
|
133
|
+
return { summary: { kind: "edit", target: displayPath(file, cwd), detail: null }, change: editChange(edits) };
|
|
134
|
+
}
|
|
135
|
+
case "apply_patch": return patchChange(pick(args, ["patch", "input"]), cwd);
|
|
136
|
+
case "bash": {
|
|
137
|
+
const command = commandParts(args.command, cwd);
|
|
138
|
+
return { summary: { kind: "command", target: command.target, detail: command.script } };
|
|
139
|
+
}
|
|
140
|
+
case "run_with_network":
|
|
141
|
+
return { summary: { kind: "network", target: commandParts(args.command, cwd).target, detail: clean(args.reason, DETAIL_CHARS) } };
|
|
142
|
+
case "grep": case "find": {
|
|
143
|
+
const where = [displayPath(args.path, cwd), clean(args.glob, 120)].filter(Boolean).join(" · ");
|
|
144
|
+
return { summary: { kind: "search", target: clean(args.pattern, TARGET_CHARS), detail: where || null } };
|
|
145
|
+
}
|
|
146
|
+
case "ls": return { summary: { kind: "list", target: displayPath(file ?? ".", cwd), detail: null } };
|
|
147
|
+
case "web_search": return { summary: { kind: "web-search", target: clean(args.query, TARGET_CHARS), detail: null } };
|
|
148
|
+
case "web_fetch": return { summary: { kind: "web-fetch", target: clean(args.url, TARGET_CHARS), detail: null } };
|
|
149
|
+
case "delegate": case "subagent":
|
|
150
|
+
return { summary: { kind: "subagent", target: clean(pick(args, ["role", "agent", "name"]), 40), detail: clean(args.task, 200) } };
|
|
151
|
+
case "fetch_origin": return { summary: { kind: "git", target: clean(pick(args, ["remote", "branch", "ref"]) ?? "origin", 120), detail: null } };
|
|
152
|
+
case "update_plan": {
|
|
153
|
+
// The checklist's progress, and the step being worked on.
|
|
154
|
+
const steps = Array.isArray(args.plan) ? (args.plan as Array<{ step?: unknown; status?: unknown }>) : [];
|
|
155
|
+
const done = steps.filter((p) => p?.status === "completed").length, current = steps.find((p) => p?.status === "in_progress");
|
|
156
|
+
return { summary: { kind: "plan", target: `${done}/${steps.length}`, detail: clean(current?.step, 200) } };
|
|
157
|
+
}
|
|
158
|
+
default: {
|
|
159
|
+
const first = Object.values(args).find((value) => typeof value === "string" && value.trim()) as string | undefined;
|
|
160
|
+
const target = first && /[\\/]/.test(first) ? displayPath(first, cwd) : clean(first, 200);
|
|
161
|
+
return { summary: { kind: "other", target, detail: null } };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function toolResultPreview(message: any): ToolResult {
|
|
167
|
+
const text = typeof message?.content === "string" ? message.content : Array.isArray(message?.content)
|
|
168
|
+
? message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string").map((part: any) => part.text).join("\n") : "";
|
|
169
|
+
const all = lines(clean(text, 1_000_000) ?? ""), kept = all.slice(0, RESULT_LINES).join("\n");
|
|
170
|
+
return { text: kept.slice(0, RESULT_CHARS), truncated: all.length > RESULT_LINES || kept.length > RESULT_CHARS, isError: Boolean(message?.isError) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function turnUsage(message: any): TurnUsage | null {
|
|
174
|
+
const usage = record(message?.usage);
|
|
175
|
+
const count = (value: unknown) => Number.isFinite(Number(value)) && Number(value) > 0 ? Math.min(10_000_000_000, Math.round(Number(value))) : 0;
|
|
176
|
+
const value = { inputTokens: count(usage.input), outputTokens: count(usage.output), cacheReadTokens: count(usage.cacheRead),
|
|
177
|
+
cacheWriteTokens: count(usage.cacheWrite), totalTokens: count(usage.totalTokens) };
|
|
178
|
+
if (!value.totalTokens) value.totalTokens = Math.min(10_000_000_000, value.inputTokens + value.outputTokens + value.cacheReadTokens + value.cacheWriteTokens);
|
|
179
|
+
return value.totalTokens ? value : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function modelLabel(message: any): string | null {
|
|
183
|
+
const model = typeof message?.model === "string" ? message.model.replace(/[^A-Za-z0-9._:@/-]/g, "").slice(0, 120) : "";
|
|
184
|
+
return model || null;
|
|
185
|
+
}
|
|
@@ -5,9 +5,13 @@ import { looksLikeCompletionClaim } from "../../piagent-core/runtime/session/com
|
|
|
5
5
|
import { isLightweightNonAuthorizingChangeLanguage } from "../../piagent-core/runtime/workflows/change-clarification.ts";
|
|
6
6
|
import { workflowCommandPattern } from "../../piagent-core/runtime/workflows/webui-workflow.ts";
|
|
7
7
|
import { hasVisibleText } from "../shared/text-visibility.ts";
|
|
8
|
+
import { effectiveStopReason } from "./message-stop-reason.ts";
|
|
8
9
|
import { WEBUI_MESSAGE_CORRELATION_ENTRY_TYPE, webUiMessageCorrelationEntry,
|
|
9
10
|
type WebUiMessageCorrelation } from "../shared/message-correlation.ts";
|
|
10
11
|
import { terminalDeliveryPairs, type TerminalDeliveryPair, type TerminalDeliveryReceipt } from "./terminal-delivery-receipt.ts";
|
|
12
|
+
import { parseFailure } from "../../piagent-core/runtime/managed-failure.mjs";
|
|
13
|
+
import { modelLabel, summarizeToolCall, toolResultPreview, turnUsage, type ToolChange, type ToolResult, type ToolSummary,
|
|
14
|
+
type TurnUsage } from "./tool-call-summary.ts";
|
|
11
15
|
|
|
12
16
|
const MAX_ENTRIES = 50_000;
|
|
13
17
|
const MAX_ITEMS = 200;
|
|
@@ -28,7 +32,11 @@ type TranscriptAttachment = { displayName: string; kind: "file" | "image" | "doc
|
|
|
28
32
|
type TranscriptItem = { messageRef: string; parentMessageRef: string | null; role: "user" | "assistant" | "tool-result" | "custom"; recordedAt: string;
|
|
29
33
|
agentOperationId: string | null; messageRequestId?: string; turnIndex: number | null; content: TranscriptContent;
|
|
30
34
|
attachments?: TranscriptAttachment[];
|
|
31
|
-
toolCalls: Array<{ toolCallRef: string; toolName: string; state: "requested" | "completed" | "failed" | "unknown"
|
|
35
|
+
toolCalls: Array<{ toolCallRef: string; toolName: string; state: "requested" | "completed" | "failed" | "unknown";
|
|
36
|
+
summary?: ToolSummary; change?: ToolChange; result?: ToolResult }>; usage?: TurnUsage; model?: string; process?: HarnessProcess };
|
|
37
|
+
type HarnessFinding = { severity: "blocking" | "major" | "minor"; file: string; line: number | null; issue: string };
|
|
38
|
+
type HarnessProcess = { phase: "plan" | "verify" | "review" | "final"; outcome?: string; loop?: number; maxLoops?: number; verified?: boolean; reviewed?: boolean;
|
|
39
|
+
blockingOpen?: number; planOpen?: number; planSkipped?: true; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string; findings?: HarnessFinding[] };
|
|
32
40
|
export type TranscriptDocument = { schemaVersion: 1; version: "piagent-webui-transcript-v1"; generatedAt: string; identity: TranscriptIdentity;
|
|
33
41
|
revision: TranscriptRevision; eventCursor: string; state: "ready" | "unavailable"; items: TranscriptItem[];
|
|
34
42
|
page: { beforeCursor: string | null; nextBeforeCursor: string | null; hasOlder: boolean; limit: number; truncated: boolean }; reasonCode: string | null };
|
|
@@ -44,6 +52,8 @@ export type TranscriptProjectionInput = {
|
|
|
44
52
|
taskOutcome?: string | null;
|
|
45
53
|
terminalDeliveryReceipt?: TerminalDeliveryReceipt;
|
|
46
54
|
terminalDeliveryEntries?: unknown[];
|
|
55
|
+
// The session's project folder: tool paths are shown relative to it.
|
|
56
|
+
cwd?: string;
|
|
47
57
|
};
|
|
48
58
|
|
|
49
59
|
function hash(value: string): string { return createHash("sha256").update(value).digest("hex"); }
|
|
@@ -88,8 +98,18 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
|
|
|
88
98
|
// text the operator typed; the remaining recognized blocks become cards.
|
|
89
99
|
return { text: attachments.length ? String(textParts[0]?.text ?? "") : messageText(message), attachments: attachments.slice(0, 4) };
|
|
90
100
|
}
|
|
101
|
+
// A company request fails with Studio's (or Agent Watch's) own code: who
|
|
102
|
+
// failed, which kind of failure, and Studio's request id. Text is never shown.
|
|
103
|
+
export type CompanyFailure = { role: "main" | "research" | "review"; reasonCode: string; code: string; requestRef: string | null; local: boolean };
|
|
104
|
+
export function companyFailure(text: unknown): CompanyFailure | null {
|
|
105
|
+
const parsed = parseFailure(text);
|
|
106
|
+
if (!parsed || !/^[A-Za-z0-9][A-Za-z0-9_:.-]{0,79}$/.test(parsed.code)) return null;
|
|
107
|
+
return { role: parsed.role as CompanyFailure["role"], reasonCode: `company-${parsed.kind}`, code: parsed.code, requestRef: parsed.requestId, local: parsed.local };
|
|
108
|
+
}
|
|
109
|
+
const COMPANY = "agent_watch_managed";
|
|
91
110
|
function assistantFailureReason(message: any): string | null {
|
|
92
111
|
if (message?.role !== "assistant" || message?.stopReason !== "error") return null;
|
|
112
|
+
if (message.provider === COMPANY) return companyFailure(message.errorMessage)?.reasonCode ?? "company-failed";
|
|
93
113
|
const detail = String(message?.errorMessage ?? "").toLowerCase();
|
|
94
114
|
if (/(?:authentication|auth|token|credential).{0,48}expired|expired.{0,48}(?:authentication|auth|token|credential)/.test(detail)) {
|
|
95
115
|
return "provider-auth-expired";
|
|
@@ -103,7 +123,7 @@ function assistantFailureReason(message: any): string | null {
|
|
|
103
123
|
}
|
|
104
124
|
function assistantUnavailableReason(message: any, text: string, projectedToolCalls: TranscriptItem["toolCalls"]): string | null {
|
|
105
125
|
if (message?.role !== "assistant") return null;
|
|
106
|
-
const stopReason =
|
|
126
|
+
const stopReason = effectiveStopReason(message);
|
|
107
127
|
if (stopReason === "error") return assistantFailureReason(message) ?? "provider-response-failed";
|
|
108
128
|
if (stopReason === "aborted") return "assistant-message-aborted";
|
|
109
129
|
if (stopReason === "length") return "assistant-output-incomplete";
|
|
@@ -141,6 +161,31 @@ function toolCalls(message: any, sessionRef: string, role: TranscriptItem["role"
|
|
|
141
161
|
return { toolCallRef: opaque("tool", [sessionRef, item.id ?? index]), toolName: safeToolName(item.name), state: item.state };
|
|
142
162
|
});
|
|
143
163
|
}
|
|
164
|
+
// The harness's process notes ("agent-watch-process"): what it asked of the
|
|
165
|
+
// agent before a code-changing turn ended, and how that turn ended.
|
|
166
|
+
const PROCESS_OUTCOMES = ["no_change", "interrupted", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"];
|
|
167
|
+
const POLICY_MODES = ["off", "suggest", "require"];
|
|
168
|
+
function harnessProcess(details: any): HarnessProcess | null {
|
|
169
|
+
if (!["plan", "verify", "review", "final"].includes(details?.phase)) return null;
|
|
170
|
+
const loop = (n: unknown): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) <= 3;
|
|
171
|
+
const out: HarnessProcess = { phase: details.phase };
|
|
172
|
+
if (PROCESS_OUTCOMES.includes(details.outcome)) out.outcome = details.outcome;
|
|
173
|
+
if (loop(details.loop)) out.loop = details.loop;
|
|
174
|
+
if (loop(details.maxLoops)) out.maxLoops = details.maxLoops;
|
|
175
|
+
if (typeof details.verified === "boolean") out.verified = details.verified;
|
|
176
|
+
if (typeof details.reviewed === "boolean") out.reviewed = details.reviewed;
|
|
177
|
+
if (Number.isInteger(details.blockingOpen) && details.blockingOpen >= 0) out.blockingOpen = Math.min(details.blockingOpen, 1000);
|
|
178
|
+
if (Number.isInteger(details.planOpen) && details.planOpen >= 1 && details.planOpen <= 30) out.planOpen = details.planOpen;
|
|
179
|
+
if (details.planSkipped === true) out.planSkipped = true;
|
|
180
|
+
if (POLICY_MODES.includes(details.policy?.verify)) out.verifyPolicy = details.policy.verify;
|
|
181
|
+
if (POLICY_MODES.includes(details.policy?.review)) out.reviewPolicy = details.policy.review;
|
|
182
|
+
if (["too_large", "not_git", "limit", "failed"].includes(details.reviewUnavailable)) out.reviewUnavailable = details.reviewUnavailable;
|
|
183
|
+
if (Array.isArray(details.findings)) out.findings = details.findings
|
|
184
|
+
.filter((f: any) => ["blocking", "major", "minor"].includes(f?.severity) && typeof f.issue === "string" && f.issue.trim()).slice(0, 10)
|
|
185
|
+
.map((f: any) => ({ severity: f.severity, file: safeText(String(f.file ?? "")).full.slice(0, 300), line: Number.isSafeInteger(f.line) && f.line > 0 ? f.line : null,
|
|
186
|
+
issue: safeText(f.issue).full.slice(0, 500) }));
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
144
189
|
function role(message: any): TranscriptItem["role"] | null {
|
|
145
190
|
return message?.role === "user" ? "user" : message?.role === "assistant" ? "assistant" : message?.role === "toolResult" ? "tool-result" : null;
|
|
146
191
|
}
|
|
@@ -198,6 +243,13 @@ function correlatedTranscriptItems(entries: unknown[], identity: TranscriptIdent
|
|
|
198
243
|
pending = webUiMessageCorrelationEntry(entry);
|
|
199
244
|
continue;
|
|
200
245
|
}
|
|
246
|
+
if (entry?.type === "custom_message" && entry.customType === "agent-watch-process") {
|
|
247
|
+
const process = harnessProcess(entry.details);
|
|
248
|
+
const projected = process && item({ ...entry, type: "message", message: { role: "custom", content: entry.content } }, identity, true);
|
|
249
|
+
if (projected) values.push({ entry, item: { ...projected, process, ...(turn ? { agentOperationId: turn.operationRef, messageRequestId: turn.messageRequestId } : {}) },
|
|
250
|
+
cursor: opaque("transcript", [identity.sessionRef, entry.id]) });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
201
253
|
if (role(entry?.message) === "user") { turn = pending; pending = null; }
|
|
202
254
|
const projected = item(entry, identity);
|
|
203
255
|
if (!projected) continue;
|
|
@@ -291,6 +343,37 @@ function boundedTurnPage(
|
|
|
291
343
|
return { selected, start, compacted: false };
|
|
292
344
|
}
|
|
293
345
|
|
|
346
|
+
// The agent timeline: each tool call of a shown answer carries its summary,
|
|
347
|
+
// an edit's diff and its result preview; answers carry usage and model.
|
|
348
|
+
function timelineItems(selected: ProjectedTranscriptItem[], all: ProjectedTranscriptItem[], cwd?: string): TranscriptItem[] {
|
|
349
|
+
const results = new Map<string, any>();
|
|
350
|
+
for (const value of all) if (value.item.role === "tool-result" && value.item.toolCalls[0]) results.set(value.item.toolCalls[0].toolCallRef, value.entry?.message);
|
|
351
|
+
return selected.map((value) => {
|
|
352
|
+
if (value.item.role !== "assistant") return value.item;
|
|
353
|
+
const message = value.entry?.message;
|
|
354
|
+
const blocks = Array.isArray(message?.content) ? message.content.filter((part: any) => part?.type === "toolCall") : [];
|
|
355
|
+
const toolCalls = value.item.toolCalls.map((call, index) => {
|
|
356
|
+
const block = blocks[index], result = results.get(call.toolCallRef);
|
|
357
|
+
if (!block) return call;
|
|
358
|
+
const { summary, change } = summarizeToolCall(block.name, block.arguments, cwd);
|
|
359
|
+
// A helper's answer can carry its delegated prompt and artifacts: only
|
|
360
|
+
// its outcome is shown, never its text.
|
|
361
|
+
const shown = result && summary.kind !== "subagent" ? { result: toolResultPreview(result) } : {};
|
|
362
|
+
// A failed helper or company search says why (its code), not what it wrote.
|
|
363
|
+
const failed = result?.isError && message?.provider === COMPANY && ["subagent", "web-search"].includes(summary.kind)
|
|
364
|
+
? companyFailure(toolResultPreview(result).text) : null;
|
|
365
|
+
return { ...call, summary, ...(change ? { change } : {}), ...shown, ...(failed ? { failure: failed } : {}),
|
|
366
|
+
...(result ? { state: result.isError ? "failed" as const : "completed" as const } : {}) };
|
|
367
|
+
});
|
|
368
|
+
// A company turn is answered by a harness role; which model sits behind it
|
|
369
|
+
// is the harness's business and stays out of the browser.
|
|
370
|
+
const company = message?.provider === COMPANY;
|
|
371
|
+
const usage = turnUsage(message), model = company ? null : modelLabel(message);
|
|
372
|
+
const failure = company && effectiveStopReason(message) === "error" ? companyFailure(message.errorMessage) : null;
|
|
373
|
+
return { ...value.item, toolCalls, ...(usage ? { usage } : {}), ...(model ? { model } : {}), ...(failure ? { failure } : {}) };
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
294
377
|
function unavailable(input: TranscriptProjectionInput, reasonCode: string, limit: number): TranscriptDocument {
|
|
295
378
|
return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
|
|
296
379
|
identity: structuredClone(input.identity), revision: structuredClone(input.revision), eventCursor: input.eventCursor,
|
|
@@ -314,7 +397,7 @@ export function projectTranscript(input: TranscriptProjectionInput): TranscriptD
|
|
|
314
397
|
if (cursorIndex < 0) return unavailable(input, "transcript-cursor-gap", limit);
|
|
315
398
|
end = cursorIndex;
|
|
316
399
|
}
|
|
317
|
-
const page = boundedTurnPage(projected, end, limit), selected = page.selected
|
|
400
|
+
const page = boundedTurnPage(projected, end, limit), selected = timelineItems(page.selected, projected, input.cwd), hasOlder = page.start > 0;
|
|
318
401
|
return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
|
|
319
402
|
identity: structuredClone(input.identity), revision: structuredClone(input.revision), eventCursor: input.eventCursor,
|
|
320
403
|
state: "ready", items: selected, page: { beforeCursor: input.beforeCursor ?? null, nextBeforeCursor: hasOlder ? cursors[page.start] : null,
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
"$defs": {
|
|
10
10
|
"action": {
|
|
11
11
|
"enum": [
|
|
12
|
-
"runtime.status", "runtime.inspector", "runtime.
|
|
12
|
+
"runtime.status", "runtime.inspector", "runtime.fast-status", "runtime.fast-on", "runtime.fast-off", "orchestration.status",
|
|
13
13
|
"usage.live", "usage.history", "usage.logs", "usage.efficiency", "usage.preflight",
|
|
14
|
-
"onboarding.status", "onboarding.profile", "onboarding.tech",
|
|
15
14
|
"profile.status", "profile.options", "profile.tech-options", "profile.apply", "profile.auto",
|
|
16
15
|
"context.status", "context.rebuild", "context.search", "context.pack", "context.impact", "context.efficiency", "context.preflight", "context.compact",
|
|
17
16
|
"memory.status",
|
|
@@ -91,6 +91,14 @@
|
|
|
91
91
|
"unread": { "type": "boolean" },
|
|
92
92
|
"composerAvailable": { "type": "boolean" },
|
|
93
93
|
"needsAttention": { "type": "boolean" },
|
|
94
|
+
"managedHelpers": { "type": "object", "additionalProperties": false, "required": ["active", "maximum"], "properties": {"active": {"type": ["integer", "null"], "minimum": 0, "maximum": 2}, "maximum": {"const": 2}} },
|
|
95
|
+
"managedThinkingLevels": {"type": "array", "uniqueItems": true, "maxItems": 7, "items": {"enum": ["off","minimal","low","medium","high","xhigh","max"]}},
|
|
96
|
+
"managedPlan": { "type": "object", "additionalProperties": false, "required": ["steps"], "properties": {
|
|
97
|
+
"steps": { "type": "array", "minItems": 1, "maxItems": 30, "items": { "type": "object", "additionalProperties": false, "required": ["step", "status"], "properties": {
|
|
98
|
+
"step": { "type": "string", "minLength": 1, "maxLength": 200 }, "status": { "enum": ["pending", "in_progress", "completed"] } } } } } },
|
|
99
|
+
"managedProcess": { "type": "object", "additionalProperties": false, "required": ["outcome", "verified", "reviewed", "blockingOpen"], "properties": {
|
|
100
|
+
"outcome": { "enum": ["no_change", "interrupted", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
|
|
101
|
+
"verified": { "type": "boolean" }, "reviewed": { "type": "boolean" }, "blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 }, "planSkipped": { "const": true } } },
|
|
94
102
|
"modelLabel": { "$ref": "common-v1.schema.json#/$defs/nullableDisplayText" },
|
|
95
103
|
"thinkingLevel": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "unknown"] },
|
|
96
104
|
"contextUsage": { "$ref": "#/$defs/contextUsage" },
|
|
@@ -152,6 +152,52 @@
|
|
|
152
152
|
}
|
|
153
153
|
]
|
|
154
154
|
},
|
|
155
|
+
"harnessProcess": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"required": ["phase"],
|
|
159
|
+
"properties": {
|
|
160
|
+
"phase": { "enum": ["plan", "verify", "review", "final"] },
|
|
161
|
+
"outcome": { "enum": ["no_change", "interrupted", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
|
|
162
|
+
"loop": { "type": "integer", "minimum": 0, "maximum": 3 },
|
|
163
|
+
"maxLoops": { "type": "integer", "minimum": 0, "maximum": 3 },
|
|
164
|
+
"verified": { "type": "boolean" },
|
|
165
|
+
"reviewed": { "type": "boolean" },
|
|
166
|
+
"blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 },
|
|
167
|
+
"planOpen": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
168
|
+
"planSkipped": { "const": true },
|
|
169
|
+
"verifyPolicy": { "enum": ["off", "suggest", "require"] },
|
|
170
|
+
"reviewPolicy": { "enum": ["off", "suggest", "require"] },
|
|
171
|
+
"reviewUnavailable": { "enum": ["too_large", "not_git", "limit", "failed"] },
|
|
172
|
+
"findings": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"maxItems": 10,
|
|
175
|
+
"items": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"additionalProperties": false,
|
|
178
|
+
"required": ["severity", "file", "line", "issue"],
|
|
179
|
+
"properties": {
|
|
180
|
+
"severity": { "enum": ["blocking", "major", "minor"] },
|
|
181
|
+
"file": { "type": "string", "maxLength": 300 },
|
|
182
|
+
"line": { "type": ["integer", "null"], "minimum": 1 },
|
|
183
|
+
"issue": { "type": "string", "maxLength": 500 }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"companyFailure": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"additionalProperties": false,
|
|
192
|
+
"required": ["role", "reasonCode", "code", "requestRef", "local"],
|
|
193
|
+
"properties": {
|
|
194
|
+
"role": { "enum": ["main", "research", "review"] },
|
|
195
|
+
"reasonCode": { "$ref": "common-v1.schema.json#/$defs/reasonCode" },
|
|
196
|
+
"code": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_:.-]{0,79}$" },
|
|
197
|
+
"requestRef": { "anyOf": [{ "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, { "type": "null" }] },
|
|
198
|
+
"local": { "type": "boolean" }
|
|
199
|
+
}
|
|
200
|
+
},
|
|
155
201
|
"toolCall": {
|
|
156
202
|
"type": "object",
|
|
157
203
|
"additionalProperties": false,
|
|
@@ -164,7 +210,39 @@
|
|
|
164
210
|
"maxLength": 80,
|
|
165
211
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,79}$"
|
|
166
212
|
},
|
|
167
|
-
"state": { "enum": ["requested", "completed", "failed", "unknown"] }
|
|
213
|
+
"state": { "enum": ["requested", "completed", "failed", "unknown"] },
|
|
214
|
+
"summary": {
|
|
215
|
+
"type": "object",
|
|
216
|
+
"additionalProperties": false,
|
|
217
|
+
"required": ["kind", "target", "detail"],
|
|
218
|
+
"properties": {
|
|
219
|
+
"kind": { "enum": ["read", "write", "edit", "command", "search", "list", "web-search", "web-fetch", "subagent", "network", "git", "plan", "other"] },
|
|
220
|
+
"target": { "anyOf": [{ "type": "string", "maxLength": 400 }, { "type": "null" }] },
|
|
221
|
+
"detail": { "anyOf": [{ "type": "string", "maxLength": 400 }, { "type": "null" }] }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"change": {
|
|
225
|
+
"type": "object",
|
|
226
|
+
"additionalProperties": false,
|
|
227
|
+
"required": ["added", "removed", "preview", "truncated"],
|
|
228
|
+
"properties": {
|
|
229
|
+
"added": { "type": "integer", "minimum": 0, "maximum": 1000000 },
|
|
230
|
+
"removed": { "type": "integer", "minimum": 0, "maximum": 1000000 },
|
|
231
|
+
"preview": { "type": "string", "maxLength": 6000 },
|
|
232
|
+
"truncated": { "type": "boolean" }
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"result": {
|
|
236
|
+
"type": "object",
|
|
237
|
+
"additionalProperties": false,
|
|
238
|
+
"required": ["text", "truncated", "isError"],
|
|
239
|
+
"properties": {
|
|
240
|
+
"text": { "type": "string", "maxLength": 4000 },
|
|
241
|
+
"truncated": { "type": "boolean" },
|
|
242
|
+
"isError": { "type": "boolean" }
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"failure": { "$ref": "#/$defs/companyFailure" }
|
|
168
246
|
}
|
|
169
247
|
},
|
|
170
248
|
"transcriptItem": {
|
|
@@ -195,6 +273,21 @@
|
|
|
195
273
|
"uniqueItems": true,
|
|
196
274
|
"items": { "$ref": "#/$defs/attachmentSummary" }
|
|
197
275
|
},
|
|
276
|
+
"usage": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"additionalProperties": false,
|
|
279
|
+
"required": ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens"],
|
|
280
|
+
"properties": {
|
|
281
|
+
"inputTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
|
|
282
|
+
"outputTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
|
|
283
|
+
"cacheReadTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
|
|
284
|
+
"cacheWriteTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 },
|
|
285
|
+
"totalTokens": { "type": "integer", "minimum": 0, "maximum": 10000000000 }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
"model": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9._:@/-]{1,120}$" },
|
|
289
|
+
"failure": { "$ref": "#/$defs/companyFailure" },
|
|
290
|
+
"process": { "$ref": "#/$defs/harnessProcess" },
|
|
198
291
|
"toolCalls": {
|
|
199
292
|
"type": "array",
|
|
200
293
|
"maxItems": 64,
|