@piagent/platform 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +15 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +15 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +16 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +16 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/managed/call-purpose.mjs +3 -2
- package/packages/piagent-core/managed/command-tools.mjs +56 -0
- package/packages/piagent-core/managed/container-engine.mjs +68 -0
- package/packages/piagent-core/managed/continuation.mjs +131 -0
- package/packages/piagent-core/managed/doctor.mjs +12 -0
- package/packages/piagent-core/managed/helper-roles.mjs +1 -1
- package/packages/piagent-core/managed/helper-run.mjs +24 -6
- package/packages/piagent-core/managed/language-environment.mjs +57 -0
- package/packages/piagent-core/managed/linux-sandbox.mjs +6 -1
- package/packages/piagent-core/managed/package-proxy.mjs +181 -0
- package/packages/piagent-core/managed/patch-snapshot.mjs +53 -9
- package/packages/piagent-core/managed/repository-operation.mjs +20 -0
- package/packages/piagent-core/managed/review-snapshot-worker.mjs +29 -4
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +7 -2
- package/packages/piagent-core/managed/session.mjs +60 -67
- package/packages/piagent-core/managed/tool-boundary.mjs +70 -11
- package/packages/piagent-core/managed/tool-worker.mjs +18 -3
- package/packages/piagent-core/managed/workflow.mjs +20 -20
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DlbuyiQ3.js → MarkdownMessage-nggiOt0N.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-BGxsZHB5.js → index-CM9cs-gP.js} +16 -14
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/company-relay.ts +14 -2
- package/packages/piagent-webui/gateway/gateway-service.ts +20 -8
- package/packages/piagent-webui/gateway/git-repositories.ts +46 -0
- package/packages/piagent-webui/gateway/managed-projection.ts +9 -2
- package/packages/piagent-webui/gateway/project-registry.ts +10 -6
- package/packages/piagent-webui/gateway/session-catalog.ts +13 -6
- package/packages/piagent-webui/server/loopback-server.ts +1 -1
- package/packages/piagent-webui/server/member-routes.ts +6 -3
- package/packages/piagent-webui/server/tool-call-summary.ts +3 -1
- package/packages/piagent-webui/server/transcript-projection.ts +20 -4
- package/packages/piagent-webui/shared/process-copy.ts +3 -1
- package/packages/piagent-webui/shared/turn-end.ts +68 -0
- package/schemas/piagent-webui/session-catalog-v1.schema.json +11 -1
- package/schemas/piagent-webui/transcript-v1.schema.json +23 -2
- package/scripts/company-terminal.mjs +15 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
|
|
8
8
|
<meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
|
|
9
9
|
<title>Piagent WebUI</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CM9cs-gP.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -4,6 +4,8 @@ import { WebSocket } from "ws";
|
|
|
4
4
|
import type { Catalog, SessionRow } from "../contracts/generated/session-catalog-v1.ts";
|
|
5
5
|
import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
6
6
|
import { requestGatewayControl } from "./control-socket.ts";
|
|
7
|
+
import { gitHeadReader } from "./git-head.ts";
|
|
8
|
+
import { childRepositoryReader } from "./git-repositories.ts";
|
|
7
9
|
import type { GatewayEventKind, GatewayEventStore } from "./gateway-events.ts";
|
|
8
10
|
|
|
9
11
|
// The personal dashboard lists and drives company sessions through this relay.
|
|
@@ -250,12 +252,22 @@ export class CompanyRelay {
|
|
|
250
252
|
const company = await this.catalog();
|
|
251
253
|
if (personal.state !== "ready" || !personal.catalogRevision) return personal;
|
|
252
254
|
const ready = this.#state === "ready";
|
|
255
|
+
// The Git facts of a company conversation's folder are read here, on the
|
|
256
|
+
// same Mac: the company Gateway may run an earlier release (Agent Watch
|
|
257
|
+
// restarts it only now and then) that does not report them.
|
|
258
|
+
const readHead = gitHeadReader(), readRepositories = childRepositoryReader();
|
|
253
259
|
const rows: SessionRow[] = (company?.state === "ready" ? company.sessions : []).map((row) => {
|
|
254
260
|
const folder = this.#paths.get(row.projectRef);
|
|
255
|
-
|
|
261
|
+
const { gitBranch: _branch, gitRepositories: _repositories, ...rest } = row;
|
|
262
|
+
const head = folder ? readHead(folder) : null, repositories = folder && !head ? readRepositories(folder) : [];
|
|
263
|
+
return { ...rest, projectRef: folder ? this.#hubProject(folder) : row.projectRef, composerAvailable: ready && row.composerAvailable,
|
|
264
|
+
...(folder ? {} : _branch ? { gitBranch: _branch } : {}), ...(folder ? {} : _repositories ? { gitRepositories: _repositories } : {}),
|
|
265
|
+
...(head ? { gitBranch: head } : {}),
|
|
266
|
+
...(repositories.length ? { gitRepositories: repositories.map((repository) => ({ name: repository.name, branch: repository.head })) } : {}) };
|
|
256
267
|
});
|
|
257
268
|
const sessions = [...personal.sessions, ...rows].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
258
|
-
const digest = createHash("sha256").update(JSON.stringify([personal.catalogRevision, company?.catalogRevision ?? null, ready
|
|
269
|
+
const digest = createHash("sha256").update(JSON.stringify([personal.catalogRevision, company?.catalogRevision ?? null, ready,
|
|
270
|
+
rows.map((row) => [row.gitBranch ?? null, row.gitRepositories ?? null])])).digest("hex");
|
|
259
271
|
return { ...personal, catalogRevision: `rev_${digest}`, sessions,
|
|
260
272
|
page: { ...personal.page, returned: sessions.length, total: personal.page.total + rows.length } };
|
|
261
273
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { buildSessionCatalog, projectRefForCwd } from "./session-catalog.ts";
|
|
22
22
|
import { suggestPaths } from "./path-suggestions.ts";
|
|
23
23
|
import { GitBranchError, listBranches, switchBranch, type SwitchRequest } from "./git-branches.ts";
|
|
24
|
+
import { childRepositories, childRepositoryFolder } from "./git-repositories.ts";
|
|
24
25
|
import { listAgentCommands } from "../../piagent-core/runtime/resources/agent-resources.mjs";
|
|
25
26
|
import { sessionRefForPath } from "../ownership/session-refs.ts";
|
|
26
27
|
import { SessionMetadataStore } from "./session-metadata-store.ts";
|
|
@@ -309,25 +310,36 @@ export async function startPiagentGateway(options: {
|
|
|
309
310
|
// The project's Git branches and a switch between them. A switch waits
|
|
310
311
|
// until no conversation in the folder runs (company ones included): the
|
|
311
312
|
// agent would find its files changed under it.
|
|
312
|
-
|
|
313
|
+
// A folder that is not a repository answers with the repositories one
|
|
314
|
+
// level down; `repository` then names the one to read or switch.
|
|
315
|
+
listBranches: async (projectRef, repository) => {
|
|
313
316
|
const root = await projectFolder(projectRef);
|
|
314
317
|
if (!root) return null;
|
|
315
|
-
const
|
|
316
|
-
|
|
318
|
+
const folder = repository === null ? root : childRepositoryFolder(root, repository);
|
|
319
|
+
if (!folder) throw new GitBranchError("not-a-git-repository");
|
|
320
|
+
const [list, running] = await Promise.all([listBranches(folder), runningInProject(projectRef)]);
|
|
321
|
+
if (list) return { ...list, running, ...(repository === null ? {} : { name: repository }) };
|
|
322
|
+
const repositories = repository === null ? await Promise.all(childRepositories(root).map(async (child) => {
|
|
323
|
+
const branches = await listBranches(path.join(root, child.name));
|
|
324
|
+
return { name: child.name, head: child.head, changedFiles: branches?.changedFiles ?? 0 };
|
|
325
|
+
})) : [];
|
|
326
|
+
return { repository: false, running, repositories };
|
|
317
327
|
},
|
|
318
328
|
switchBranch: async (projectRef, request) => {
|
|
319
329
|
const root = await projectFolder(projectRef);
|
|
320
330
|
if (!root) return null;
|
|
321
|
-
const value = request as Partial<SwitchRequest> | null;
|
|
331
|
+
const value = request as (Partial<SwitchRequest> & { repository?: string }) | null;
|
|
322
332
|
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.branch !== "string"
|
|
323
|
-
|| Object.keys(value).some((name) => !["branch", "create", "remote"].includes(name))
|
|
333
|
+
|| Object.keys(value).some((name) => !["branch", "create", "remote", "repository"].includes(name))
|
|
334
|
+
|| (value.repository !== undefined && typeof value.repository !== "string")
|
|
324
335
|
|| (value.create !== undefined && typeof value.create !== "boolean")
|
|
325
336
|
|| (value.remote !== undefined && value.remote !== null && typeof value.remote !== "string")) throw new GitBranchError("branch-request-invalid");
|
|
326
|
-
|
|
337
|
+
const folder = value.repository === undefined ? root : childRepositoryFolder(root, value.repository);
|
|
338
|
+
if (!folder || !(await listBranches(folder))) throw new GitBranchError("not-a-git-repository");
|
|
327
339
|
if (await runningInProject(projectRef) > 0) throw new GitBranchError("branch-switch-blocked-running");
|
|
328
|
-
const head = await switchBranch(
|
|
340
|
+
const head = await switchBranch(folder, { branch: value.branch, create: value.create === true, remote: value.remote ?? null });
|
|
329
341
|
events.publish("catalog.changed", { reasonCode: "git-branch-switched" });
|
|
330
|
-
return { head, branches: await listBranches(
|
|
342
|
+
return { head, branches: await listBranches(folder) };
|
|
331
343
|
},
|
|
332
344
|
updates: { status: () => updates!.status(), check: () => updates!.check(), apply: (request) => updates!.apply(request) },
|
|
333
345
|
answerSessionQuestion: (sessionRef, questionRef, answer) => company(sessionRef)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readGitHead, type GitHead } from "./git-head.ts";
|
|
5
|
+
|
|
6
|
+
// A project folder that is not itself in a repository may hold several (a
|
|
7
|
+
// workspace folder with the front end, the back end and other projects side
|
|
8
|
+
// by side, each its own repository). Like an editor's source control view,
|
|
9
|
+
// the dashboard looks one level down for them: each child folder with its
|
|
10
|
+
// own `.git` (a folder, or a worktree's file) is a repository, shown with
|
|
11
|
+
// its branch. Hidden folders and dependency folders are skipped.
|
|
12
|
+
export type ChildRepository = { name: string; path: string; head: GitHead | null };
|
|
13
|
+
|
|
14
|
+
const MAX_REPOSITORIES = 30;
|
|
15
|
+
const SKIPPED = new Set(["node_modules", "vendor", "target", "dist", "build", "Pods", "venv", "__pycache__"]);
|
|
16
|
+
|
|
17
|
+
export function childRepositories(cwd: string): ChildRepository[] {
|
|
18
|
+
if (!cwd || readGitHead(cwd)) return [];
|
|
19
|
+
let entries: fs.Dirent[];
|
|
20
|
+
try { entries = fs.readdirSync(cwd, { withFileTypes: true }); } catch { return []; }
|
|
21
|
+
const found: ChildRepository[] = [];
|
|
22
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
23
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || SKIPPED.has(entry.name)) continue;
|
|
24
|
+
const folder = path.join(cwd, entry.name);
|
|
25
|
+
try { fs.lstatSync(path.join(folder, ".git")); } catch { continue; }
|
|
26
|
+
found.push({ name: entry.name, path: entry.name, head: readGitHead(folder) });
|
|
27
|
+
if (found.length === MAX_REPOSITORIES) break;
|
|
28
|
+
}
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The folder of one child repository, or null when `name` is not one: a
|
|
33
|
+
// single folder name, never a path that climbs out of the project.
|
|
34
|
+
export function childRepositoryFolder(cwd: string, name: string): string | null {
|
|
35
|
+
if (!name || name.length > 255 || name.startsWith(".") || name.includes("/") || name.includes("\\") || /[\u0000-\u001f\u007f]/.test(name)) return null;
|
|
36
|
+
return childRepositories(cwd).some((repository) => repository.name === name) ? path.join(cwd, name) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// One read per project folder while a catalog is built.
|
|
40
|
+
export function childRepositoryReader(): (cwd: string) => ChildRepository[] {
|
|
41
|
+
const seen = new Map<string, ChildRepository[]>();
|
|
42
|
+
return (cwd) => {
|
|
43
|
+
if (!seen.has(cwd)) seen.set(cwd, childRepositories(cwd));
|
|
44
|
+
return seen.get(cwd)!;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { turnEndOf } from '../shared/turn-end.ts';
|
|
1
2
|
// Public managed state contains no provider/model route or credential.
|
|
2
|
-
const STATUS = ['pending', 'in_progress', 'completed'];
|
|
3
|
+
const STATUS = ['pending', 'in_progress', 'completed', 'deferred'];
|
|
3
4
|
const OUTCOMES = ['no_change', 'interrupted', 'disputed', 'blocking_open', 'unverified', 'review_unavailable', 'unreviewed', 'clean'];
|
|
4
5
|
export function managedProjection(context: any, entries: any[]) {
|
|
5
6
|
if (context?.model?.provider !== 'agent_watch_managed') return {};
|
|
@@ -14,6 +15,12 @@ export function managedProjection(context: any, entries: any[]) {
|
|
|
14
15
|
const final = entries.filter(e => e.type === 'custom_message' && e.customType === 'agent-watch-process' && e.details?.phase === 'final').at(-1)?.details;
|
|
15
16
|
const process = final && OUTCOMES.includes(final.outcome) ? { outcome: final.outcome, verified: final.verified === true, reviewed: final.reviewed === true,
|
|
16
17
|
blockingOpen: Number.isInteger(final.blockingOpen) && final.blockingOpen >= 0 ? Math.min(final.blockingOpen, 1000) : 0, ...(final.planSkipped === true ? { planSkipped: true as const } : {}) } : null;
|
|
18
|
+
// How the member's last message ended, unless a newer message is running.
|
|
19
|
+
const lastIndex = (match: (e: any) => boolean) => { for (let i = entries.length - 1; i >= 0; i -= 1) if (match(entries[i])) return i; return -1; };
|
|
20
|
+
const lastUser = lastIndex(e => e.type === 'message' && e.message?.role === 'user');
|
|
21
|
+
const endAt = lastIndex(e => e.type === 'custom' && e.customType === 'agent-watch-turn-end');
|
|
22
|
+
const end = endAt > lastUser ? turnEndOf(entries[endAt].data) : null;
|
|
23
|
+
const turnEnd = end ? { state: end.state, planSteps: end.planSteps, planDone: end.planDone, ...(end.planDeferred ? { planDeferred: end.planDeferred } : {}), ...(end.reason ? { reason: end.reason } : {}) } : null;
|
|
17
24
|
// Helpers the Harness enables (up to four); a conversation recorded before
|
|
18
25
|
// that was known says two.
|
|
19
26
|
const maximum = Number.isInteger(counter?.maximum) && counter.maximum >= 0 && counter.maximum <= 4 ? counter.maximum : 2;
|
|
@@ -21,5 +28,5 @@ export function managedProjection(context: any, entries: any[]) {
|
|
|
21
28
|
const permission = last('agent-watch-permission')?.mode === 'trusted-full-access' ? 'trusted-full-access' : 'workspace-write';
|
|
22
29
|
return {modelLabel:'agent-watch-auto', managedPermission: permission, managedHelpers:{active:Number.isInteger(counter?.active) && counter.active>=0 && counter.active<=maximum ? counter.active : 0, maximum},
|
|
23
30
|
...(Array.isArray(capabilities)?{managedThinkingLevels:levels.filter(level=>capabilities.includes(level))}:{}),
|
|
24
|
-
...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {})};
|
|
31
|
+
...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {}), ...(turnEnd ? {managedTurnEnd:turnEnd} : {})};
|
|
25
32
|
}
|
|
@@ -78,12 +78,16 @@ export class ProjectRegistry {
|
|
|
78
78
|
if (!stat.isDirectory() || stat.isSymbolicLink() || cwd === path.parse(cwd).root) throw new Error("project-import-folder-invalid");
|
|
79
79
|
const current = this.#read(), projectRef = projectRefForCwd(this.#key, cwd);
|
|
80
80
|
const existing = current.projects.find((item) => item.projectRef === projectRef);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
// Added again: it moves to the newest place, so it outlives older folders.
|
|
82
|
+
const others = current.projects.filter((item) => item.projectRef !== projectRef);
|
|
83
|
+
const item = { projectRef, cwd, label: existing?.label ?? cleanLabel(path.basename(cwd)), importedAt: now.toISOString() };
|
|
84
|
+
// The list is the menu's memory of added folders, not where conversations
|
|
85
|
+
// live (their folders come from the sessions): a full list makes room by
|
|
86
|
+
// forgetting folders gone from the disk, then the folders added longest ago.
|
|
87
|
+
const kept = others.length < MAX_PROJECTS ? others : others.filter((other) => {
|
|
88
|
+
try { return fs.realpathSync(other.cwd) === other.cwd && fs.lstatSync(other.cwd).isDirectory(); } catch { return false; }
|
|
89
|
+
}).sort((left, right) => Date.parse(left.importedAt) - Date.parse(right.importedAt)).slice(-(MAX_PROJECTS - 1));
|
|
90
|
+
this.#write({ ...current, projects: [...kept, item] });
|
|
87
91
|
return { projectRef: item.projectRef, placeRef: item.projectRef, label: item.label };
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -7,6 +7,7 @@ import type { Catalog, SessionRow } from "../contracts/generated/session-catalog
|
|
|
7
7
|
import type { MetadataSnapshot, SessionMetadata } from "./session-metadata-store.ts";
|
|
8
8
|
import { projectRefForCwd, sessionRefForPath } from "../ownership/session-refs.ts";
|
|
9
9
|
import { gitHeadReader, type GitHead } from "./git-head.ts";
|
|
10
|
+
import { childRepositoryReader, type ChildRepository } from "./git-repositories.ts";
|
|
10
11
|
|
|
11
12
|
export { projectRefForCwd, sessionRefForPath } from "../ownership/session-refs.ts";
|
|
12
13
|
|
|
@@ -48,7 +49,7 @@ function catalogRef(key: Buffer, namespace: string, value: string): string {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export type SessionOwnerProjection = Pick<SessionRow, "state" | "liveState" | "composerAvailable" | "needsAttention" | "owner" | "reasonCode">;
|
|
51
|
-
export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedPlan" | "managedProcess">;
|
|
52
|
+
export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedPlan" | "managedProcess" | "managedTurnEnd">;
|
|
52
53
|
|
|
53
54
|
function display(value: unknown, maximum: number, fallback: string): string {
|
|
54
55
|
const clean = redactSensitiveText(String(value ?? "")).text
|
|
@@ -106,8 +107,11 @@ function revision(key: Buffer, value: unknown): string {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undefined, metadataReason: string | null,
|
|
109
|
-
ownership?: SessionOwnerProjection, sessionOptions?: SessionOptionProjection, gitHead?: GitHead | null): SessionRow {
|
|
110
|
+
ownership?: SessionOwnerProjection, sessionOptions?: SessionOptionProjection, gitHead?: GitHead | null, repositories: ChildRepository[] = []): SessionRow {
|
|
110
111
|
const gitBranch = gitHead ? { name: display(gitHead.name, 200, "HEAD"), detached: gitHead.detached } : null;
|
|
112
|
+
// A folder that is not a repository but holds some (front end, back end…).
|
|
113
|
+
const gitRepositories = gitBranch ? [] : repositories.map((repository) => ({ name: display(repository.name, 255, "repository").replace(/[/\\]/g, "-"),
|
|
114
|
+
branch: repository.head ? { name: display(repository.head.name, 200, "HEAD"), detached: repository.head.detached } : null }));
|
|
111
115
|
const projectLabel = display(info.cwd ? path.basename(info.cwd) : "", 120, "Unknown project");
|
|
112
116
|
const projected = projectedSessionTitle(info);
|
|
113
117
|
return {
|
|
@@ -126,12 +130,14 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
|
|
|
126
130
|
composerAvailable: metadata?.archived ? false : ownership?.composerAvailable ?? false,
|
|
127
131
|
needsAttention: metadata?.archived ? false : ownership?.needsAttention ?? false,
|
|
128
132
|
...(gitBranch ? { gitBranch } : {}),
|
|
133
|
+
...(gitRepositories.length ? { gitRepositories } : {}),
|
|
129
134
|
...(sessionOptions?.managedHelpers ? {managedHelpers:{...sessionOptions.managedHelpers,
|
|
130
135
|
active: ownership?.liveState === 'idle' ? 0 : ownership?.liveState === 'running' || ownership?.liveState === 'waiting-approval' ? sessionOptions.managedHelpers.active : null}} : {}),
|
|
131
136
|
...(sessionOptions?.managedThinkingLevels ? {managedThinkingLevels:sessionOptions.managedThinkingLevels} : {}),
|
|
132
137
|
...(sessionOptions?.managedPermission ? {managedPermission:sessionOptions.managedPermission} : {}),
|
|
133
138
|
...(sessionOptions?.managedPlan ? {managedPlan:sessionOptions.managedPlan} : {}),
|
|
134
139
|
...(sessionOptions?.managedProcess ? {managedProcess:sessionOptions.managedProcess} : {}),
|
|
140
|
+
...(sessionOptions?.managedTurnEnd ? {managedTurnEnd:sessionOptions.managedTurnEnd} : {}),
|
|
135
141
|
modelLabel: sessionOptions?.modelLabel ?? null,
|
|
136
142
|
thinkingLevel: sessionOptions?.thinkingLevel ?? "unknown",
|
|
137
143
|
contextUsage: { usedTokens: null, contextWindow: null, ratio: null, state: "unknown" },
|
|
@@ -140,8 +146,8 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
|
|
|
140
146
|
? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "released" }
|
|
141
147
|
: ownership?.owner ?? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
|
|
142
148
|
sessionRevision: revision(key, [info.path, info.modified.toISOString(), info.messageCount, info.name ?? null, metadata?.revision ?? null,
|
|
143
|
-
sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess,
|
|
144
|
-
gitBranch,
|
|
149
|
+
sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd,
|
|
150
|
+
gitBranch, gitRepositories,
|
|
145
151
|
metadata?.archived ? "archived" : ownership ? [ownership.state, ownership.liveState, ownership.owner.kind,
|
|
146
152
|
ownership.owner.ownerEpoch, ownership.owner.runtimeInstanceRef, ownership.reasonCode] : "offline"]),
|
|
147
153
|
reasonCode: metadata?.archived ? metadataReason : ownership?.reasonCode ?? metadataReason
|
|
@@ -156,17 +162,18 @@ export async function buildSessionCatalog(options: {
|
|
|
156
162
|
readOwnership?(sessionRef: string): SessionOwnerProjection;
|
|
157
163
|
readSessionOptions?(info: PiSessionInfo): SessionOptionProjection;
|
|
158
164
|
readGitHead?(cwd: string): GitHead | null;
|
|
165
|
+
readChildRepositories?(cwd: string): ChildRepository[];
|
|
159
166
|
limit?: number;
|
|
160
167
|
}): Promise<Catalog> {
|
|
161
168
|
const limit = Math.max(1, Math.min(200, options.limit ?? 200));
|
|
162
169
|
try {
|
|
163
170
|
const found = (await options.listSessions()).filter(isUserConversationSession);
|
|
164
|
-
const readHead = options.readGitHead ?? gitHeadReader();
|
|
171
|
+
const readHead = options.readGitHead ?? gitHeadReader(), readRepositories = options.readChildRepositories ?? childRepositoryReader();
|
|
165
172
|
const metadata = options.readMetadata?.() ?? { state: "ready", revision: null, sessions: new Map(), reasonCode: null };
|
|
166
173
|
const sessions = found.map((info) => {
|
|
167
174
|
const sessionRef = sessionRefForPath(options.key, info.path);
|
|
168
175
|
return row(options.key, info, metadata.sessions.get(sessionRef), metadata.reasonCode, options.readOwnership?.(sessionRef),
|
|
169
|
-
options.readSessionOptions?.(info), readHead(info.cwd));
|
|
176
|
+
options.readSessionOptions?.(info), readHead(info.cwd), readRepositories(info.cwd));
|
|
170
177
|
}).sort((left, right) => Number(right.pinned) - Number(left.pinned)
|
|
171
178
|
|| Date.parse(right.updatedAt) - Date.parse(left.updatedAt)).slice(0, limit);
|
|
172
179
|
return {
|
|
@@ -72,7 +72,7 @@ export async function startLoopbackServer(options: {
|
|
|
72
72
|
answerSessionQuestion?: (sessionRef: string, questionRef: string, answer: unknown) => unknown | Promise<unknown>;
|
|
73
73
|
suggestPaths?: (projectRef: string, query: string) => unknown | Promise<unknown>;
|
|
74
74
|
listCommands?: (projectRef: string) => unknown | Promise<unknown>;
|
|
75
|
-
listBranches?: (projectRef: string) => unknown | Promise<unknown>;
|
|
75
|
+
listBranches?: (projectRef: string, repository: string | null) => unknown | Promise<unknown>;
|
|
76
76
|
switchBranch?: (projectRef: string, request: unknown) => unknown | Promise<unknown>;
|
|
77
77
|
updates?: UpdateRoutes;
|
|
78
78
|
bootstrapTtlMs?: number;
|
|
@@ -37,7 +37,7 @@ type Options = {
|
|
|
37
37
|
listCommands?: (projectRef: string) => unknown | Promise<unknown>;
|
|
38
38
|
// The project's Git branches, and a switch to one of them (or a new one);
|
|
39
39
|
// null as above.
|
|
40
|
-
listBranches?: (projectRef: string) => unknown | Promise<unknown>;
|
|
40
|
+
listBranches?: (projectRef: string, repository: string | null) => unknown | Promise<unknown>;
|
|
41
41
|
switchBranch?: (projectRef: string, request: unknown) => unknown | Promise<unknown>;
|
|
42
42
|
updates?: UpdateRoutes;
|
|
43
43
|
};
|
|
@@ -111,9 +111,12 @@ export async function routeMemberRequest(context: Context, options: Options): Pr
|
|
|
111
111
|
const projectRef = decodeURIComponent(branches[1]);
|
|
112
112
|
if (request.method === "GET") {
|
|
113
113
|
if (!auth.authenticate(request)) { errorResponse(response, 401, "authentication-required"); return true; }
|
|
114
|
-
|
|
114
|
+
const repository = url.searchParams.get("repository");
|
|
115
|
+
if (!CURSOR.test(projectRef) || repository !== null && (repository.length === 0 || repository.length > 255 || /[\x00-\x1f/\\]/.test(repository))) {
|
|
116
|
+
errorResponse(response, 400, "invalid-project-ref"); return true;
|
|
117
|
+
}
|
|
115
118
|
try {
|
|
116
|
-
const value = await options.listBranches!(projectRef);
|
|
119
|
+
const value = await options.listBranches!(projectRef, repository);
|
|
117
120
|
if (value === null) errorResponse(response, 404, "project-not-found"); else jsonResponse(response, 200, value);
|
|
118
121
|
} catch { errorResponse(response, 503, "branches-unavailable"); }
|
|
119
122
|
return true;
|
|
@@ -8,7 +8,7 @@ import { redactSensitiveText } from "../../piagent-core/extensions/redaction-cor
|
|
|
8
8
|
// and a bounded result preview. Every string is ANSI-stripped and redacted;
|
|
9
9
|
// raw arguments and results never leave the server.
|
|
10
10
|
export type ToolKind = "read" | "write" | "edit" | "command" | "search" | "list" | "web-search" | "web-fetch"
|
|
11
|
-
| "subagent" | "network" | "git" | "plan" | "other";
|
|
11
|
+
| "subagent" | "network" | "docker" | "git" | "plan" | "other";
|
|
12
12
|
export type ToolSummary = { kind: ToolKind; target: string | null; detail: string | null };
|
|
13
13
|
export type ToolChange = { added: number; removed: number; preview: string; truncated: boolean };
|
|
14
14
|
export type ToolResult = { text: string; truncated: boolean; isError: boolean };
|
|
@@ -139,6 +139,8 @@ export function summarizeToolCall(name: unknown, input: unknown, cwd?: string):
|
|
|
139
139
|
}
|
|
140
140
|
case "run_with_network":
|
|
141
141
|
return { summary: { kind: "network", target: commandParts(args.command, cwd).target, detail: clean(args.reason, DETAIL_CHARS) } };
|
|
142
|
+
case "run_with_docker":
|
|
143
|
+
return { summary: { kind: "docker", target: commandParts(args.command, cwd).target, detail: clean(args.reason, DETAIL_CHARS) } };
|
|
142
144
|
case "grep": case "find": {
|
|
143
145
|
const where = [displayPath(args.path, cwd), clean(args.glob, 120)].filter(Boolean).join(" · ");
|
|
144
146
|
return { summary: { kind: "search", target: clean(args.pattern, TARGET_CHARS), detail: where || null } };
|
|
@@ -5,6 +5,7 @@ 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 { turnEndOf, type TurnEnd } from "../shared/turn-end.ts";
|
|
8
9
|
import { effectiveStopReason } from "./message-stop-reason.ts";
|
|
9
10
|
import { WEBUI_MESSAGE_CORRELATION_ENTRY_TYPE, webUiMessageCorrelationEntry,
|
|
10
11
|
type WebUiMessageCorrelation } from "../shared/message-correlation.ts";
|
|
@@ -33,11 +34,11 @@ type TranscriptItem = { messageRef: string; parentMessageRef: string | null; rol
|
|
|
33
34
|
agentOperationId: string | null; messageRequestId?: string; turnIndex: number | null; content: TranscriptContent;
|
|
34
35
|
attachments?: TranscriptAttachment[];
|
|
35
36
|
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
|
+
summary?: ToolSummary; change?: ToolChange; result?: ToolResult }>; usage?: TurnUsage; model?: string; process?: HarnessProcess; turnEnd?: TurnEnd };
|
|
37
38
|
type HarnessFinding = { severity: "blocking" | "major" | "minor"; file: string; line: number | null; issue: string };
|
|
38
39
|
type HarnessIssue = { kind: "wrong_premise" | "conflicts_with_request" | "ambiguous" | "out_of_scope"; detail: string };
|
|
39
|
-
type HarnessProcess = { phase: "plan" | "verify" | "review" | "final" | "objection" | "answer" | "claims" | "rejudge" | "dispute"; outcome?: string; loop?: number; maxLoops?: number;
|
|
40
|
-
verified?: boolean; reviewed?: boolean; blockingOpen?: number; planOpen?: number; planSkipped?: true; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string;
|
|
40
|
+
type HarnessProcess = { phase: "plan" | "verify" | "review" | "final" | "objection" | "answer" | "claims" | "rejudge" | "dispute" | "continue"; outcome?: string; loop?: number; maxLoops?: number;
|
|
41
|
+
verified?: boolean; reviewed?: boolean; blockingOpen?: number; planOpen?: number; planDone?: number; planSteps?: number; round?: number; maxRounds?: number; idle?: number; planSkipped?: true; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string;
|
|
41
42
|
findings?: HarnessFinding[]; role?: "scout" | "research" | "verify" | "review"; by?: "main" | "harness"; disputes?: number; unchanged?: true; issues?: HarnessIssue[]; claims?: { claim: string }[] };
|
|
42
43
|
export type TranscriptDocument = { schemaVersion: 1; version: "piagent-webui-transcript-v1"; generatedAt: string; identity: TranscriptIdentity;
|
|
43
44
|
revision: TranscriptRevision; eventCursor: string; state: "ready" | "unavailable"; items: TranscriptItem[];
|
|
@@ -173,7 +174,8 @@ function toolCalls(message: any, sessionRef: string, role: TranscriptItem["role"
|
|
|
173
174
|
// agent before a code-changing turn ended, and how that turn ended.
|
|
174
175
|
const PROCESS_OUTCOMES = ["no_change", "interrupted", "disputed", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"];
|
|
175
176
|
// A helper's objection to its brief, a failed claim, a disagreement handed to the member.
|
|
176
|
-
|
|
177
|
+
// "continue": the harness sent the agent back to a checklist with open steps.
|
|
178
|
+
const PROCESS_PHASES = ["plan", "verify", "review", "final", "objection", "answer", "claims", "rejudge", "dispute", "continue"];
|
|
177
179
|
const ISSUE_KINDS = ["wrong_premise", "conflicts_with_request", "ambiguous", "out_of_scope"], HELPER_ROLES = ["scout", "research", "verify", "review"];
|
|
178
180
|
const POLICY_MODES = ["off", "suggest", "require"];
|
|
179
181
|
function harnessProcess(details: any): HarnessProcess | null {
|
|
@@ -187,6 +189,12 @@ function harnessProcess(details: any): HarnessProcess | null {
|
|
|
187
189
|
if (typeof details.reviewed === "boolean") out.reviewed = details.reviewed;
|
|
188
190
|
if (Number.isInteger(details.blockingOpen) && details.blockingOpen >= 0) out.blockingOpen = Math.min(details.blockingOpen, 1000);
|
|
189
191
|
if (Number.isInteger(details.planOpen) && details.planOpen >= 1 && details.planOpen <= 30) out.planOpen = details.planOpen;
|
|
192
|
+
const upTo = (n: unknown, low: number, high: number): n is number => Number.isInteger(n) && (n as number) >= low && (n as number) <= high;
|
|
193
|
+
if (upTo(details.planDone, 0, 30)) out.planDone = details.planDone;
|
|
194
|
+
if (upTo(details.planSteps, 1, 30)) out.planSteps = details.planSteps;
|
|
195
|
+
if (upTo(details.round, 1, 30)) out.round = details.round;
|
|
196
|
+
if (upTo(details.maxRounds, 1, 30)) out.maxRounds = details.maxRounds;
|
|
197
|
+
if (upTo(details.idle, 1, 2)) out.idle = details.idle;
|
|
190
198
|
if (details.planSkipped === true) out.planSkipped = true;
|
|
191
199
|
if (POLICY_MODES.includes(details.policy?.verify)) out.verifyPolicy = details.policy.verify;
|
|
192
200
|
if (POLICY_MODES.includes(details.policy?.review)) out.reviewPolicy = details.policy.review;
|
|
@@ -262,6 +270,14 @@ function correlatedTranscriptItems(entries: unknown[], identity: TranscriptIdent
|
|
|
262
270
|
pending = webUiMessageCorrelationEntry(entry);
|
|
263
271
|
continue;
|
|
264
272
|
}
|
|
273
|
+
// How the member's message ended: an entry, not a message the model reads.
|
|
274
|
+
if (entry?.type === "custom" && entry.customType === "agent-watch-turn-end") {
|
|
275
|
+
const turnEnd = turnEndOf(entry.data);
|
|
276
|
+
const projected = turnEnd && item({ ...entry, type: "message", message: { role: "custom", content: "" } }, identity, true);
|
|
277
|
+
if (projected) values.push({ entry, item: { ...projected, turnEnd, ...(turn ? { agentOperationId: turn.operationRef, messageRequestId: turn.messageRequestId } : {}) },
|
|
278
|
+
cursor: opaque("transcript", [identity.sessionRef, entry.id]) });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
265
281
|
if (entry?.type === "custom_message" && entry.customType === "agent-watch-process") {
|
|
266
282
|
const process = harnessProcess(entry.details);
|
|
267
283
|
const projected = process && item({ ...entry, type: "message", message: { role: "custom", content: entry.content } }, identity, true);
|
|
@@ -4,7 +4,7 @@ import { failureRoleText, type CopyLocale, type FailureRole } from "./company-co
|
|
|
4
4
|
|
|
5
5
|
export type ProcessCopyInput = {
|
|
6
6
|
phase?: string; outcome?: string; loop?: number; maxLoops?: number; verified?: boolean; reviewed?: boolean; blockingOpen?: number;
|
|
7
|
-
planOpen?: number; planSkipped?: boolean; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string; role?: FailureRole;
|
|
7
|
+
planOpen?: number; planDone?: number; planSteps?: number; round?: number; maxRounds?: number; idle?: number; planSkipped?: boolean; verifyPolicy?: string; reviewPolicy?: string; reviewUnavailable?: string; role?: FailureRole;
|
|
8
8
|
disputes?: number; unchanged?: boolean;
|
|
9
9
|
findings?: Array<{ severity: string; file?: string; line?: number | null; issue: string }>;
|
|
10
10
|
claims?: Array<{ claim: string }>; issues?: Array<{ kind: string; detail: string }>;
|
|
@@ -41,6 +41,8 @@ export function headlineText(process: ProcessCopyInput, locale: CopyLocale): str
|
|
|
41
41
|
const round = process.loop && process.maxLoops ? pick(locale, ` (vòng ${process.loop}/${process.maxLoops})`, ` (round ${process.loop}/${process.maxLoops})`) : "";
|
|
42
42
|
const who = failureRoleText(process.role ?? "review", locale), count = (process.findings ?? process.claims ?? process.issues ?? []).length;
|
|
43
43
|
switch (process.phase) {
|
|
44
|
+
case "continue": return pick(locale, `Harness: checklist còn ${process.planOpen ?? 0} bước (${process.planDone ?? 0}/${process.planSteps ?? 0} xong), cho main agent làm tiếp · vòng ${process.round ?? 1}${process.idle ? " (vòng trước không xong thêm bước nào)" : ""}`,
|
|
45
|
+
`Harness: the checklist has ${process.planOpen ?? 0} open step(s) (${process.planDone ?? 0}/${process.planSteps ?? 0} done); sent the main agent on · round ${process.round ?? 1}${process.idle ? " (the previous round finished no step)" : ""}`);
|
|
44
46
|
case "plan": return pick(locale, `Harness: plan còn ${process.planOpen ?? 0} bước chưa đánh dấu xong, yêu cầu agent cập nhật`, `Harness: ${process.planOpen ?? 0} plan step(s) not marked completed; asked the agent to update the plan`);
|
|
45
47
|
case "verify": return pick(locale, `Harness: chưa có check nào pass trên code hiện tại, yêu cầu agent chạy check${round}`, `Harness: no check has passed on the current code; asked the agent to run checks${round}`);
|
|
46
48
|
case "review": return pick(locale, `Harness: review tìm thấy ${count} lỗi blocking, gửi lại agent để fix hoặc giải thích${round}`, `Harness: the review found ${count} blocking issue(s); sent back to the agent to fix or answer${round}`);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// How a member's message ended in a company conversation ("agent-watch-turn-end"),
|
|
2
|
+
// read the same way by the transcript, the session list and the company
|
|
3
|
+
// Terminal: every checklist step done, stopped with steps open, failed (which
|
|
4
|
+
// harness role), or stopped by the member.
|
|
5
|
+
import { failureRoleText, type CopyLocale, type FailureRole } from "./company-copy.ts";
|
|
6
|
+
|
|
7
|
+
export type TurnEndState = "done" | "midway" | "failed" | "cancelled";
|
|
8
|
+
export type TurnEnd = { state: TurnEndState; planSteps: number; planDone: number; planDeferred?: number; rounds: number;
|
|
9
|
+
reason?: "idle" | "limit" | "policy"; role?: FailureRole; code?: string };
|
|
10
|
+
const ROLES: FailureRole[] = ["main", "scout", "research", "verify", "review"];
|
|
11
|
+
// The harness gives up after this many rounds in a row without progress, and
|
|
12
|
+
// runs at most this many (packages/piagent-core/managed/continuation.mjs).
|
|
13
|
+
export const IDLE_ROUNDS = 2, MAX_ROUNDS = 30;
|
|
14
|
+
|
|
15
|
+
// The recorded details, checked; null when they do not have the expected shape.
|
|
16
|
+
export function turnEndOf(details: any): TurnEnd | null {
|
|
17
|
+
const count = (n: unknown): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) <= 30;
|
|
18
|
+
if (!["done", "midway", "failed", "cancelled"].includes(details?.state) || !count(details.planSteps) || !count(details.planDone)
|
|
19
|
+
|| !count(details.rounds) || (details.planDeferred !== undefined && !count(details.planDeferred))
|
|
20
|
+
|| details.planDone + (details.planDeferred ?? 0) > details.planSteps) return null;
|
|
21
|
+
const out: TurnEnd = { state: details.state, planSteps: details.planSteps, planDone: details.planDone, rounds: details.rounds };
|
|
22
|
+
if (details.planDeferred) out.planDeferred = details.planDeferred;
|
|
23
|
+
if (details.state === "midway" && ["idle", "limit", "policy"].includes(details.reason)) out.reason = details.reason;
|
|
24
|
+
if (details.state === "failed") {
|
|
25
|
+
out.role = ROLES.includes(details.role) ? details.role : "main";
|
|
26
|
+
if (typeof details.code === "string" && /^[a-z0-9_]{1,64}$/.test(details.code)) out.code = details.code;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pick = (locale: CopyLocale, vi: string, en: string) => locale === "vi" ? vi : en;
|
|
32
|
+
export type TurnEndCopy = { tone: "success" | "warning" | "error" | "info"; title: string; text: string; continuable: boolean };
|
|
33
|
+
|
|
34
|
+
// What the member reads under the last answer: is the task finished, and if
|
|
35
|
+
// not, why the agent stopped and what to do.
|
|
36
|
+
export function turnEndCopy(end: TurnEnd, locale: CopyLocale): TurnEndCopy {
|
|
37
|
+
const later = end.planDeferred ?? 0, open = end.planSteps - end.planDone - later;
|
|
38
|
+
const rounds = end.rounds ? pick(locale, ` Harness đã cho agent làm tiếp ${end.rounds} vòng.`, ` The harness sent the agent back ${end.rounds} time(s).`) : "";
|
|
39
|
+
if (end.state === "cancelled") return { tone: "info", continuable: true, title: pick(locale, "Bạn đã dừng lượt này", "You stopped this turn"),
|
|
40
|
+
text: pick(locale, `Task chưa xong${end.planSteps ? ` (${end.planDone}/${end.planSteps} bước)` : ""}. Bấm Tiếp tục để agent làm tiếp từ chỗ dừng.`,
|
|
41
|
+
`The task is not finished${end.planSteps ? ` (${end.planDone}/${end.planSteps} steps)` : ""}. Choose Continue to pick up where it stopped.`) };
|
|
42
|
+
if (end.state === "failed") return { tone: "error", continuable: true,
|
|
43
|
+
title: pick(locale, `Lượt dừng vì lỗi · ${failureRoleText(end.role ?? "main", locale)}`, `Turn stopped by a failure · ${failureRoleText(end.role ?? "main", locale)}`),
|
|
44
|
+
text: pick(locale, `Task chưa xong${end.planSteps ? `: còn ${open}/${end.planSteps} bước` : ""}. Xem lỗi phía trên; khi đã xử lý, bấm Tiếp tục để chạy lại từ chỗ dừng.`,
|
|
45
|
+
`The task is not finished${end.planSteps ? `: ${open} of ${end.planSteps} steps open` : ""}. See the failure above; once it is resolved, choose Continue.`) + (end.code ? ` [${end.code}]` : "") };
|
|
46
|
+
if (end.state === "midway") {
|
|
47
|
+
const why = end.reason === "idle" ? pick(locale, `Main agent dừng ${IDLE_ROUNDS} vòng liên tiếp mà không xong thêm bước nào: đọc câu trả lời cuối để biết vướng gì.`,
|
|
48
|
+
`The main agent stopped ${IDLE_ROUNDS} rounds in a row without finishing a step: its last answer says what is in the way.`)
|
|
49
|
+
: end.reason === "limit" ? pick(locale, `Đã tới giới hạn ${MAX_ROUNDS} vòng tự làm tiếp cho một tin nhắn.`, `The limit of ${MAX_ROUNDS} automatic rounds for one message was reached.`)
|
|
50
|
+
: pick(locale, "Harness của team không bắt buộc plan nên không tự làm tiếp.", "The team's Harness does not require a plan, so it does not continue on its own.");
|
|
51
|
+
return { tone: "warning", continuable: true, title: pick(locale, `Dừng giữa chừng · còn ${open}/${end.planSteps} bước`, `Stopped midway · ${open} of ${end.planSteps} steps open`),
|
|
52
|
+
text: `${why}${rounds} ${pick(locale, "Bấm Tiếp tục để làm tiếp.", "Choose Continue to go on.")}` };
|
|
53
|
+
}
|
|
54
|
+
// Steps left for later at the member's word (or no longer needed): done as asked, not all done.
|
|
55
|
+
if (later) return { tone: "info", continuable: true, title: pick(locale, `Dừng theo yêu cầu · ${end.planDone}/${end.planSteps} bước xong, ${later} bước để sau`, `Stopped as asked · ${end.planDone} of ${end.planSteps} steps done, ${later} left for later`),
|
|
56
|
+
text: pick(locale, "Các bước còn lại được để sau theo yêu cầu hoặc vì không cần nữa: câu trả lời cuối nói lý do. Bấm Tiếp tục khi muốn làm nốt.", "The other steps were left for later as asked, or are no longer needed: the last answer says why. Choose Continue to do them.") + rounds };
|
|
57
|
+
return { tone: "success", continuable: false, title: end.planSteps ? pick(locale, `Hoàn thành · ${end.planSteps}/${end.planSteps} bước`, `Done · ${end.planSteps}/${end.planSteps} steps`) : pick(locale, "Xong lượt", "Turn finished"),
|
|
58
|
+
text: (end.planSteps ? pick(locale, "Mọi bước trong checklist đã xong.", "Every checklist step is completed.") : pick(locale, "Main agent đã trả lời xong; lượt này không có checklist.", "The main agent finished its answer; this turn had no checklist.")) + rounds };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The short label of the session list.
|
|
62
|
+
export function turnEndShort(end: Pick<TurnEnd, "state" | "planSteps" | "planDone" | "planDeferred">, locale: CopyLocale): string {
|
|
63
|
+
if (end.state === "done" && end.planDeferred) return pick(locale, `Xong ${end.planDone}/${end.planSteps}, để sau ${end.planDeferred}`, `Done ${end.planDone}/${end.planSteps}, ${end.planDeferred} later`);
|
|
64
|
+
if (end.state === "midway") return pick(locale, `Dừng giữa chừng ${end.planDone}/${end.planSteps}`, `Stopped ${end.planDone}/${end.planSteps}`);
|
|
65
|
+
if (end.state === "failed") return pick(locale, "Lỗi, chưa xong", "Failed");
|
|
66
|
+
if (end.state === "cancelled") return pick(locale, "Đã dừng", "Stopped");
|
|
67
|
+
return end.planSteps ? pick(locale, `Xong ${end.planSteps}/${end.planSteps}`, `Done ${end.planSteps}/${end.planSteps}`) : pick(locale, "Xong", "Done");
|
|
68
|
+
}
|
|
@@ -95,13 +95,23 @@
|
|
|
95
95
|
"managedThinkingLevels": {"type": "array", "uniqueItems": true, "maxItems": 7, "items": {"enum": ["off","minimal","low","medium","high","xhigh","max"]}}, "managedPermission": {"enum": ["workspace-write","trusted-full-access"]},
|
|
96
96
|
"managedPlan": { "type": "object", "additionalProperties": false, "required": ["steps"], "properties": {
|
|
97
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"] } } } } } },
|
|
98
|
+
"step": { "type": "string", "minLength": 1, "maxLength": 200 }, "status": { "enum": ["pending", "in_progress", "completed", "deferred"] } } } } } },
|
|
99
99
|
"managedProcess": { "type": "object", "additionalProperties": false, "required": ["outcome", "verified", "reviewed", "blockingOpen"], "properties": {
|
|
100
100
|
"outcome": { "enum": ["no_change", "interrupted", "disputed", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
|
|
101
101
|
"verified": { "type": "boolean" }, "reviewed": { "type": "boolean" }, "blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 }, "planSkipped": { "const": true } } },
|
|
102
|
+
"managedTurnEnd": { "type": "object", "additionalProperties": false, "required": ["state", "planSteps", "planDone"], "properties": {
|
|
103
|
+
"state": { "enum": ["done", "midway", "failed", "cancelled"] },
|
|
104
|
+
"planSteps": { "type": "integer", "minimum": 0, "maximum": 30 }, "planDone": { "type": "integer", "minimum": 0, "maximum": 30 },
|
|
105
|
+
"planDeferred": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
106
|
+
"reason": { "enum": ["idle", "limit", "policy"] } } },
|
|
102
107
|
"gitBranch": { "type": "object", "additionalProperties": false, "required": ["name", "detached"], "properties": {
|
|
103
108
|
"name": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
104
109
|
"detached": { "type": "boolean" } } },
|
|
110
|
+
"gitRepositories": { "type": "array", "maxItems": 30, "items": { "type": "object", "additionalProperties": false, "required": ["name", "branch"], "properties": {
|
|
111
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^[^\\u0000-\\u001F\\u007F/\\\\]*$" },
|
|
112
|
+
"branch": { "anyOf": [{ "type": "null" }, { "type": "object", "additionalProperties": false, "required": ["name", "detached"], "properties": {
|
|
113
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
114
|
+
"detached": { "type": "boolean" } } }] } } } },
|
|
105
115
|
"modelLabel": { "$ref": "common-v1.schema.json#/$defs/nullableDisplayText" },
|
|
106
116
|
"thinkingLevel": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "unknown"] },
|
|
107
117
|
"contextUsage": { "$ref": "#/$defs/contextUsage" },
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"additionalProperties": false,
|
|
158
158
|
"required": ["phase"],
|
|
159
159
|
"properties": {
|
|
160
|
-
"phase": { "enum": ["plan", "verify", "review", "final", "objection", "answer", "claims", "rejudge", "dispute"] },
|
|
160
|
+
"phase": { "enum": ["plan", "verify", "review", "final", "objection", "answer", "claims", "rejudge", "dispute", "continue"] },
|
|
161
161
|
"outcome": { "enum": ["no_change", "interrupted", "disputed", "blocking_open", "unverified", "review_unavailable", "unreviewed", "clean"] },
|
|
162
162
|
"role": { "enum": ["scout", "research", "verify", "review"] },
|
|
163
163
|
"by": { "enum": ["main", "harness"] },
|
|
@@ -192,6 +192,11 @@
|
|
|
192
192
|
"reviewed": { "type": "boolean" },
|
|
193
193
|
"blockingOpen": { "type": "integer", "minimum": 0, "maximum": 1000 },
|
|
194
194
|
"planOpen": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
195
|
+
"planDone": { "type": "integer", "minimum": 0, "maximum": 30 },
|
|
196
|
+
"planSteps": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
197
|
+
"round": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
198
|
+
"maxRounds": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
199
|
+
"idle": { "type": "integer", "minimum": 1, "maximum": 2 },
|
|
195
200
|
"planSkipped": { "const": true },
|
|
196
201
|
"verifyPolicy": { "enum": ["off", "suggest", "require"] },
|
|
197
202
|
"reviewPolicy": { "enum": ["off", "suggest", "require"] },
|
|
@@ -213,6 +218,21 @@
|
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
},
|
|
221
|
+
"turnEnd": {
|
|
222
|
+
"type": "object",
|
|
223
|
+
"additionalProperties": false,
|
|
224
|
+
"required": ["state", "planSteps", "planDone", "rounds"],
|
|
225
|
+
"properties": {
|
|
226
|
+
"state": { "enum": ["done", "midway", "failed", "cancelled"] },
|
|
227
|
+
"planSteps": { "type": "integer", "minimum": 0, "maximum": 30 },
|
|
228
|
+
"planDone": { "type": "integer", "minimum": 0, "maximum": 30 },
|
|
229
|
+
"planDeferred": { "type": "integer", "minimum": 1, "maximum": 30 },
|
|
230
|
+
"rounds": { "type": "integer", "minimum": 0, "maximum": 30 },
|
|
231
|
+
"reason": { "enum": ["idle", "limit", "policy"] },
|
|
232
|
+
"role": { "enum": ["main", "scout", "research", "verify", "review"] },
|
|
233
|
+
"code": { "type": "string", "pattern": "^[a-z0-9_]{1,64}$" }
|
|
234
|
+
}
|
|
235
|
+
},
|
|
216
236
|
"companyFailure": {
|
|
217
237
|
"type": "object",
|
|
218
238
|
"additionalProperties": false,
|
|
@@ -243,7 +263,7 @@
|
|
|
243
263
|
"additionalProperties": false,
|
|
244
264
|
"required": ["kind", "target", "detail"],
|
|
245
265
|
"properties": {
|
|
246
|
-
"kind": { "enum": ["read", "write", "edit", "command", "search", "list", "web-search", "web-fetch", "subagent", "network", "git", "plan", "other"] },
|
|
266
|
+
"kind": { "enum": ["read", "write", "edit", "command", "search", "list", "web-search", "web-fetch", "subagent", "network", "docker", "git", "plan", "other"] },
|
|
247
267
|
"target": { "anyOf": [{ "type": "string", "maxLength": 400 }, { "type": "null" }] },
|
|
248
268
|
"detail": { "anyOf": [{ "type": "string", "maxLength": 400 }, { "type": "null" }] }
|
|
249
269
|
}
|
|
@@ -315,6 +335,7 @@
|
|
|
315
335
|
"model": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9._:@/-]{1,120}$" },
|
|
316
336
|
"failure": { "$ref": "#/$defs/companyFailure" },
|
|
317
337
|
"process": { "$ref": "#/$defs/harnessProcess" },
|
|
338
|
+
"turnEnd": { "$ref": "#/$defs/turnEnd" },
|
|
318
339
|
"toolCalls": {
|
|
319
340
|
"type": "array",
|
|
320
341
|
"maxItems": 64,
|