@pasko70/pibo 2.5.2 → 3.0.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/context/pibo-native-tooling.md +1 -0
- package/dist/agent-runtime/contract.js +2 -1
- package/dist/agent-runtime/history.js +53 -1
- package/dist/agent-runtime/routed-session.js +100 -26
- package/dist/agent-runtimes/codex-native/adapter.js +304 -30
- package/dist/agent-runtimes/codex-native/client.js +22 -3
- package/dist/agent-runtimes/codex-native/first-use.js +294 -0
- package/dist/agent-runtimes/codex-native/history.js +26 -7
- package/dist/agent-runtimes/codex-native/process.js +14 -0
- package/dist/agent-runtimes/codex-native/thread.js +59 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -0
- package/dist/agent-runtimes/history-proof.js +8 -0
- package/dist/agent-runtimes/omp/adapter.js +61 -14
- package/dist/agent-runtimes/omp/history.js +189 -34
- package/dist/agent-runtimes/omp/thread.js +82 -36
- package/dist/agent-runtimes/pi/adapter.js +30 -3
- package/dist/agent-runtimes/pi/history.js +66 -5
- package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
- package/dist/agent-runtimes/pi/routed-session.js +344 -37
- package/dist/apps/chat/chat-api-routes.js +1 -1
- package/dist/apps/chat/chat-files.js +130 -9
- package/dist/apps/chat/chat-settings-routes.js +27 -2
- package/dist/apps/chat/chat-speech.js +100 -0
- package/dist/apps/chat/data/chat-data-mappers.js +25 -5
- package/dist/apps/chat/data/timeline-query-service.js +89 -1
- package/dist/apps/chat/trace-v2.js +210 -8
- package/dist/apps/chat/trace.js +3 -0
- package/dist/apps/chat/web-app.js +108 -3
- package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
- package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
- package/dist/cli.js +49 -5
- package/dist/config/config.js +59 -8
- package/dist/core/preview-server-settings.js +20 -0
- package/dist/core/session-router.js +457 -37
- package/dist/core/user-settings.js +10 -0
- package/dist/data/payload-store.js +57 -1
- package/dist/data/schema.js +18 -1
- package/dist/debug/trace.js +5 -0
- package/dist/gateway/client.js +368 -65
- package/dist/gateway/protocol.js +2 -0
- package/dist/gateway/server.js +8 -0
- package/dist/gateway/web.js +2 -0
- package/dist/index.js +1 -1
- package/dist/loops/store.js +6 -4
- package/dist/mcp/config.js +89 -17
- package/dist/mcp/daemon-client.js +473 -153
- package/dist/mcp/daemon.js +401 -37
- package/dist/plugins/builtin.js +2 -2
- package/dist/plugins/codex-native.js +2 -0
- package/dist/plugins/registry.js +259 -1
- package/dist/previews/cli.js +345 -0
- package/dist/previews/config.js +61 -0
- package/dist/previews/manager.js +644 -0
- package/dist/previews/network.js +198 -0
- package/dist/previews/plugin.js +11 -0
- package/dist/previews/process-supervisor.js +38 -0
- package/dist/previews/proxy.js +387 -0
- package/dist/previews/store.js +750 -0
- package/dist/previews/types.js +1 -0
- package/dist/previews/web-app.js +446 -0
- package/dist/runs/registry.js +22 -0
- package/dist/runs/tools.js +46 -7
- package/dist/session-ui/terminalRows.js +187 -13
- package/dist/sessions/pibo-data-store.js +37 -0
- package/dist/sessions/runtime-binding-persistence.js +34 -0
- package/dist/sessions/runtime-binding.js +6 -0
- package/dist/sessions/sqlite-store.js +37 -0
- package/dist/shared/trace-engine.js +12 -64
- package/dist/shared/trace-event-projection.js +16 -0
- package/dist/shared/trace-history.js +684 -184
- package/dist/shared/trace-limits.js +7 -0
- package/dist/shared/trace-live-patch.js +61 -0
- package/dist/shared/trace-tool-identity.js +43 -0
- package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
- package/dist/speech/openai-codex.js +407 -0
- package/dist/speech/types.js +8 -0
- package/dist/subagents/context.js +5 -3
- package/dist/subagents/observations.js +94 -6
- package/dist/subagents/tool.js +103 -20
- package/dist/tools/codex-image-artifacts.js +24 -0
- package/dist/tools/codex-image-generation.js +4 -22
- package/dist/web/channel.js +88 -4
- package/dist/web/http.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
- package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createReadStream, existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, constants, createReadStream, existsSync, fstatSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, extname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
import { piboHomePath } from "../../core/pibo-home.js";
|
|
5
5
|
import { protectPrivateDirectorySync, protectPrivateFileSync } from "../../core/private-path.js";
|
|
6
6
|
import { PiboWebHttpError } from "../../web/http.js";
|
|
7
|
+
import { imageMimeTypeFromBytes, TRACE_IMAGE_MAX_DECODED_BYTES } from "./trace-v2.js";
|
|
7
8
|
export const CHAT_UPLOAD_DIR = piboHomePath("uploads");
|
|
8
9
|
const CHAT_FILE_ATTACHMENT_LIMIT = 10;
|
|
9
10
|
export function prepareChatFileAttachments(input) {
|
|
@@ -46,16 +47,31 @@ export async function saveUploadedChatFiles(request) {
|
|
|
46
47
|
export function resolveDownloadPath(path, basePath) {
|
|
47
48
|
return isAbsolute(path) ? resolve(path) : resolve(basePath, path);
|
|
48
49
|
}
|
|
49
|
-
export function
|
|
50
|
-
|
|
50
|
+
export function resolveImagePreviewPath(path, basePath) {
|
|
51
|
+
return resolveImagePreviewPathWithinRoots(resolveDownloadPath(path, basePath), [basePath, CHAT_UPLOAD_DIR]);
|
|
52
|
+
}
|
|
53
|
+
export function resolveImagePreviewPathWithinRoots(path, allowedRootPaths) {
|
|
54
|
+
let canonicalPath;
|
|
51
55
|
try {
|
|
52
|
-
|
|
56
|
+
canonicalPath = realpathSync(path);
|
|
53
57
|
}
|
|
54
58
|
catch {
|
|
55
|
-
throw new PiboWebHttpError("
|
|
59
|
+
throw new PiboWebHttpError("Image preview file was not found", 404);
|
|
56
60
|
}
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const allowedRoots = [];
|
|
62
|
+
for (const allowedRoot of allowedRootPaths) {
|
|
63
|
+
if (!existsSync(allowedRoot))
|
|
64
|
+
continue;
|
|
65
|
+
const canonicalRoot = realpathSync(allowedRoot);
|
|
66
|
+
allowedRoots.push(canonicalRoot);
|
|
67
|
+
}
|
|
68
|
+
if (allowedRoots.some((allowedRoot) => isPathInsideRoot(canonicalPath, allowedRoot))) {
|
|
69
|
+
return { path: canonicalPath, allowedRoots };
|
|
70
|
+
}
|
|
71
|
+
throw new PiboWebHttpError("Image preview path is outside its authorized roots", 403);
|
|
72
|
+
}
|
|
73
|
+
export function responseChatFileDownload(absolutePath) {
|
|
74
|
+
const stats = requireChatFile(absolutePath);
|
|
59
75
|
return new Response(Readable.toWeb(createReadStream(absolutePath)), {
|
|
60
76
|
headers: {
|
|
61
77
|
"content-type": contentTypeForDownload(absolutePath),
|
|
@@ -65,6 +81,108 @@ export function responseChatFileDownload(absolutePath) {
|
|
|
65
81
|
},
|
|
66
82
|
});
|
|
67
83
|
}
|
|
84
|
+
export function responseChatImagePreview(absolutePath, allowedRoots) {
|
|
85
|
+
const { bytes, stats } = readBoundedImageFile(absolutePath, TRACE_IMAGE_MAX_DECODED_BYTES, allowedRoots);
|
|
86
|
+
const mimeType = imageMimeTypeFromBytes(bytes.subarray(0, Math.min(32, bytes.byteLength)));
|
|
87
|
+
if (!mimeType)
|
|
88
|
+
throw new PiboWebHttpError("Unsupported image preview format", 415);
|
|
89
|
+
return new Response(bytes, {
|
|
90
|
+
headers: imagePreviewHeaders({
|
|
91
|
+
mimeType,
|
|
92
|
+
contentLength: stats.size,
|
|
93
|
+
filename: basename(absolutePath),
|
|
94
|
+
cacheControl: "no-store",
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function responseChatTraceImage(image) {
|
|
99
|
+
return new Response(image.bytes, {
|
|
100
|
+
headers: imagePreviewHeaders({
|
|
101
|
+
mimeType: image.mimeType,
|
|
102
|
+
contentLength: image.bytes.byteLength,
|
|
103
|
+
cacheControl: "private, max-age=31536000, immutable",
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function requireChatFile(absolutePath) {
|
|
108
|
+
let stats;
|
|
109
|
+
try {
|
|
110
|
+
stats = statSync(absolutePath);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new PiboWebHttpError("File not found: " + absolutePath, 404);
|
|
114
|
+
}
|
|
115
|
+
if (!stats.isFile())
|
|
116
|
+
throw new PiboWebHttpError("Path is not a file: " + absolutePath, 400);
|
|
117
|
+
return stats;
|
|
118
|
+
}
|
|
119
|
+
function readBoundedImageFile(absolutePath, maxBytes, allowedRoots) {
|
|
120
|
+
let descriptor;
|
|
121
|
+
try {
|
|
122
|
+
descriptor = openSync(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (isFileSystemError(error) && error.code === "ELOOP")
|
|
126
|
+
throw new PiboWebHttpError("Image preview path changed during authorization", 409);
|
|
127
|
+
throw new PiboWebHttpError("File not found: " + absolutePath, 404);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const stats = fstatSync(descriptor);
|
|
131
|
+
if (!stats.isFile())
|
|
132
|
+
throw new PiboWebHttpError("Path is not a file: " + absolutePath, 400);
|
|
133
|
+
if (allowedRoots?.length)
|
|
134
|
+
assertOpenedImagePathAuthority(absolutePath, stats, allowedRoots);
|
|
135
|
+
if (stats.size > maxBytes)
|
|
136
|
+
throw new PiboWebHttpError(`Image preview exceeds the ${maxBytes}-byte limit`, 413);
|
|
137
|
+
const bytes = Buffer.alloc(stats.size);
|
|
138
|
+
let offset = 0;
|
|
139
|
+
while (offset < bytes.byteLength) {
|
|
140
|
+
const read = readSync(descriptor, bytes, offset, bytes.byteLength - offset, offset);
|
|
141
|
+
if (read === 0)
|
|
142
|
+
break;
|
|
143
|
+
offset += read;
|
|
144
|
+
}
|
|
145
|
+
if (offset !== bytes.byteLength)
|
|
146
|
+
throw new PiboWebHttpError("Image changed while it was being read", 409);
|
|
147
|
+
const extra = Buffer.alloc(1);
|
|
148
|
+
if (readSync(descriptor, extra, 0, 1, offset) !== 0)
|
|
149
|
+
throw new PiboWebHttpError(`Image preview exceeds the ${maxBytes}-byte limit`, 413);
|
|
150
|
+
return { bytes, stats };
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
closeSync(descriptor);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function assertOpenedImagePathAuthority(absolutePath, openedStats, allowedRoots) {
|
|
157
|
+
let currentPath;
|
|
158
|
+
let currentStats;
|
|
159
|
+
try {
|
|
160
|
+
currentPath = realpathSync(absolutePath);
|
|
161
|
+
currentStats = statSync(currentPath);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new PiboWebHttpError("Image preview path changed during authorization", 409);
|
|
165
|
+
}
|
|
166
|
+
if (currentStats.dev !== openedStats.dev || currentStats.ino !== openedStats.ino) {
|
|
167
|
+
throw new PiboWebHttpError("Image preview file changed during authorization", 409);
|
|
168
|
+
}
|
|
169
|
+
if (!allowedRoots.some((allowedRoot) => isPathInsideRoot(currentPath, allowedRoot))) {
|
|
170
|
+
throw new PiboWebHttpError("Image preview path escaped its authorized root", 403);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function isFileSystemError(error) {
|
|
174
|
+
return error instanceof Error && "code" in error;
|
|
175
|
+
}
|
|
176
|
+
function imagePreviewHeaders(input) {
|
|
177
|
+
return {
|
|
178
|
+
"content-type": input.mimeType,
|
|
179
|
+
"content-length": String(input.contentLength),
|
|
180
|
+
...(input.filename ? { "content-disposition": "inline; filename*=UTF-8''" + encodeURIComponent(input.filename) } : {}),
|
|
181
|
+
"cache-control": input.cacheControl,
|
|
182
|
+
"x-content-type-options": "nosniff",
|
|
183
|
+
"cross-origin-resource-policy": "same-origin",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
68
186
|
function normalizeChatFileAttachmentPaths(value) {
|
|
69
187
|
if (value === undefined || value === null)
|
|
70
188
|
return [];
|
|
@@ -100,8 +218,11 @@ function chatFileAttachmentForPath(path) {
|
|
|
100
218
|
return { name: basename(path), path, bytes: stats.size };
|
|
101
219
|
}
|
|
102
220
|
function isPathInsideUploadDir(path) {
|
|
103
|
-
|
|
104
|
-
|
|
221
|
+
return isPathInsideRoot(path, CHAT_UPLOAD_DIR);
|
|
222
|
+
}
|
|
223
|
+
function isPathInsideRoot(path, root) {
|
|
224
|
+
const child = relative(root, path);
|
|
225
|
+
return child !== "" && child !== ".." && !child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(child);
|
|
105
226
|
}
|
|
106
227
|
function renderAttachedChatFiles(attachments) {
|
|
107
228
|
const bounded = attachments.slice(0, CHAT_FILE_ATTACHMENT_LIMIT);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readPiboBasePrompt, savePiboCustomBasePrompt, setPiboBasePromptMode } from "../../core/base-prompt.js";
|
|
2
2
|
import { readPiboCompactionPrompt, savePiboCustomCompactionPrompt, setPiboCompactionPromptMode } from "../../core/compaction-prompt.js";
|
|
3
3
|
import { loadPiboGatewaySettings, sanitizeConcurrentYieldedRuns, updatePiboGatewaySettings, } from "../../core/gateway-settings.js";
|
|
4
|
+
import { sanitizePreviewServerSettings } from "../../core/preview-server-settings.js";
|
|
4
5
|
import { sanitizeTelemetryRetentionDays, sanitizeTelemetryRetentionSettings } from "../../core/telemetry-retention-settings.js";
|
|
5
6
|
import { loadPiboUserSettings, sanitizeShortcutSettings, sanitizeTimezone, sanitizeTranscriptionProviderId, updatePiboUserSettings, updateTelemetryRetentionLastPrunedAt } from "../../core/user-settings.js";
|
|
6
7
|
import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
|
|
@@ -51,7 +52,9 @@ export async function handleChatSettingsRoute(input) {
|
|
|
51
52
|
if (route.action === "read")
|
|
52
53
|
return responseJson({ userSettings: loadPiboUserSettings() });
|
|
53
54
|
const body = await readJsonBody(request);
|
|
54
|
-
return responseJson({
|
|
55
|
+
return responseJson({
|
|
56
|
+
userSettings: updatePiboUserSettings(userSettingsPatch(body, input.transcriptionProviderIds, input.speechProviderIds)),
|
|
57
|
+
});
|
|
55
58
|
}
|
|
56
59
|
if (route.kind === "gateway-settings") {
|
|
57
60
|
if (route.action === "read")
|
|
@@ -106,7 +109,7 @@ function gatewaySettingsPatch(body) {
|
|
|
106
109
|
throw new PiboWebHttpError("No gateway settings provided", 400);
|
|
107
110
|
return patch;
|
|
108
111
|
}
|
|
109
|
-
function userSettingsPatch(body, transcriptionProviderIds) {
|
|
112
|
+
function userSettingsPatch(body, transcriptionProviderIds, speechProviderIds) {
|
|
110
113
|
const patch = {};
|
|
111
114
|
if (body.timezone !== undefined) {
|
|
112
115
|
const timezone = sanitizeTimezone(body.timezone);
|
|
@@ -128,6 +131,28 @@ function userSettingsPatch(body, transcriptionProviderIds) {
|
|
|
128
131
|
}
|
|
129
132
|
patch.transcription = { providerId };
|
|
130
133
|
}
|
|
134
|
+
if (body.speech !== undefined) {
|
|
135
|
+
const raw = body.speech && typeof body.speech === "object" && !Array.isArray(body.speech)
|
|
136
|
+
? body.speech
|
|
137
|
+
: {};
|
|
138
|
+
const providerId = sanitizeTranscriptionProviderId(raw.providerId);
|
|
139
|
+
if (!providerId)
|
|
140
|
+
throw new PiboWebHttpError("Invalid speech provider", 400);
|
|
141
|
+
if (speechProviderIds && !speechProviderIds.includes(providerId)) {
|
|
142
|
+
throw new PiboWebHttpError(`Unknown speech provider "${providerId}"`, 400);
|
|
143
|
+
}
|
|
144
|
+
patch.speech = { providerId };
|
|
145
|
+
}
|
|
146
|
+
if (body.previewServers !== undefined) {
|
|
147
|
+
const sanitized = sanitizePreviewServerSettings(body.previewServers);
|
|
148
|
+
const raw = body.previewServers && typeof body.previewServers === "object" && !Array.isArray(body.previewServers)
|
|
149
|
+
? body.previewServers
|
|
150
|
+
: {};
|
|
151
|
+
if (raw.maxRunningServers !== sanitized.maxRunningServers || raw.autoStopMinutes !== sanitized.autoStopMinutes) {
|
|
152
|
+
throw new PiboWebHttpError("Invalid Preview server settings", 400);
|
|
153
|
+
}
|
|
154
|
+
patch.previewServers = sanitized;
|
|
155
|
+
}
|
|
131
156
|
if (body.telemetryRetention !== undefined) {
|
|
132
157
|
const current = loadPiboUserSettings().telemetryRetention;
|
|
133
158
|
patch.telemetryRetention = {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { loadPiboUserSettings } from "../../core/user-settings.js";
|
|
2
|
+
import { PiboSpeechError, } from "../../speech/types.js";
|
|
3
|
+
import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
|
|
4
|
+
export const CHAT_SPEECH_MAX_CHARACTERS = 32_000;
|
|
5
|
+
export const CHAT_SPEECH_MAX_OFFER_CHARACTERS = 256_000;
|
|
6
|
+
export async function responseChatSpeechProviders(context) {
|
|
7
|
+
const providers = await requireProviderInfos(context);
|
|
8
|
+
return responseJson({
|
|
9
|
+
providers,
|
|
10
|
+
selectedProviderId: loadPiboUserSettings().speech.providerId,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function responseChatSpeechSessionStart(request, context) {
|
|
14
|
+
const startSpeechSession = context.channelContext.startSpeechSession;
|
|
15
|
+
if (!startSpeechSession)
|
|
16
|
+
throw new PiboWebHttpError("Speech synthesis is not available", 503);
|
|
17
|
+
const providerId = loadPiboUserSettings().speech.providerId;
|
|
18
|
+
const providers = await requireProviderInfos(context);
|
|
19
|
+
if (!providers.some((provider) => provider.id === providerId)) {
|
|
20
|
+
throw new PiboWebHttpError(`Speech provider "${providerId}" is not registered`, 409);
|
|
21
|
+
}
|
|
22
|
+
const body = await readJsonBody(request);
|
|
23
|
+
if (typeof body.offerSdp !== "string" || !body.offerSdp.startsWith("v=0")) {
|
|
24
|
+
throw new PiboWebHttpError("A WebRTC audio offer is required", 400);
|
|
25
|
+
}
|
|
26
|
+
if (body.offerSdp.length > CHAT_SPEECH_MAX_OFFER_CHARACTERS) {
|
|
27
|
+
throw new PiboWebHttpError("The WebRTC audio offer is too large", 413);
|
|
28
|
+
}
|
|
29
|
+
const text = requireSpeechText(body.text);
|
|
30
|
+
let speechSession;
|
|
31
|
+
try {
|
|
32
|
+
speechSession = await startSpeechSession(providerId, { offerSdp: body.offerSdp, text }, { signal: request.signal });
|
|
33
|
+
if (request.signal.aborted)
|
|
34
|
+
throw request.signal.reason;
|
|
35
|
+
return responseJson({ speechSession }, { status: 201 });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (speechSession)
|
|
39
|
+
await context.channelContext.stopSpeechSession?.(speechSession.sessionId).catch(() => { });
|
|
40
|
+
throw speechHttpError(error, "Speech session failed to start");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function responseChatSpeechSessionSpeak(request, context, sessionId) {
|
|
44
|
+
const speakSpeechSession = context.channelContext.speakSpeechSession;
|
|
45
|
+
if (!speakSpeechSession)
|
|
46
|
+
throw new PiboWebHttpError("Speech synthesis is not available", 503);
|
|
47
|
+
const body = await readJsonBody(request);
|
|
48
|
+
const text = requireSpeechText(body.text);
|
|
49
|
+
try {
|
|
50
|
+
await speakSpeechSession(sessionId, { text });
|
|
51
|
+
return new Response(null, { status: 204 });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw speechHttpError(error, "Speech synthesis failed");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function responseChatSpeechSessionStop(context, sessionId) {
|
|
58
|
+
const stopSpeechSession = context.channelContext.stopSpeechSession;
|
|
59
|
+
if (!stopSpeechSession)
|
|
60
|
+
throw new PiboWebHttpError("Speech synthesis is not available", 503);
|
|
61
|
+
try {
|
|
62
|
+
await stopSpeechSession(sessionId);
|
|
63
|
+
return new Response(null, { status: 204 });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw speechHttpError(error, "Speech session could not be stopped");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function requireSpeechText(value) {
|
|
70
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
71
|
+
throw new PiboWebHttpError("Speech text is required", 400);
|
|
72
|
+
}
|
|
73
|
+
const text = value.trim();
|
|
74
|
+
if (text.length > CHAT_SPEECH_MAX_CHARACTERS) {
|
|
75
|
+
throw new PiboWebHttpError(`Speech text exceeds the ${CHAT_SPEECH_MAX_CHARACTERS.toLocaleString("en-US")} character limit`, 413);
|
|
76
|
+
}
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
async function requireProviderInfos(context) {
|
|
80
|
+
const getProviderInfos = context.channelContext.getSpeechProviderInfos;
|
|
81
|
+
if (!getProviderInfos)
|
|
82
|
+
throw new PiboWebHttpError("Speech providers are not available", 503);
|
|
83
|
+
return await getProviderInfos();
|
|
84
|
+
}
|
|
85
|
+
function speechHttpError(error, fallback) {
|
|
86
|
+
if (error instanceof PiboSpeechError) {
|
|
87
|
+
if (error.code === "invalid_offer" || error.code === "invalid_text")
|
|
88
|
+
return new PiboWebHttpError(error.message, 400);
|
|
89
|
+
if (error.code === "session_not_found")
|
|
90
|
+
return new PiboWebHttpError(error.message, 404);
|
|
91
|
+
if (error.code === "not_configured")
|
|
92
|
+
return new PiboWebHttpError(error.message, 409);
|
|
93
|
+
if (error.code === "capacity_exceeded")
|
|
94
|
+
return new PiboWebHttpError(error.message, 429);
|
|
95
|
+
if (error.code === "aborted")
|
|
96
|
+
return new PiboWebHttpError(error.message, 499);
|
|
97
|
+
return new PiboWebHttpError(error.message, 502);
|
|
98
|
+
}
|
|
99
|
+
return new PiboWebHttpError(error instanceof Error ? error.message : fallback, 502);
|
|
100
|
+
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { normalizeSessionErrorDetails } from "../../../core/session-errors.js";
|
|
2
2
|
import { roomWorkspaceFromMetadata } from "../types/rooms.js";
|
|
3
|
+
import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
|
|
4
|
+
const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
|
|
3
5
|
export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
4
6
|
const attributes = parseJsonObject(row.attributes_json);
|
|
5
7
|
const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore));
|
|
6
8
|
if (!payload)
|
|
7
9
|
return undefined;
|
|
8
|
-
|
|
10
|
+
const payloadIdentity = tracePayloadIdentityForEvent(row, attributes);
|
|
11
|
+
const storedPayloadRef = row.payload_ref && row.session_id && payloadStore && payloadIdentity
|
|
12
|
+
? tracePayloadRefForStoredPayload({
|
|
13
|
+
payloadStore,
|
|
14
|
+
piboSessionId: row.session_id,
|
|
15
|
+
payloadId: row.payload_ref,
|
|
16
|
+
...payloadIdentity,
|
|
17
|
+
})
|
|
18
|
+
: undefined;
|
|
19
|
+
return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
|
|
9
20
|
}
|
|
10
21
|
export function storedChatEventFromV2Row(row, payloadStore) {
|
|
11
22
|
const attributes = parseJsonObject(row.attributes_json);
|
|
@@ -76,16 +87,25 @@ function readPersistedPayload(row, payloadStore) {
|
|
|
76
87
|
return undefined;
|
|
77
88
|
try {
|
|
78
89
|
const metadata = payloadStore.getPayload(row.payload_ref);
|
|
79
|
-
if (!metadata)
|
|
90
|
+
if (!metadata || metadata.byteSize > MAX_TRACE_EVENT_HYDRATION_BYTES)
|
|
80
91
|
return undefined;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: payloadStore.readPayloadText(row.payload_ref);
|
|
92
|
+
const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, MAX_TRACE_EVENT_HYDRATION_BYTES)).toString("utf8");
|
|
93
|
+
return metadata.contentType.includes("json") ? JSON.parse(text) : text;
|
|
84
94
|
}
|
|
85
95
|
catch {
|
|
86
96
|
return undefined;
|
|
87
97
|
}
|
|
88
98
|
}
|
|
99
|
+
function tracePayloadIdentityForEvent(row, attributes) {
|
|
100
|
+
const toolCallId = stringAttribute(attributes, "toolCallId");
|
|
101
|
+
if (!toolCallId)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (row.type === "tool_call" || row.type === "tool_execution_started")
|
|
104
|
+
return { nodeId: `tool:${toolCallId}`, payloadKind: "input" };
|
|
105
|
+
if (row.type === "tool_execution_updated" || row.type === "tool_execution_finished")
|
|
106
|
+
return { nodeId: `tool:${toolCallId}`, payloadKind: "output" };
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
89
109
|
export function sessionFromRow(row) { return { piboSessionId: row.id, piSessionId: row.pi_session_id ?? "", runtimeInstanceId: row.runtime_instance_id ?? undefined, runtimeAdapterId: row.runtime_adapter_id ?? undefined, runtimeBindingState: isRuntimeBindingState(row.binding_state) ? row.binding_state : undefined, nativeSessionId: row.native_session_id ?? undefined, parentId: row.parent_id ?? undefined, profile: row.profile, channel: row.channel, kind: row.kind, createdAt: row.created_at, updatedAt: row.updated_at, lastActivityAt: row.last_activity_at, status: row.status === "running" || row.status === "error" ? row.status : "idle" }; }
|
|
90
110
|
export function roomFromRow(row) { const metadata = parseJsonObject(row.metadata_json); return { id: row.id, name: row.name, topic: row.topic ?? undefined, workspace: row.workspace ?? roomWorkspaceFromMetadata(metadata), type: row.type, parentRoomId: row.parent_room_id ?? undefined, createdAt: row.created_at, updatedAt: row.updated_at, retentionPolicyId: row.retention_policy_id ?? undefined, metadata }; }
|
|
91
111
|
export function parseJsonObject(value) { try {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { messageTurnTimingsFromEvents } from "../../../shared/trace-event-projection.js";
|
|
2
3
|
import { storedChatEventFromV2Row, storedPiboEventFromV2Row } from "./chat-data-mappers.js";
|
|
4
|
+
import { TRACE_RECONCILIATION_ENTRY_CAP, TRACE_RECONCILIATION_TIMING_CAP } from "../../../shared/trace-limits.js";
|
|
5
|
+
import { parseTraceToolNodeIdentity } from "../../../shared/trace-tool-identity.js";
|
|
3
6
|
export class ChatTimelineQueryService {
|
|
4
7
|
store;
|
|
5
8
|
constructor(store) {
|
|
@@ -37,7 +40,10 @@ export class ChatTimelineQueryService {
|
|
|
37
40
|
WHERE session_id = ?
|
|
38
41
|
AND type IN ('message_queued', 'message_steered', 'message_started', 'message_finished', 'session_error', 'thinking_finished', 'assistant_message')
|
|
39
42
|
ORDER BY session_sequence ASC, stream_id ASC
|
|
40
|
-
|
|
43
|
+
LIMIT ?
|
|
44
|
+
`).all(piboSessionId, TRACE_RECONCILIATION_TIMING_CAP + 1);
|
|
45
|
+
if (rows.length > TRACE_RECONCILIATION_TIMING_CAP)
|
|
46
|
+
return [];
|
|
41
47
|
const events = rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
|
|
42
48
|
return messageTurnTimingsFromEvents(events);
|
|
43
49
|
}
|
|
@@ -70,6 +76,67 @@ export class ChatTimelineQueryService {
|
|
|
70
76
|
`).all(...values, limit);
|
|
71
77
|
return rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
|
|
72
78
|
}
|
|
79
|
+
isPayloadAttachedToTraceNode(input) {
|
|
80
|
+
const nodeIdentity = parseTraceToolNodeIdentity(input.nodeId);
|
|
81
|
+
if (!nodeIdentity)
|
|
82
|
+
return false;
|
|
83
|
+
const payload = this.store.payloads.getPayload(input.payloadId);
|
|
84
|
+
if (!payload)
|
|
85
|
+
return false;
|
|
86
|
+
const eventClause = nodeIdentity.qualifier ? "AND event_id = ?" : "";
|
|
87
|
+
const values = nodeIdentity.qualifier
|
|
88
|
+
? [input.piboSessionId, nodeIdentity.toolCallId, nodeIdentity.qualifier.eventId]
|
|
89
|
+
: [input.piboSessionId, nodeIdentity.toolCallId];
|
|
90
|
+
const rows = this.store.db.prepare(`
|
|
91
|
+
SELECT type, event_id, payload_ref, attributes_json
|
|
92
|
+
FROM event_log
|
|
93
|
+
WHERE session_id = ?
|
|
94
|
+
AND tool_call_id = ?
|
|
95
|
+
AND type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished')
|
|
96
|
+
${eventClause}
|
|
97
|
+
ORDER BY session_sequence ASC, stream_id ASC
|
|
98
|
+
LIMIT ?
|
|
99
|
+
`).all(...values, TRACE_RECONCILIATION_ENTRY_CAP + 1);
|
|
100
|
+
if (rows.length > TRACE_RECONCILIATION_ENTRY_CAP)
|
|
101
|
+
return false;
|
|
102
|
+
const lifecycles = new Map();
|
|
103
|
+
const invocations = [];
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
const lifecycle = lifecycles.get(row.event_id) ?? { nextOrdinal: 0 };
|
|
106
|
+
lifecycles.set(row.event_id, lifecycle);
|
|
107
|
+
if (row.type === "tool_execution_started") {
|
|
108
|
+
if (lifecycle.active)
|
|
109
|
+
return false;
|
|
110
|
+
lifecycle.active = {
|
|
111
|
+
eventId: row.event_id,
|
|
112
|
+
invocationOrdinal: lifecycle.nextOrdinal,
|
|
113
|
+
payloadMatches: false,
|
|
114
|
+
};
|
|
115
|
+
lifecycle.nextOrdinal += 1;
|
|
116
|
+
invocations.push(lifecycle.active);
|
|
117
|
+
}
|
|
118
|
+
else if (!lifecycle.active) {
|
|
119
|
+
lifecycle.active = {
|
|
120
|
+
eventId: row.event_id,
|
|
121
|
+
invocationOrdinal: lifecycle.nextOrdinal,
|
|
122
|
+
payloadMatches: false,
|
|
123
|
+
};
|
|
124
|
+
lifecycle.nextOrdinal += 1;
|
|
125
|
+
invocations.push(lifecycle.active);
|
|
126
|
+
}
|
|
127
|
+
if (row.type !== "tool_execution_started"
|
|
128
|
+
&& payloadMatchesEventRow(this.store, payload.sha256, input.payloadId, row))
|
|
129
|
+
lifecycle.active.payloadMatches = true;
|
|
130
|
+
if (row.type === "tool_execution_finished")
|
|
131
|
+
lifecycle.active = undefined;
|
|
132
|
+
}
|
|
133
|
+
if (!nodeIdentity.qualifier) {
|
|
134
|
+
return invocations.length === 1 && invocations[0].payloadMatches;
|
|
135
|
+
}
|
|
136
|
+
const exact = invocations.filter((invocation) => invocation.eventId === nodeIdentity.qualifier.eventId
|
|
137
|
+
&& invocation.invocationOrdinal === nodeIdentity.qualifier.invocationOrdinal);
|
|
138
|
+
return exact.length === 1 && exact[0].payloadMatches;
|
|
139
|
+
}
|
|
73
140
|
countEventsByType(input = {}) {
|
|
74
141
|
const clauses = [];
|
|
75
142
|
const values = [];
|
|
@@ -111,3 +178,24 @@ export class ChatTimelineQueryService {
|
|
|
111
178
|
return row?.stream_id ?? undefined;
|
|
112
179
|
}
|
|
113
180
|
}
|
|
181
|
+
function payloadMatchesEventRow(store, payloadSha256, payloadId, row) {
|
|
182
|
+
if (row.payload_ref === payloadId)
|
|
183
|
+
return true;
|
|
184
|
+
let attributes;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(row.attributes_json);
|
|
187
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
188
|
+
return false;
|
|
189
|
+
attributes = parsed;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (!("inlinePayload" in attributes))
|
|
195
|
+
return false;
|
|
196
|
+
const bytes = Buffer.from(JSON.stringify(attributes.inlinePayload), "utf8");
|
|
197
|
+
const inlineSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
198
|
+
if (payloadSha256 !== inlineSha256)
|
|
199
|
+
return false;
|
|
200
|
+
return store.payloads.findBySha256(inlineSha256)?.id === payloadId;
|
|
201
|
+
}
|