@piagent/platform 1.4.1 → 1.5.2
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 +69 -0
- package/README.md +5 -1
- package/README.vi.md +21 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +9 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.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/index.html +1 -1
- 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 +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- 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 +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/piagent-webui/document-workspace-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/document-workspace-v1.valid.json +42 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +2 -1
- package/package.json +1 -1
- package/packages/piagent-core/extensions/document-intake.ts +6 -3
- package/packages/piagent-core/extensions/piagent-guard.ts +2 -2
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/input/attachment-document-command.ts +39 -0
- package/packages/piagent-core/runtime/input/attachment-reservation.ts +36 -0
- package/packages/{piagent-webui/extension → piagent-core/runtime/input}/attachment-store.ts +221 -44
- package/packages/piagent-core/runtime/inspection/document-workspace-projection.ts +276 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +20 -5
- package/packages/piagent-core/runtime/inspection/session-identity.ts +20 -0
- package/packages/piagent-core/runtime/registration/workflow-commands.ts +5 -4
- package/packages/piagent-core/runtime/workflows/input-routing.ts +13 -2
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-CbUK4gg2.js +29 -0
- package/packages/piagent-webui/dist/client/assets/index-BV-_03yl.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-CNbYWd-R.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/piagent-webui.ts +15 -2
- package/packages/piagent-webui/extension/same-session-bridge.ts +9 -9
- package/packages/piagent-webui/gateway/ai-capability-inspection.ts +53 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +23 -3
- package/packages/piagent-webui/gateway/native-project-picker.ts +63 -7
- package/packages/piagent-webui/gateway/session-attachment-registry.ts +165 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +62 -9
- package/packages/piagent-webui/gateway/session-command-controller.ts +47 -14
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +30 -10
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +19 -10
- package/packages/piagent-webui/package.json +3 -1
- package/packages/piagent-webui/server/core-inspection-provider.ts +48 -11
- package/packages/piagent-webui/server/ipc-read-model-client.ts +2 -0
- package/packages/piagent-webui/server/loopback-server.ts +63 -25
- package/packages/piagent-webui/server/read-model-provider.ts +2 -0
- package/packages/piagent-webui/server/read-only-router.ts +6 -0
- package/packages/piagent-webui/server/transcript-projection.ts +43 -2
- package/schemas/piagent-webui/attachment-v1.schema.json +24 -4
- package/schemas/piagent-webui/capabilities-v1.schema.json +2 -2
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/document-workspace-v1.schema.json +155 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +14 -3
- package/schemas/piagent-webui/transcript-v1.schema.json +27 -0
- package/scripts/verify-local.sh +16 -1
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +0 -1
- package/packages/piagent-webui/dist/client/assets/index-bu7XpD9Q.js +0 -172
|
@@ -19,7 +19,8 @@ const MAX_JSON_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
|
19
19
|
const CURSOR = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
20
20
|
|
|
21
21
|
type ReadCapabilities = () => unknown | Promise<unknown>;
|
|
22
|
-
type RateState = { windowStart: number; requests: number; bootstraps: number
|
|
22
|
+
type RateState = { windowStart: number; requests: number; bootstraps: number };
|
|
23
|
+
type ControlRateState = { windowStart: number; controls: number };
|
|
23
24
|
|
|
24
25
|
export type LoopbackServer = {
|
|
25
26
|
origin: string;
|
|
@@ -57,6 +58,7 @@ export async function startLoopbackServer(options: {
|
|
|
57
58
|
readSessionCatalog?: () => unknown | Promise<unknown>;
|
|
58
59
|
readSessionCreationOptions?: () => unknown | Promise<unknown>;
|
|
59
60
|
readSessionModel?: (sessionRef: string) => WebUiReadModelProvider | Promise<WebUiReadModelProvider>;
|
|
61
|
+
executeSessionAttachment?: (sessionRef: string, command: unknown) => unknown | Promise<unknown>;
|
|
60
62
|
readSessionConnections?: (sessionRef: string) => unknown | Promise<unknown>;
|
|
61
63
|
executeSessionConnection?: (command: unknown) => unknown | Promise<unknown>;
|
|
62
64
|
readMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
|
|
@@ -82,6 +84,15 @@ export async function startLoopbackServer(options: {
|
|
|
82
84
|
const auth = new SessionAuthority({ bootstrapTtlMs: options.bootstrapTtlMs, sessionTtlMs: options.sessionTtlMs });
|
|
83
85
|
const sse = options.readModel ? new SseHub(options.readModel) : null;
|
|
84
86
|
const rates = new Map<string, RateState>();
|
|
87
|
+
const controlRates = new Map<string, ControlRateState>();
|
|
88
|
+
const consumeControl = (sessionId: string, now: number): boolean => {
|
|
89
|
+
for (const [id, value] of controlRates) if (now - value.windowStart >= 60_000) controlRates.delete(id);
|
|
90
|
+
const value = controlRates.get(sessionId) ?? { windowStart: now, controls: 0 };
|
|
91
|
+
if (now - value.windowStart >= 60_000) Object.assign(value, { windowStart: now, controls: 0 });
|
|
92
|
+
value.controls += 1; controlRates.set(sessionId, value);
|
|
93
|
+
while (controlRates.size > 64) controlRates.delete(controlRates.keys().next().value as string);
|
|
94
|
+
return value.controls <= CONTROLS_PER_MINUTE;
|
|
95
|
+
};
|
|
85
96
|
let origin = "";
|
|
86
97
|
const server = http.createServer(async (request, response) => {
|
|
87
98
|
applySecurityHeaders(response, styleNonce);
|
|
@@ -90,9 +101,16 @@ export async function startLoopbackServer(options: {
|
|
|
90
101
|
const requestOrigin = request.headers.origin;
|
|
91
102
|
if (requestOrigin && requestOrigin !== origin) return errorResponse(response, 403, "origin-mismatch");
|
|
92
103
|
const remote = request.socket.remoteAddress ?? "unknown", now = Date.now();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
// A browser session is already a launch-scoped authenticated capability.
|
|
105
|
+
// Charging every tab and every prior launch to one localhost IP lets a
|
|
106
|
+
// refresh or an unauthenticated local process lock out the active operator.
|
|
107
|
+
const browserSession = auth.authenticate(request, now);
|
|
108
|
+
const rateKey = browserSession ? `${remote}:${browserSession.id}` : `remote:${remote}`;
|
|
109
|
+
for (const [key, value] of rates) if (now - value.windowStart >= 60_000) rates.delete(key);
|
|
110
|
+
const rate = rates.get(rateKey) ?? { windowStart: now, requests: 0, bootstraps: 0 };
|
|
111
|
+
if (now - rate.windowStart >= 60_000) Object.assign(rate, { windowStart: now, requests: 0, bootstraps: 0 });
|
|
112
|
+
rate.requests += 1; rates.set(rateKey, rate);
|
|
113
|
+
while (rates.size > 128) rates.delete(rates.keys().next().value as string);
|
|
96
114
|
if (rate.requests > REQUESTS_PER_MINUTE) return errorResponse(response, 429, "rate-limit");
|
|
97
115
|
let url: URL;
|
|
98
116
|
try { url = new URL(request.url ?? "/", origin); }
|
|
@@ -122,10 +140,10 @@ export async function startLoopbackServer(options: {
|
|
|
122
140
|
|
|
123
141
|
if (request.method === "POST" && ["/api/v1/chat/messages", "/api/v1/session-options", "/api/v1/lifecycle",
|
|
124
142
|
"/api/v1/control/resume-and-continue", "/api/v1/reviews", "/api/v1/source-mutations", "/api/v1/source-handoffs"].includes(url.pathname) && options.executeControl) {
|
|
125
|
-
rate.controls += 1;
|
|
126
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
127
143
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
128
|
-
|
|
144
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
145
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
146
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
129
147
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
130
148
|
let command: unknown;
|
|
131
149
|
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
@@ -135,10 +153,10 @@ export async function startLoopbackServer(options: {
|
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
if (request.method === "POST" && url.pathname === "/api/v1/projects/import" && options.executeProjectImport) {
|
|
138
|
-
rate.controls += 1;
|
|
139
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
140
156
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
141
|
-
|
|
157
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
158
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
159
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
142
160
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
143
161
|
let command: unknown;
|
|
144
162
|
try { command = JSON.parse((await requestBody(request, MAX_BOOTSTRAP_BODY_BYTES)).toString("utf8")); }
|
|
@@ -155,10 +173,10 @@ export async function startLoopbackServer(options: {
|
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
if (request.method === "POST" && url.pathname === "/api/v1/provider-auth" && options.executeProviderAuth) {
|
|
158
|
-
rate.controls += 1;
|
|
159
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
160
176
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
161
|
-
|
|
177
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
178
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
179
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
162
180
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
163
181
|
let command: unknown;
|
|
164
182
|
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
@@ -172,10 +190,10 @@ export async function startLoopbackServer(options: {
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
if (request.method === "POST" && url.pathname === "/api/v1/session-connections" && options.executeSessionConnection) {
|
|
175
|
-
rate.controls += 1;
|
|
176
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
177
193
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
178
|
-
|
|
194
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
195
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
196
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
179
197
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
180
198
|
let command: unknown;
|
|
181
199
|
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
@@ -188,9 +206,10 @@ export async function startLoopbackServer(options: {
|
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
if (request.method === "POST" && url.pathname.startsWith("/api/v1/mcp-auth/jobs/") && url.pathname.endsWith("/cancel") && options.cancelMcpAuthJob) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
209
|
+
if (requestOrigin !== origin) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
210
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
211
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
212
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
194
213
|
const jobRef = url.pathname.slice("/api/v1/mcp-auth/jobs/".length, -"/cancel".length);
|
|
195
214
|
if (!CURSOR.test(jobRef) || jobRef.includes("/") || url.search) return errorResponse(response, 400, "invalid-mcp-auth-job-ref");
|
|
196
215
|
try { return jsonResponse(response, 200, await options.cancelMcpAuthJob(jobRef)); }
|
|
@@ -198,10 +217,10 @@ export async function startLoopbackServer(options: {
|
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
if (request.method === "POST" && url.pathname === "/api/v1/attachments" && options.executeAttachment) {
|
|
201
|
-
rate.controls += 1;
|
|
202
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
203
220
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
204
|
-
|
|
221
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
222
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
223
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
205
224
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
206
225
|
let command: unknown;
|
|
207
226
|
try { command = JSON.parse((await requestBody(request, MAX_ATTACHMENT_BODY_BYTES)).toString("utf8")); }
|
|
@@ -210,11 +229,30 @@ export async function startLoopbackServer(options: {
|
|
|
210
229
|
catch { return errorResponse(response, 503, "attachment-runtime-unavailable"); }
|
|
211
230
|
}
|
|
212
231
|
|
|
232
|
+
// The Gateway drives many sessions, so an attachment has to name the one it
|
|
233
|
+
// belongs to in the path. The body is the same bounded stage/discard command
|
|
234
|
+
// the single-session route takes, and every guard in front of it is the same.
|
|
235
|
+
if (request.method === "POST" && url.pathname.startsWith("/api/v1/sessions/") && url.pathname.endsWith("/attachments")
|
|
236
|
+
&& options.executeSessionAttachment) {
|
|
237
|
+
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
238
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
239
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
240
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
241
|
+
const sessionRef = url.pathname.slice("/api/v1/sessions/".length, -"/attachments".length);
|
|
242
|
+
if (!CURSOR.test(sessionRef) || sessionRef.includes("/")) return errorResponse(response, 400, "invalid-session-attachment-ref");
|
|
243
|
+
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
244
|
+
let command: unknown;
|
|
245
|
+
try { command = JSON.parse((await requestBody(request, MAX_ATTACHMENT_BODY_BYTES)).toString("utf8")); }
|
|
246
|
+
catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-attachment-command"); }
|
|
247
|
+
try { return jsonResponse(response, 200, await options.executeSessionAttachment(sessionRef, command)); }
|
|
248
|
+
catch { return errorResponse(response, 503, "attachment-runtime-unavailable"); }
|
|
249
|
+
}
|
|
250
|
+
|
|
213
251
|
if (request.method === "POST" && url.pathname.startsWith("/api/v1/approvals/") && url.pathname.endsWith("/decision") && options.executeApproval) {
|
|
214
|
-
rate.controls += 1;
|
|
215
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
216
252
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
217
|
-
|
|
253
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
254
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
255
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
218
256
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
219
257
|
const approvalRef = url.pathname.slice("/api/v1/approvals/".length, -"/decision".length);
|
|
220
258
|
if (!CURSOR.test(approvalRef) || approvalRef.includes("/")) return errorResponse(response, 400, "invalid-approval-ref");
|
|
@@ -22,6 +22,8 @@ export type WebUiReadModelProvider = {
|
|
|
22
22
|
handoffHistory(runRef: string): unknown | Promise<unknown>;
|
|
23
23
|
subagentTree(runRef: string): unknown | Promise<unknown>;
|
|
24
24
|
releaseMonitor(): unknown | Promise<unknown>;
|
|
25
|
+
documents(): unknown | Promise<unknown>;
|
|
26
|
+
document(documentRef: string): unknown | Promise<unknown>;
|
|
25
27
|
activity(): unknown | Promise<unknown>;
|
|
26
28
|
logPreview(activityRef: string): unknown | Promise<unknown>;
|
|
27
29
|
transcript(beforeCursor: string | null, limit: number): unknown | Promise<unknown>;
|
|
@@ -29,6 +29,12 @@ export async function routeReadOnlyRequest(url: URL, provider: WebUiReadModelPro
|
|
|
29
29
|
if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-task-run-ref" } } };
|
|
30
30
|
return { handled: true, status: 200, value: await provider.subagentTree(ref) };
|
|
31
31
|
}
|
|
32
|
+
if (url.pathname === "/api/v1/documents" && !url.search) return { handled: true, status: 200, value: await provider.documents() };
|
|
33
|
+
if (url.pathname.startsWith("/api/v1/documents/")) {
|
|
34
|
+
const ref = url.pathname.slice("/api/v1/documents/".length);
|
|
35
|
+
if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-document-ref" } } };
|
|
36
|
+
return { handled: true, status: 200, value: await provider.document(ref) };
|
|
37
|
+
}
|
|
32
38
|
if (url.pathname === "/api/v1/chat/queue" && !url.search) return { handled: true, status: 200, value: await provider.queue() };
|
|
33
39
|
if (url.pathname === "/api/v1/session-options/models" && !url.search) return { handled: true, status: 200, value: await provider.modelCatalog() };
|
|
34
40
|
if (url.pathname.startsWith("/api/v1/approvals/")) {
|
|
@@ -13,8 +13,10 @@ type TranscriptRevision = { runtimeRevision: string; taskRevision: string | null
|
|
|
13
13
|
indexRevision: string | null; approvalRevision: string | null; sessionOptionRevision: string | null; queueRevision: string | null };
|
|
14
14
|
type TranscriptContent = { state: "available" | "redacted" | "unavailable"; text: string | null; textChars: number | null; digest: string | null;
|
|
15
15
|
truncated: boolean; redacted: boolean; imageCount: number; reasonCode: string | null };
|
|
16
|
+
type TranscriptAttachment = { displayName: string; kind: "file" | "image" | "document"; mimeType: string; truncated: boolean };
|
|
16
17
|
type TranscriptItem = { messageRef: string; parentMessageRef: string | null; role: "user" | "assistant" | "tool-result"; recordedAt: string;
|
|
17
18
|
agentOperationId: string | null; turnIndex: number | null; content: TranscriptContent;
|
|
19
|
+
attachments?: TranscriptAttachment[];
|
|
18
20
|
toolCalls: Array<{ toolCallRef: string; toolName: string; state: "requested" | "completed" | "failed" | "unknown" }> };
|
|
19
21
|
export type TranscriptDocument = { schemaVersion: 1; version: "piagent-webui-transcript-v1"; generatedAt: string; identity: TranscriptIdentity;
|
|
20
22
|
revision: TranscriptRevision; eventCursor: string; state: "ready" | "unavailable"; items: TranscriptItem[];
|
|
@@ -41,6 +43,37 @@ function messageText(message: any): string {
|
|
|
41
43
|
if (!Array.isArray(message?.content)) return "";
|
|
42
44
|
return message.content.filter((item: any) => item?.type === "text" && typeof item.text === "string").map((item: any) => item.text).join("\n");
|
|
43
45
|
}
|
|
46
|
+
function safeAttachmentName(value: unknown): string {
|
|
47
|
+
const projected = safeText(String(value ?? "attachment"));
|
|
48
|
+
return projected.full.replace(/[\\/]/g, "_").replace(/\s+/g, " ").trim().slice(0, 160) || "attachment";
|
|
49
|
+
}
|
|
50
|
+
function userMessageProjection(message: any): { text: string; attachments: TranscriptAttachment[] } {
|
|
51
|
+
if (message?.role !== "user" || !Array.isArray(message?.content)) return { text: messageText(message), attachments: [] };
|
|
52
|
+
const textParts = message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string");
|
|
53
|
+
const attachments: TranscriptAttachment[] = [];
|
|
54
|
+
for (const part of textParts.slice(1)) {
|
|
55
|
+
const lines = String(part.text).split("\n", 3);
|
|
56
|
+
if (!lines[0]?.startsWith("attached file: ") || !lines[1]?.startsWith("format: ")) continue;
|
|
57
|
+
let displayName: unknown;
|
|
58
|
+
try { displayName = JSON.parse(lines[0].slice("attached file: ".length)); } catch { continue; }
|
|
59
|
+
if (typeof displayName !== "string") continue;
|
|
60
|
+
const format = /^format: ([A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*)(, truncated)?$/.exec(lines[1]);
|
|
61
|
+
if (!format) continue;
|
|
62
|
+
const mimeType = format[1]!;
|
|
63
|
+
const kind = mimeType === "application/pdf" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
64
|
+
? "document" as const : "file" as const;
|
|
65
|
+
attachments.push({ displayName: safeAttachmentName(displayName), kind, mimeType, truncated: Boolean(format[2]) });
|
|
66
|
+
}
|
|
67
|
+
for (const part of message.content) {
|
|
68
|
+
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;
|
|
69
|
+
attachments.push({ displayName: "Image", kind: "image", mimeType: part.mimeType.slice(0, 120), truncated: false });
|
|
70
|
+
}
|
|
71
|
+
// Attachment bodies are provider input, not transcript prose. Showing or
|
|
72
|
+
// hashing them here would turn the chat bubble into a document dump and make
|
|
73
|
+
// a bounded UI digest an oracle over user files. The first block is the exact
|
|
74
|
+
// text the operator typed; the remaining recognized blocks become cards.
|
|
75
|
+
return { text: attachments.length ? String(textParts[0]?.text ?? "") : messageText(message), attachments: attachments.slice(0, 4) };
|
|
76
|
+
}
|
|
44
77
|
function assistantFailureReason(message: any): string | null {
|
|
45
78
|
if (message?.role !== "assistant" || message?.stopReason !== "error") return null;
|
|
46
79
|
const detail = String(message?.errorMessage ?? "").toLowerCase();
|
|
@@ -82,15 +115,22 @@ function toolCalls(message: any, sessionRef: string, role: TranscriptItem["role"
|
|
|
82
115
|
function role(message: any): TranscriptItem["role"] | null {
|
|
83
116
|
return message?.role === "user" ? "user" : message?.role === "assistant" ? "assistant" : message?.role === "toolResult" ? "tool-result" : null;
|
|
84
117
|
}
|
|
118
|
+
function isInternalFreshTransition(message: any): boolean {
|
|
119
|
+
if (message?.role !== "user") return false;
|
|
120
|
+
return /^\/fresh\s+(?:task|scout|be-to-fe)\s+(?:--session-title\s+"[^"\r\n]{1,64}"\s+)?Read task intake from \.pi\/task-inbox\/[A-Za-z0-9._-]+\.md\.\s+Current session is near context limits; use a fresh governed session\.$/u
|
|
121
|
+
.test(messageText(message).trim());
|
|
122
|
+
}
|
|
85
123
|
function unavailableContent(reasonCode: string): TranscriptContent {
|
|
86
124
|
return { state: "unavailable", text: null, textChars: null, digest: null, truncated: false, redacted: false, imageCount: 0, reasonCode };
|
|
87
125
|
}
|
|
88
126
|
function item(entry: any, identity: TranscriptIdentity): TranscriptItem | null {
|
|
89
127
|
if (!entry || entry.type !== "message" || !entry.message) return null;
|
|
128
|
+
if (isInternalFreshTransition(entry.message)) return null;
|
|
90
129
|
const itemRole = role(entry.message), recordedAt = timestamp(entry.timestamp ?? entry.message.timestamp);
|
|
91
130
|
if (!itemRole || !recordedAt || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
131
|
+
const userProjection = userMessageProjection(entry.message);
|
|
92
132
|
const content = itemRole === "tool-result" ? unavailableContent("tool-output-in-activity-preview") : (() => {
|
|
93
|
-
const projected = safeText(messageText(entry.message));
|
|
133
|
+
const projected = safeText(itemRole === "user" ? userProjection.text : messageText(entry.message));
|
|
94
134
|
const failureReason = projected.full.length === 0 ? assistantFailureReason(entry.message) : null;
|
|
95
135
|
if (failureReason) return unavailableContent(failureReason);
|
|
96
136
|
return { state: projected.redacted ? "redacted" as const : "available" as const, text: projected.preview,
|
|
@@ -98,7 +138,8 @@ function item(entry: any, identity: TranscriptIdentity): TranscriptItem | null {
|
|
|
98
138
|
redacted: projected.redacted, imageCount: imageCount(entry.message), reasonCode: projected.redacted ? "sensitive-values-redacted" as const : null };
|
|
99
139
|
})();
|
|
100
140
|
return { messageRef: opaque("message", [identity.sessionRef, entry.id]), parentMessageRef: null, role: itemRole, recordedAt,
|
|
101
|
-
agentOperationId: null, turnIndex: null, content,
|
|
141
|
+
agentOperationId: null, turnIndex: null, content, ...(userProjection.attachments.length ? { attachments: userProjection.attachments } : {}),
|
|
142
|
+
toolCalls: toolCalls(entry.message, identity.sessionRef, itemRole) };
|
|
102
143
|
}
|
|
103
144
|
function unavailable(input: TranscriptProjectionInput, reasonCode: string, limit: number): TranscriptDocument {
|
|
104
145
|
return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
|
|
@@ -49,7 +49,12 @@
|
|
|
49
49
|
"image/bmp",
|
|
50
50
|
"text/plain",
|
|
51
51
|
"text/markdown",
|
|
52
|
-
"
|
|
52
|
+
"text/csv",
|
|
53
|
+
"text/tab-separated-values",
|
|
54
|
+
"application/json",
|
|
55
|
+
"application/yaml",
|
|
56
|
+
"application/pdf",
|
|
57
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
53
58
|
]
|
|
54
59
|
},
|
|
55
60
|
"dataBase64": {
|
|
@@ -86,7 +91,7 @@
|
|
|
86
91
|
"attachment": {
|
|
87
92
|
"type": "object",
|
|
88
93
|
"additionalProperties": false,
|
|
89
|
-
"required": ["attachmentRef", "messageRequestId", "displayName", "kind", "mimeType", "sizeBytes", "expiresAt"],
|
|
94
|
+
"required": ["attachmentRef", "messageRequestId", "displayName", "kind", "mimeType", "sizeBytes", "sourceBytes", "truncated", "expiresAt"],
|
|
90
95
|
"properties": {
|
|
91
96
|
"attachmentRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
92
97
|
"messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
@@ -96,14 +101,29 @@
|
|
|
96
101
|
"maxLength": 160,
|
|
97
102
|
"pattern": "^[^\\u0000-\\u001F\\u007F/\\\\]*$"
|
|
98
103
|
},
|
|
99
|
-
"kind": { "enum": ["file", "image"] },
|
|
104
|
+
"kind": { "enum": ["file", "image", "document"] },
|
|
100
105
|
"mimeType": {
|
|
101
106
|
"type": "string",
|
|
102
107
|
"minLength": 3,
|
|
103
108
|
"maxLength": 127,
|
|
104
109
|
"pattern": "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+*-]+$"
|
|
105
110
|
},
|
|
106
|
-
"sizeBytes": {
|
|
111
|
+
"sizeBytes": {
|
|
112
|
+
"description": "Bytes held for dispatch: image bytes, or the UTF-8 text extracted from a text or document file.",
|
|
113
|
+
"type": "integer",
|
|
114
|
+
"minimum": 1,
|
|
115
|
+
"maximum": 8388608
|
|
116
|
+
},
|
|
117
|
+
"sourceBytes": {
|
|
118
|
+
"description": "Bytes of the file the operator selected, before extraction. Equal to sizeBytes for images.",
|
|
119
|
+
"type": "integer",
|
|
120
|
+
"minimum": 1,
|
|
121
|
+
"maximum": 8388608
|
|
122
|
+
},
|
|
123
|
+
"truncated": {
|
|
124
|
+
"description": "True when extraction produced more text than the per-attachment cap, so Pi receives a prefix.",
|
|
125
|
+
"type": "boolean"
|
|
126
|
+
},
|
|
107
127
|
"expiresAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" }
|
|
108
128
|
}
|
|
109
129
|
},
|
|
@@ -535,9 +535,9 @@
|
|
|
535
535
|
"kinds": {
|
|
536
536
|
"type": "array",
|
|
537
537
|
"minItems": 1,
|
|
538
|
-
"maxItems":
|
|
538
|
+
"maxItems": 3,
|
|
539
539
|
"uniqueItems": true,
|
|
540
|
-
"items": { "enum": ["file", "image"] }
|
|
540
|
+
"items": { "enum": ["file", "image", "document"] }
|
|
541
541
|
},
|
|
542
542
|
"mimeTypes": {
|
|
543
543
|
"type": "array",
|
|
@@ -173,6 +173,15 @@
|
|
|
173
173
|
"supported": true,
|
|
174
174
|
"current": true
|
|
175
175
|
},
|
|
176
|
+
{
|
|
177
|
+
"name": "document-workspace-v1",
|
|
178
|
+
"file": "document-workspace-v1.schema.json",
|
|
179
|
+
"id": "https://piagent.io.vn/schemas/piagent-webui/document-workspace-v1.schema.json",
|
|
180
|
+
"documentVersion": 1,
|
|
181
|
+
"role": "read-model",
|
|
182
|
+
"supported": true,
|
|
183
|
+
"current": true
|
|
184
|
+
},
|
|
176
185
|
{
|
|
177
186
|
"name": "control-command-v1",
|
|
178
187
|
"file": "control-command-v1.schema.json",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://piagent.io.vn/schemas/piagent-webui/document-workspace-v1.schema.json",
|
|
4
|
+
"title": "Piagent WebUI bounded document workspace v1",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{ "$ref": "#/$defs/listing" },
|
|
7
|
+
{ "$ref": "#/$defs/document" }
|
|
8
|
+
],
|
|
9
|
+
"$defs": {
|
|
10
|
+
"displayText": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"maxLength": 1024,
|
|
14
|
+
"pattern": "^[^\\u0000-\\u001F\\u007F]*$"
|
|
15
|
+
},
|
|
16
|
+
"nullableDisplayText": {
|
|
17
|
+
"anyOf": [
|
|
18
|
+
{ "$ref": "#/$defs/displayText" },
|
|
19
|
+
{ "type": "null" }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"root": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"required": ["rootRef", "path", "source", "documentCount"],
|
|
26
|
+
"properties": {
|
|
27
|
+
"rootRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
28
|
+
"path": { "$ref": "#/$defs/displayText" },
|
|
29
|
+
"source": { "enum": ["project", "profile", "environment"] },
|
|
30
|
+
"documentCount": { "type": "integer", "minimum": 0, "maximum": 2000 }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"entry": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["documentRef", "rootRef", "name", "relativePath", "extension", "sizeBytes", "modifiedAt"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"documentRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
39
|
+
"rootRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
40
|
+
"name": { "$ref": "#/$defs/displayText" },
|
|
41
|
+
"relativePath": { "$ref": "#/$defs/displayText" },
|
|
42
|
+
"extension": {
|
|
43
|
+
"enum": ["md", "markdown", "txt", "text", "csv", "tsv", "json", "yaml", "yml", "pdf", "docx"]
|
|
44
|
+
},
|
|
45
|
+
"sizeBytes": { "type": "integer", "minimum": 1, "maximum": 12582912 },
|
|
46
|
+
"modifiedAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"listing": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"required": ["schemaVersion", "version", "messageType", "generatedAt", "state", "roots", "documents", "truncated", "reasonCode"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"schemaVersion": { "const": 1 },
|
|
55
|
+
"version": { "const": "piagent-webui-document-workspace-v1" },
|
|
56
|
+
"messageType": { "const": "listing" },
|
|
57
|
+
"generatedAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" },
|
|
58
|
+
"state": { "enum": ["ready", "unavailable"] },
|
|
59
|
+
"roots": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"maxItems": 32,
|
|
62
|
+
"uniqueItems": true,
|
|
63
|
+
"items": { "$ref": "#/$defs/root" }
|
|
64
|
+
},
|
|
65
|
+
"documents": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"maxItems": 2000,
|
|
68
|
+
"items": { "$ref": "#/$defs/entry" }
|
|
69
|
+
},
|
|
70
|
+
"truncated": { "type": "boolean" },
|
|
71
|
+
"reasonCode": { "$ref": "common-v1.schema.json#/$defs/nullableReasonCode" }
|
|
72
|
+
},
|
|
73
|
+
"allOf": [
|
|
74
|
+
{
|
|
75
|
+
"if": { "properties": { "state": { "const": "unavailable" } }, "required": ["state"] },
|
|
76
|
+
"then": {
|
|
77
|
+
"properties": {
|
|
78
|
+
"roots": { "type": "array", "maxItems": 0 },
|
|
79
|
+
"documents": { "type": "array", "maxItems": 0 },
|
|
80
|
+
"reasonCode": { "$ref": "common-v1.schema.json#/$defs/reasonCode" }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"if": { "properties": { "state": { "const": "ready" } }, "required": ["state"] },
|
|
86
|
+
"then": {
|
|
87
|
+
"properties": {
|
|
88
|
+
"roots": { "type": "array", "minItems": 1 },
|
|
89
|
+
"reasonCode": { "type": "null" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"document": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"additionalProperties": false,
|
|
98
|
+
"required": ["schemaVersion", "version", "messageType", "generatedAt", "documentRef", "state", "name", "relativePath", "rootRef", "format", "text", "sizeBytes", "truncated", "redacted", "reasonCode"],
|
|
99
|
+
"properties": {
|
|
100
|
+
"schemaVersion": { "const": 1 },
|
|
101
|
+
"version": { "const": "piagent-webui-document-workspace-v1" },
|
|
102
|
+
"messageType": { "const": "document" },
|
|
103
|
+
"generatedAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" },
|
|
104
|
+
"documentRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
105
|
+
"state": { "enum": ["ready", "unavailable"] },
|
|
106
|
+
"name": { "$ref": "#/$defs/nullableDisplayText" },
|
|
107
|
+
"relativePath": { "$ref": "#/$defs/nullableDisplayText" },
|
|
108
|
+
"rootRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
|
|
109
|
+
"format": { "enum": ["text", "docx", "pdf", null] },
|
|
110
|
+
"text": {
|
|
111
|
+
"anyOf": [
|
|
112
|
+
{ "type": "string", "maxLength": 400000 },
|
|
113
|
+
{ "type": "null" }
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"sizeBytes": {
|
|
117
|
+
"anyOf": [
|
|
118
|
+
{ "type": "integer", "minimum": 1, "maximum": 12582912 },
|
|
119
|
+
{ "type": "null" }
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"truncated": { "type": "boolean" },
|
|
123
|
+
"redacted": { "type": "boolean" },
|
|
124
|
+
"reasonCode": { "$ref": "common-v1.schema.json#/$defs/nullableReasonCode" }
|
|
125
|
+
},
|
|
126
|
+
"allOf": [
|
|
127
|
+
{
|
|
128
|
+
"if": { "properties": { "state": { "const": "ready" } }, "required": ["state"] },
|
|
129
|
+
"then": {
|
|
130
|
+
"properties": {
|
|
131
|
+
"name": { "$ref": "#/$defs/displayText" },
|
|
132
|
+
"relativePath": { "$ref": "#/$defs/displayText" },
|
|
133
|
+
"rootRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
134
|
+
"format": { "enum": ["text", "docx", "pdf"] },
|
|
135
|
+
"text": { "type": "string", "maxLength": 400000 },
|
|
136
|
+
"reasonCode": { "type": "null" }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"if": { "properties": { "state": { "const": "unavailable" } }, "required": ["state"] },
|
|
142
|
+
"then": {
|
|
143
|
+
"properties": {
|
|
144
|
+
"text": { "type": "null" },
|
|
145
|
+
"format": { "type": "null" },
|
|
146
|
+
"truncated": { "const": false },
|
|
147
|
+
"redacted": { "const": false },
|
|
148
|
+
"reasonCode": { "$ref": "common-v1.schema.json#/$defs/reasonCode" }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -56,7 +56,11 @@
|
|
|
56
56
|
"modelRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
|
|
57
57
|
"thinkingLevel": { "$ref": "#/$defs/thinkingLevel" },
|
|
58
58
|
"message": { "type": "string", "minLength": 1, "maxLength": 32768, "pattern": "^[^\\u0000]*$" },
|
|
59
|
-
"messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" }
|
|
59
|
+
"messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
60
|
+
"deferInitialMessage": {
|
|
61
|
+
"description": "Create the durable session without dispatching the first turn so the client can stage selected attachments against the new session before sending it.",
|
|
62
|
+
"type": "boolean"
|
|
63
|
+
}
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -75,12 +79,19 @@
|
|
|
75
79
|
"payload": {
|
|
76
80
|
"type": "object",
|
|
77
81
|
"additionalProperties": false,
|
|
78
|
-
"required": ["delivery", "message", "messageRequestId", "expectedOperationRef"],
|
|
82
|
+
"required": ["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"],
|
|
79
83
|
"properties": {
|
|
80
84
|
"delivery": { "enum": ["new-operation", "follow-up", "steer"] },
|
|
81
85
|
"message": { "type": "string", "minLength": 1, "maxLength": 32768, "pattern": "^[^\\u0000]*$" },
|
|
82
86
|
"messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
83
|
-
"expectedOperationRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" }
|
|
87
|
+
"expectedOperationRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
|
|
88
|
+
"attachmentRefs": {
|
|
89
|
+
"description": "Refs staged for this exact messageRequestId. Claimed once at dispatch and consumed.",
|
|
90
|
+
"type": "array",
|
|
91
|
+
"maxItems": 4,
|
|
92
|
+
"uniqueItems": true,
|
|
93
|
+
"items": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" }
|
|
94
|
+
}
|
|
84
95
|
},
|
|
85
96
|
"allOf": [
|
|
86
97
|
{
|
|
@@ -188,6 +188,12 @@
|
|
|
188
188
|
"agentOperationId": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
|
|
189
189
|
"turnIndex": { "$ref": "#/$defs/nullableCount" },
|
|
190
190
|
"content": { "$ref": "#/$defs/content" },
|
|
191
|
+
"attachments": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"maxItems": 4,
|
|
194
|
+
"uniqueItems": true,
|
|
195
|
+
"items": { "$ref": "#/$defs/attachmentSummary" }
|
|
196
|
+
},
|
|
191
197
|
"toolCalls": {
|
|
192
198
|
"type": "array",
|
|
193
199
|
"maxItems": 64,
|
|
@@ -217,6 +223,27 @@
|
|
|
217
223
|
}
|
|
218
224
|
]
|
|
219
225
|
},
|
|
226
|
+
"attachmentSummary": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"additionalProperties": false,
|
|
229
|
+
"required": ["displayName", "kind", "mimeType", "truncated"],
|
|
230
|
+
"properties": {
|
|
231
|
+
"displayName": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"minLength": 1,
|
|
234
|
+
"maxLength": 160,
|
|
235
|
+
"pattern": "^[^\\u0000-\\u001F\\u007F/\\\\]*$"
|
|
236
|
+
},
|
|
237
|
+
"kind": { "enum": ["file", "image", "document"] },
|
|
238
|
+
"mimeType": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
"minLength": 1,
|
|
241
|
+
"maxLength": 120,
|
|
242
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9.+-]*/[A-Za-z0-9][A-Za-z0-9.+-]*$"
|
|
243
|
+
},
|
|
244
|
+
"truncated": { "type": "boolean" }
|
|
245
|
+
}
|
|
246
|
+
},
|
|
220
247
|
"page": {
|
|
221
248
|
"type": "object",
|
|
222
249
|
"additionalProperties": false,
|