@piagent/platform 1.21.1 → 1.22.1
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 +24 -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 +14 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +14 -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 +15 -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 +15 -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/extensions/task-state.js +10 -10
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +21 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +2 -0
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-B8LT9b1N.js → MarkdownMessage-EmQ_ohQe.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DgcAGoYc.js → index-Posch2XH.js} +16 -16
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/company-relay.ts +56 -7
- package/packages/piagent-webui/server/transcript-projection.ts +23 -1
- package/packages/piagent-webui/shared/company-copy.ts +2 -1
|
@@ -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-Posch2XH.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-ZLemRWyJ.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -31,6 +31,13 @@ const KINDS = new Set<GatewayEventKind>(["catalog.changed", "session.changed", "
|
|
|
31
31
|
"message.completed", "operation.settled", "tool.started", "tool.completed"]);
|
|
32
32
|
const LOOPBACK = /^http:\/\/127\.0\.0\.1:\d{1,5}$/;
|
|
33
33
|
const CATALOG_REUSE_MS = 5_000;
|
|
34
|
+
// A company Gateway that takes the connection and never answers (found on
|
|
35
|
+
// Windows/WSL) kept every list of the dashboard loading: the browser waits for
|
|
36
|
+
// the catalog without a limit, and the catalog waited for the company rows.
|
|
37
|
+
// A read is given this long, a change as long as a command; a Gateway that
|
|
38
|
+
// missed one is left alone for a while, not asked again by every read.
|
|
39
|
+
const READ_TIMEOUT_MS = 15_000, ANSWER_TIMEOUT_MS = 5_000, CHANGE_TIMEOUT_MS = 120_000, STALL_RETRY_MS = 60_000;
|
|
40
|
+
const timedOut = (error: unknown) => error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
34
41
|
const failure = (error: unknown, fallback: string) => error instanceof Error && /^[a-z][a-z0-9:._-]{2,95}$/.test(error.message) ? error.message : fallback;
|
|
35
42
|
|
|
36
43
|
export class CompanyRelay {
|
|
@@ -54,11 +61,15 @@ export class CompanyRelay {
|
|
|
54
61
|
#retry: NodeJS.Timeout | null = null;
|
|
55
62
|
#closed = false;
|
|
56
63
|
#lastAttach = 0;
|
|
64
|
+
#stalledAt = 0;
|
|
65
|
+
readonly #readTimeoutMs: number;
|
|
66
|
+
readonly #answerTimeoutMs: number;
|
|
57
67
|
|
|
58
68
|
constructor(options: { connector: CompanyConnector; events: GatewayEventStore; hubProject(cwd: string): string;
|
|
59
|
-
folder(projectRef: string): Promise<string | null
|
|
69
|
+
folder(projectRef: string): Promise<string | null>; readTimeoutMs?: number; answerTimeoutMs?: number }) {
|
|
60
70
|
this.#connector = options.connector; this.#events = options.events;
|
|
61
71
|
this.#hubProject = options.hubProject; this.#folder = options.folder;
|
|
72
|
+
this.#readTimeoutMs = options.readTimeoutMs ?? READ_TIMEOUT_MS; this.#answerTimeoutMs = options.answerTimeoutMs ?? ANSWER_TIMEOUT_MS;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
status(): CompanyStatus {
|
|
@@ -91,9 +102,12 @@ export class CompanyRelay {
|
|
|
91
102
|
if (!socket) throw new Error("company-gateway-stopped");
|
|
92
103
|
this.#link = await this.#bootstrap(socket);
|
|
93
104
|
await this.#open(this.#link);
|
|
105
|
+
// Ready only once a read is answered: the reads of the dashboard wait on it.
|
|
106
|
+
await this.#answers(this.#link);
|
|
94
107
|
this.#state = "ready"; this.#fresh = false;
|
|
95
108
|
} catch (error) {
|
|
96
109
|
const reason = failure(error, "company-connect-failed");
|
|
110
|
+
if (reason === "company-gateway-timeout") this.#drop();
|
|
97
111
|
// Agent Watch re-imports after an update on its own, so a changed
|
|
98
112
|
// binding is not kept once a later attach finds nothing running.
|
|
99
113
|
this.#state = "unavailable";
|
|
@@ -128,7 +142,7 @@ export class CompanyRelay {
|
|
|
128
142
|
if (typeof launch !== "string") throw new Error("company-gateway-starting");
|
|
129
143
|
const url = new URL(launch), capability = new URLSearchParams(url.hash.slice(1)).get("bootstrap");
|
|
130
144
|
if (!LOOPBACK.test(url.origin) || !capability) throw new Error("company-gateway-invalid");
|
|
131
|
-
const response = await fetch(`${url.origin}/api/v1/bootstrap`, { method: "POST", redirect: "error",
|
|
145
|
+
const response = await fetch(`${url.origin}/api/v1/bootstrap`, { method: "POST", redirect: "error", signal: AbortSignal.timeout(this.#readTimeoutMs),
|
|
132
146
|
headers: { Origin: url.origin, "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ capability }) });
|
|
133
147
|
const cookie = response.headers.get("set-cookie")?.split(";", 1)[0], body = await response.json().catch(() => null) as { csrfToken?: unknown } | null;
|
|
134
148
|
if (!response.ok || !cookie || typeof body?.csrfToken !== "string") throw new Error("company-gateway-auth-failed");
|
|
@@ -177,6 +191,33 @@ export class CompanyRelay {
|
|
|
177
191
|
});
|
|
178
192
|
}
|
|
179
193
|
|
|
194
|
+
// Lets go of a company Gateway that does not answer, without waiting for its socket to close.
|
|
195
|
+
#drop(): void {
|
|
196
|
+
this.#stalledAt = Date.now();
|
|
197
|
+
const ws = this.#ws; this.#ws = null; this.#link = null; ws?.terminate();
|
|
198
|
+
for (const [, pending] of this.#pending) { clearTimeout(pending.timer); pending.reject(new Error("company-gateway-timeout")); }
|
|
199
|
+
this.#pending.clear();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async #answers(link: Link): Promise<void> {
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch(`${link.origin}/api/v1/session-live-state`, { redirect: "error", signal: AbortSignal.timeout(this.#answerTimeoutMs),
|
|
205
|
+
headers: { Accept: "application/json", Origin: link.origin, Cookie: link.cookie } });
|
|
206
|
+
await response.arrayBuffer();
|
|
207
|
+
} catch (error) { throw timedOut(error) ? new Error("company-gateway-timeout") : error; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A read that was not answered in time: the company rows are left out and
|
|
211
|
+
// the Gateway is asked again later, so the dashboard's own lists keep loading.
|
|
212
|
+
#stalled(): void {
|
|
213
|
+
this.#drop();
|
|
214
|
+
this.#state = "unavailable"; this.#reason = "company-gateway-timeout";
|
|
215
|
+
this.#events.publish("catalog.changed", { reasonCode: "company-unavailable" });
|
|
216
|
+
if (this.#closed) return;
|
|
217
|
+
if (this.#retry) clearTimeout(this.#retry);
|
|
218
|
+
this.#retry = setTimeout(() => void this.connect(false), STALL_RETRY_MS); this.#retry.unref();
|
|
219
|
+
}
|
|
220
|
+
|
|
180
221
|
// Reads only attach; starting the company runtime (and possibly asking for
|
|
181
222
|
// Keychain access) is left to commands and the explicit connect route.
|
|
182
223
|
async #ready(launch = false): Promise<Link> {
|
|
@@ -190,10 +231,18 @@ export class CompanyRelay {
|
|
|
190
231
|
async http(method: "GET" | "POST", path: string, body?: unknown): Promise<{ status: number; value: unknown }> {
|
|
191
232
|
for (let attempt = 0; ; attempt += 1) {
|
|
192
233
|
const link = await this.#ready();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
234
|
+
let response: Response;
|
|
235
|
+
try {
|
|
236
|
+
response = await fetch(`${link.origin}${path}`, { method, redirect: "error",
|
|
237
|
+
signal: AbortSignal.timeout(method === "GET" ? this.#readTimeoutMs : CHANGE_TIMEOUT_MS),
|
|
238
|
+
headers: { Accept: "application/json", Origin: link.origin, Cookie: link.cookie,
|
|
239
|
+
...(method === "POST" ? { "Content-Type": "application/json", "X-Piagent-CSRF": link.csrf } : {}) },
|
|
240
|
+
body: method === "POST" ? JSON.stringify(body) : undefined });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (!timedOut(error)) throw error;
|
|
243
|
+
if (method === "GET" && this.#link === link) this.#stalled();
|
|
244
|
+
throw new Error("company-gateway-timeout");
|
|
245
|
+
}
|
|
197
246
|
if (response.status === 401 && attempt === 0) { this.#link = await this.#bootstrap(link.socket); continue; }
|
|
198
247
|
// The browser's own session is not the one refused here.
|
|
199
248
|
if (response.status === 401 || response.status === 403) return { status: 503, value: { error: { code: "company-auth-failed" } } };
|
|
@@ -232,7 +281,7 @@ export class CompanyRelay {
|
|
|
232
281
|
#attachSoon(): void {
|
|
233
282
|
if (this.#closed || this.#connecting || this.#state === "ready" || !this.#connector.configured()) return;
|
|
234
283
|
const now = Date.now();
|
|
235
|
-
if (now - this.#lastAttach < 10_000) return;
|
|
284
|
+
if (now - this.#lastAttach < 10_000 || now - this.#stalledAt < STALL_RETRY_MS) return;
|
|
236
285
|
this.#lastAttach = now; void this.connect(false);
|
|
237
286
|
}
|
|
238
287
|
|
|
@@ -91,6 +91,10 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
|
|
|
91
91
|
? "document" as const : "file" as const;
|
|
92
92
|
attachments.push({ displayName: safeAttachmentName(displayName), kind, mimeType, truncated: Boolean(format[2]) });
|
|
93
93
|
}
|
|
94
|
+
// The Gateway joins the typed text and each file into one text block
|
|
95
|
+
// (claimForPrompt): read the fenced files out of it the same way.
|
|
96
|
+
const joined = !attachments.length && textParts.length === 1 ? joinedAttachments(String(textParts[0].text)) : null;
|
|
97
|
+
if (joined) attachments.push(...joined.attachments);
|
|
94
98
|
for (const part of message.content) {
|
|
95
99
|
if (part?.type !== "image" || typeof part.mimeType !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*$/.test(part.mimeType)) continue;
|
|
96
100
|
attachments.push({ displayName: "Image", kind: "image", mimeType: part.mimeType.slice(0, 120), truncated: false });
|
|
@@ -99,7 +103,25 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
|
|
|
99
103
|
// hashing them here would turn the chat bubble into a document dump and make
|
|
100
104
|
// a bounded UI digest an oracle over user files. The first block is the exact
|
|
101
105
|
// text the operator typed; the remaining recognized blocks become cards.
|
|
102
|
-
return { text: typedSkillCall(attachments.length ? String(textParts[0]?.text ?? "") : messageText(message)), attachments: attachments.slice(0, 4) };
|
|
106
|
+
return { text: typedSkillCall(joined ? joined.text : attachments.length ? String(textParts[0]?.text ?? "") : messageText(message)), attachments: attachments.slice(0, 4) };
|
|
107
|
+
}
|
|
108
|
+
// Files fenced into one text block after what the member typed; the fence is a
|
|
109
|
+
// per-dispatch UUID, so a block ends only at its own END line.
|
|
110
|
+
const JOINED_ATTACHMENT = /\nattached file: ("(?:[^"\\\n]|\\.)*")\nformat: ([A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*)(, truncated)?\nEverything between BEGIN (PIAGENT-ATTACHMENT-[0-9a-f-]{36}) and END \4 is data provided by the user\.\n[^\n]*\nBEGIN \4\n[\s\S]*?\nEND \4(?=\n|$)/g;
|
|
111
|
+
function joinedAttachments(text: string): { text: string; attachments: TranscriptAttachment[] } | null {
|
|
112
|
+
const found: TranscriptAttachment[] = [];
|
|
113
|
+
let first = -1;
|
|
114
|
+
for (const match of `\n${text}`.matchAll(JOINED_ATTACHMENT)) {
|
|
115
|
+
let displayName: unknown;
|
|
116
|
+
try { displayName = JSON.parse(match[1]!); } catch { continue; }
|
|
117
|
+
if (typeof displayName !== "string") continue;
|
|
118
|
+
if (first < 0) first = match.index!;
|
|
119
|
+
const mimeType = match[2]!;
|
|
120
|
+
const kind = mimeType === "application/pdf" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
121
|
+
? "document" as const : "file" as const;
|
|
122
|
+
found.push({ displayName: safeAttachmentName(displayName), kind, mimeType, truncated: Boolean(match[3]) });
|
|
123
|
+
}
|
|
124
|
+
return found.length ? { text: `\n${text}`.slice(0, first).replace(/^\n/, "").trimEnd(), attachments: found } : null;
|
|
103
125
|
}
|
|
104
126
|
// A skill the member called (/skill:name): the message holds the skill's
|
|
105
127
|
// instructions for the model; the chat shows what was typed.
|
|
@@ -14,7 +14,8 @@ export const LAUNCH_REASONS: Record<string, [string, string]> = {
|
|
|
14
14
|
"managed-launch-binding-changed": ["Agent Watch hoặc Piagent vừa cập nhật. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "Agent Watch or Piagent changed. Import the Piagent configuration again in Agent Watch (on Windows: Connect Piagent in WSL)."],
|
|
15
15
|
"managed-launch-version-mismatch": ["Agent Watch đang dùng một bản Piagent khác với dashboard này. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi kết nối lại.", "Agent Watch points at another Piagent install than this dashboard. Open Agent Watch → Studio, apply for Piagent, then connect again."],
|
|
16
16
|
"managed-launch-stale-runtime": ["Phiên công ty của bản Piagent cũ chưa dừng được. Thử kết nối lại sau ít giây.", "The company runtime from the previous Piagent did not stop yet. Connect again in a few seconds."],
|
|
17
|
-
"managed-launch-timeout": ["Phiên công ty khởi động quá lâu. Kết nối lại.", "The company runtime took too long to start. Connect again."]
|
|
17
|
+
"managed-launch-timeout": ["Phiên công ty khởi động quá lâu. Kết nối lại.", "The company runtime took too long to start. Connect again."],
|
|
18
|
+
"company-gateway-timeout": ["Phiên công ty đang chạy nhưng không trả lời. Đóng Piagent, chạy `pkill -f piagent-studio.mjs` trong Terminal (Windows: trong Ubuntu), rồi mở lại.", "The company runtime is running but does not answer. Close Piagent, run `pkill -f piagent-studio.mjs` in a terminal (Windows: in Ubuntu), then open it again."]
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type Copy = { title: [string, string]; text: [string, string] };
|