@pasko70/pibo 3.0.2 → 3.1.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/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +12 -6
- package/dist/agent-runtimes/pi/routed-session.js +10 -4
- package/dist/apps/chat/agent-store.js +55 -3
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/web-app.js +84 -25
- package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/output-render-sequence.js +148 -1
- package/dist/core/session-router.js +33 -105
- package/dist/data/ingest-service.js +14 -6
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +135 -5
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +2 -0
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +6 -6
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +174 -111
- package/dist/session-ui/terminalRows.js +2 -1
- package/dist/sessions/pibo-data-store.js +81 -0
- package/dist/subagents/observation-query.js +121 -0
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +8 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
- package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
FROM node:24-slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y \
|
|
4
|
+
python3 python3-venv python3-dev \
|
|
5
|
+
build-essential curl git ca-certificates \
|
|
6
|
+
chromium chromium-driver xvfb xauth x11-xserver-utils \
|
|
7
|
+
fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \
|
|
8
|
+
libatk1.0-0 libcups2 libdbus-1-3 libdrm2 libgbm1 libgtk-3-0 libnspr4 \
|
|
9
|
+
libnss3 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 xdg-utils \
|
|
10
|
+
dbus dbus-x11 \
|
|
11
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
12
|
+
|
|
13
|
+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
14
|
+
ENV PATH="/root/.local/bin:${PATH}"
|
|
15
|
+
ENV DISPLAY=:99
|
|
16
|
+
ENV PIBO_BROWSER_USE_CHROME=/usr/bin/chromium
|
|
17
|
+
|
|
18
|
+
# The host stages only the published package archive and this Dockerfile, so an
|
|
19
|
+
# installed package's production node_modules never enters the build context.
|
|
20
|
+
COPY pibo-package.tgz /tmp/pibo-package.tgz
|
|
21
|
+
|
|
22
|
+
WORKDIR /app
|
|
23
|
+
RUN npm init -y >/dev/null && \
|
|
24
|
+
npm install --omit=dev --no-audit --no-fund /tmp/pibo-package.tgz && \
|
|
25
|
+
rm -f /tmp/pibo-package.tgz
|
|
26
|
+
|
|
27
|
+
RUN mkdir -p /root/.pibo/tools/browser-use/home/bin && \
|
|
28
|
+
uv venv /root/.pibo/tools/browser-use/.venv --python 3.12 && \
|
|
29
|
+
uv pip install --python /root/.pibo/tools/browser-use/.venv/bin/python \
|
|
30
|
+
'browser-use[cli]==0.12.6'
|
|
31
|
+
|
|
32
|
+
RUN mkdir -p /root/.pibo/tools/agent-browser/home/bin \
|
|
33
|
+
/root/.pibo/tools/agent-browser/home/profiles \
|
|
34
|
+
/root/.pibo/tools/agent-browser/node && \
|
|
35
|
+
npm install --prefix /root/.pibo/tools/agent-browser/node agent-browser@0.27.0
|
|
36
|
+
|
|
37
|
+
WORKDIR /app/node_modules/@pasko70/pibo
|
|
38
|
+
RUN sed -i 's/\r$//' scripts/*.sh && \
|
|
39
|
+
chmod +x scripts/*.sh && \
|
|
40
|
+
scripts/prepare-browser-use-wrapper.sh && \
|
|
41
|
+
scripts/prepare-agent-browser-wrapper.sh && \
|
|
42
|
+
mkdir -p /root/.pibo /root/.browser-use /root/.pibo/tools/agent-browser/home
|
|
43
|
+
|
|
44
|
+
EXPOSE 4788 4789 56663
|
|
45
|
+
|
|
46
|
+
ENTRYPOINT ["/app/node_modules/@pasko70/pibo/scripts/docker-entrypoint.sh"]
|
|
47
|
+
CMD ["gateway:web"]
|
|
@@ -1,46 +1,96 @@
|
|
|
1
1
|
import { AgentRuntimeContractError } from "./errors.js";
|
|
2
|
+
import { validateAgentRuntimeCapabilities } from "./capabilities.js";
|
|
2
3
|
import { hasPendingNativeSession } from "../sessions/runtime-binding.js";
|
|
4
|
+
const REQUIRED_SESSION_METHODS = ["getBinding", "subscribe", "prompt", "abort", "dispose", "getStatus"];
|
|
5
|
+
const BINDING_STATES = new Set(["unbound", "bound", "missing", "error"]);
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
8
|
+
}
|
|
3
9
|
export function validateAgentRuntimeSessionContract(session) {
|
|
4
10
|
const errors = [];
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
if (!isRecord(session))
|
|
12
|
+
return ["session must be an object"];
|
|
13
|
+
for (const field of ["adapterId", "runtimeInstanceId", "cwd"]) {
|
|
14
|
+
if (typeof session[field] !== "string" || !session[field].trim()) {
|
|
15
|
+
errors.push(`session.${field} must be a non-empty string`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const method of REQUIRED_SESSION_METHODS) {
|
|
19
|
+
if (typeof session[method] !== "function")
|
|
20
|
+
errors.push(`session.${method}() is required`);
|
|
21
|
+
}
|
|
22
|
+
let capabilities;
|
|
23
|
+
if (!isRecord(session.capabilities)) {
|
|
24
|
+
errors.push("session.capabilities must be an object");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const capabilityErrors = validateAgentRuntimeCapabilities(session.capabilities);
|
|
28
|
+
errors.push(...capabilityErrors.map((error) => `session.capabilities.${error}`));
|
|
29
|
+
if (capabilityErrors.length === 0)
|
|
30
|
+
capabilities = session.capabilities;
|
|
31
|
+
}
|
|
32
|
+
const controls = isRecord(session.controls) ? session.controls : undefined;
|
|
33
|
+
if (session.controls !== undefined && !controls)
|
|
34
|
+
errors.push("session.controls must be an object when provided");
|
|
7
35
|
const requireMethod = (enabled, path, method) => {
|
|
8
36
|
if (enabled && typeof controls?.[method] !== "function") {
|
|
9
37
|
errors.push(`${path} requires controls.${String(method)}()`);
|
|
10
38
|
}
|
|
11
39
|
};
|
|
12
|
-
if (capabilities
|
|
40
|
+
if (capabilities?.input.steering && typeof session.steer !== "function") {
|
|
13
41
|
errors.push("input.steering requires session.steer()");
|
|
14
42
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
if (capabilities) {
|
|
44
|
+
requireMethod(capabilities.lifecycle.listNativeSessions, "lifecycle.listNativeSessions", "listSessions");
|
|
45
|
+
requireMethod(capabilities.lifecycle.fork, "lifecycle.fork", "forkSession");
|
|
46
|
+
requireMethod(capabilities.lifecycle.clone, "lifecycle.clone", "cloneSession");
|
|
47
|
+
requireMethod(capabilities.lifecycle.tree, "lifecycle.tree", "getSessionTree");
|
|
48
|
+
requireMethod(capabilities.lifecycle.tree, "lifecycle.tree", "navigateSessionTree");
|
|
49
|
+
requireMethod(capabilities.models.switchInSession, "models.switchInSession", "setModel");
|
|
50
|
+
requireMethod(capabilities.reasoning.supported, "reasoning.supported", "getReasoning");
|
|
51
|
+
requireMethod(capabilities.reasoning.supported, "reasoning.supported", "setReasoning");
|
|
52
|
+
requireMethod(capabilities.approvals.supported, "approvals.supported", "respondToApproval");
|
|
53
|
+
requireMethod(capabilities.approvals.structuredUserInput, "approvals.structuredUserInput", "respondToUserInput");
|
|
54
|
+
requireMethod(capabilities.maintenance.compaction, "maintenance.compaction", "compact");
|
|
55
|
+
}
|
|
56
|
+
if (typeof session.getBinding === "function") {
|
|
57
|
+
try {
|
|
58
|
+
const binding = session.getBinding();
|
|
59
|
+
if (!isRecord(binding)) {
|
|
60
|
+
errors.push("session.getBinding() must return an object");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
for (const field of ["piboSessionId", "runtimeInstanceId", "adapterId"]) {
|
|
64
|
+
if (typeof binding[field] !== "string" || !binding[field].trim()) {
|
|
65
|
+
errors.push(`session.getBinding().${field} must be a non-empty string`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!BINDING_STATES.has(String(binding.state))) {
|
|
69
|
+
errors.push("session.getBinding().state is invalid");
|
|
70
|
+
}
|
|
71
|
+
if (binding.adapterId !== session.adapterId) {
|
|
72
|
+
errors.push(`binding.adapterId "${String(binding.adapterId)}" does not match session.adapterId "${String(session.adapterId)}"`);
|
|
73
|
+
}
|
|
74
|
+
if (binding.runtimeInstanceId !== session.runtimeInstanceId) {
|
|
75
|
+
errors.push(`binding.runtimeInstanceId "${String(binding.runtimeInstanceId)}" does not match session.runtimeInstanceId "${String(session.runtimeInstanceId)}"`);
|
|
76
|
+
}
|
|
77
|
+
if (binding.state === "bound" && !binding.nativeSessionId) {
|
|
78
|
+
errors.push("a bound session requires binding.nativeSessionId");
|
|
79
|
+
}
|
|
80
|
+
if (binding.state === "unbound" && binding.nativeSessionId && !hasPendingNativeSession(binding)) {
|
|
81
|
+
errors.push("an unbound session must not expose binding.nativeSessionId");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
errors.push("session.getBinding() must not throw during contract validation");
|
|
87
|
+
}
|
|
38
88
|
}
|
|
39
89
|
return errors;
|
|
40
90
|
}
|
|
41
|
-
export function assertAgentRuntimeSessionContract(session) {
|
|
91
|
+
export function assertAgentRuntimeSessionContract(session, runtimeInstanceId) {
|
|
42
92
|
const errors = validateAgentRuntimeSessionContract(session);
|
|
43
93
|
if (errors.length > 0) {
|
|
44
|
-
throw new AgentRuntimeContractError(session.runtimeInstanceId, `Agent runtime session contract failed: ${errors.join("; ")}`);
|
|
94
|
+
throw new AgentRuntimeContractError(runtimeInstanceId ?? (isRecord(session) && typeof session.runtimeInstanceId === "string" ? session.runtimeInstanceId : "unknown"), `Agent runtime session contract failed: ${errors.join("; ")}`);
|
|
45
95
|
}
|
|
46
96
|
}
|
|
@@ -46,6 +46,9 @@ function boundedIdentifierText(value, fallback, maxLength = 512) {
|
|
|
46
46
|
function optionalBoundedIdentifierText(value) {
|
|
47
47
|
return value ? boundedIdentifierText(value, "portable-id") : undefined;
|
|
48
48
|
}
|
|
49
|
+
function toolInvocationKey(turnId, toolCallId) {
|
|
50
|
+
return JSON.stringify([turnId ?? null, toolCallId]);
|
|
51
|
+
}
|
|
49
52
|
function boundedText(value, maxBytes = MAX_ENTRY_BYTES) {
|
|
50
53
|
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
51
54
|
return value;
|
|
@@ -274,18 +277,22 @@ export class PiboDataPortableHistoryProvider {
|
|
|
274
277
|
for (const row of rows) {
|
|
275
278
|
if (!row.tool_call_id)
|
|
276
279
|
continue;
|
|
280
|
+
const invocationKey = toolInvocationKey(row.turn_id, row.tool_call_id);
|
|
277
281
|
if (row.type === "tool_call") {
|
|
278
282
|
const attributes = parseObject(row.attributes_json);
|
|
279
|
-
const existing = calls.get(
|
|
283
|
+
const existing = calls.get(invocationKey);
|
|
280
284
|
if (!existing || attributes.argsComplete === true)
|
|
281
|
-
calls.set(
|
|
285
|
+
calls.set(invocationKey, row);
|
|
282
286
|
}
|
|
283
287
|
else {
|
|
284
|
-
results.set(
|
|
288
|
+
results.set(invocationKey, row);
|
|
285
289
|
}
|
|
286
290
|
}
|
|
287
291
|
const entries = [];
|
|
288
|
-
for (const
|
|
292
|
+
for (const row of calls.values()) {
|
|
293
|
+
const toolCallId = row.tool_call_id;
|
|
294
|
+
if (!toolCallId)
|
|
295
|
+
continue;
|
|
289
296
|
const attributes = parseObject(row.attributes_json);
|
|
290
297
|
const portableToolCallId = boundedIdentifierText(toolCallId, `tool-call-${row.stream_id}`);
|
|
291
298
|
const toolName = boundedIdentifierText(typeof attributes.toolName === "string" ? attributes.toolName : row.preview_text ?? "tool", "tool", 256);
|
|
@@ -309,7 +316,10 @@ export class PiboDataPortableHistoryProvider {
|
|
|
309
316
|
toolName,
|
|
310
317
|
});
|
|
311
318
|
}
|
|
312
|
-
for (const
|
|
319
|
+
for (const row of results.values()) {
|
|
320
|
+
const toolCallId = row.tool_call_id;
|
|
321
|
+
if (!toolCallId)
|
|
322
|
+
continue;
|
|
313
323
|
const attributes = parseObject(row.attributes_json);
|
|
314
324
|
const portableToolCallId = boundedIdentifierText(toolCallId, `tool-call-${row.stream_id}`);
|
|
315
325
|
const toolName = boundedIdentifierText(typeof attributes.toolName === "string" ? attributes.toolName : row.preview_text ?? "tool", "tool", 256);
|
|
@@ -374,18 +384,18 @@ function normalizeToolPairs(entries) {
|
|
|
374
384
|
if (entry.type !== "message")
|
|
375
385
|
continue;
|
|
376
386
|
if (entry.role === "tool" && entry.toolCallId)
|
|
377
|
-
resultIds.add(entry.toolCallId);
|
|
387
|
+
resultIds.add(toolInvocationKey(entry.turnId, entry.toolCallId));
|
|
378
388
|
if (!Array.isArray(entry.content))
|
|
379
389
|
continue;
|
|
380
390
|
for (const part of entry.content) {
|
|
381
391
|
if (part.type === "tool_call")
|
|
382
|
-
callIds.add(part.toolCallId);
|
|
392
|
+
callIds.add(toolInvocationKey(entry.turnId, part.toolCallId));
|
|
383
393
|
}
|
|
384
394
|
}
|
|
385
395
|
return entries.map((entry) => {
|
|
386
396
|
if (entry.type !== "message")
|
|
387
397
|
return entry;
|
|
388
|
-
if (entry.role === "tool" && entry.toolCallId && !callIds.has(entry.toolCallId)) {
|
|
398
|
+
if (entry.role === "tool" && entry.toolCallId && !callIds.has(toolInvocationKey(entry.turnId, entry.toolCallId))) {
|
|
389
399
|
return {
|
|
390
400
|
...entry,
|
|
391
401
|
role: "user",
|
|
@@ -402,7 +412,7 @@ function normalizeToolPairs(entries) {
|
|
|
402
412
|
if (entry.role !== "assistant" || !Array.isArray(entry.content))
|
|
403
413
|
return entry;
|
|
404
414
|
const content = entry.content.map((part) => {
|
|
405
|
-
if (part.type !== "tool_call" || resultIds.has(part.toolCallId))
|
|
415
|
+
if (part.type !== "tool_call" || resultIds.has(toolInvocationKey(entry.turnId, part.toolCallId)))
|
|
406
416
|
return part;
|
|
407
417
|
return {
|
|
408
418
|
type: "text",
|
|
@@ -450,10 +450,16 @@ export class AgentRuntimeAdapterRegistry {
|
|
|
450
450
|
const adapter = this.requireInstance(instanceId);
|
|
451
451
|
const session = await adapter.openSession(input);
|
|
452
452
|
try {
|
|
453
|
-
assertAgentRuntimeSessionContract(session);
|
|
453
|
+
assertAgentRuntimeSessionContract(session, instanceId);
|
|
454
454
|
}
|
|
455
455
|
catch (error) {
|
|
456
|
-
|
|
456
|
+
try {
|
|
457
|
+
if (typeof session?.dispose === "function")
|
|
458
|
+
await session.dispose();
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// Preserve the admission error when best-effort cleanup also fails.
|
|
462
|
+
}
|
|
457
463
|
throw error;
|
|
458
464
|
}
|
|
459
465
|
return session;
|
|
@@ -14,7 +14,7 @@ const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
|
|
|
14
14
|
const RUN_REMINDER_MAX_TOOL_EXECUTIONS = 64;
|
|
15
15
|
const RUN_REMINDER_MAX_PROVIDER_ROUNDS = 64;
|
|
16
16
|
const RUN_REMINDER_MAX_ACTIVE_TOKENS = 2_000_000;
|
|
17
|
-
const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
|
|
17
|
+
export const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
|
|
18
18
|
const RUN_REMINDER_MAX_REPEATED_TOOL_CALLS = 12;
|
|
19
19
|
function serializedToolArgs(value) {
|
|
20
20
|
try {
|
|
@@ -208,14 +208,20 @@ export class RuntimeRoutedSession {
|
|
|
208
208
|
this.assertActive();
|
|
209
209
|
if (event.action === "compact")
|
|
210
210
|
return this.enqueueCompactAction(event);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
const sessionIdentityOperation = event.action === "session.fork"
|
|
212
|
+
? "fork"
|
|
213
|
+
: event.action === "session.clone"
|
|
214
|
+
? "clone"
|
|
215
|
+
: event.action === "session.switch"
|
|
216
|
+
? "switch"
|
|
217
|
+
: undefined;
|
|
218
|
+
if (sessionIdentityOperation) {
|
|
219
|
+
this.assertSessionIdentityOperationIdle(sessionIdentityOperation);
|
|
214
220
|
this.sessionIdentityOperationInFlight = true;
|
|
215
221
|
this.notifyState();
|
|
216
222
|
}
|
|
217
223
|
try {
|
|
218
|
-
if (
|
|
224
|
+
if (sessionIdentityOperation)
|
|
219
225
|
await this.options.onBeforeSessionIdentityOperation?.(event);
|
|
220
226
|
const result = await this.runAction(event);
|
|
221
227
|
if (isSessionOperationResult(result))
|
|
@@ -231,7 +237,7 @@ export class RuntimeRoutedSession {
|
|
|
231
237
|
return output;
|
|
232
238
|
}
|
|
233
239
|
finally {
|
|
234
|
-
if (
|
|
240
|
+
if (sessionIdentityOperation) {
|
|
235
241
|
this.sessionIdentityOperationInFlight = false;
|
|
236
242
|
this.notifyState();
|
|
237
243
|
this.startDrain();
|
|
@@ -1107,9 +1107,15 @@ export class RoutedSession {
|
|
|
1107
1107
|
if (event.action === "compact") {
|
|
1108
1108
|
return this.enqueueCompactAction(event);
|
|
1109
1109
|
}
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1110
|
+
const sessionIdentityOperation = event.action === "session.fork"
|
|
1111
|
+
? "fork"
|
|
1112
|
+
: event.action === "session.clone"
|
|
1113
|
+
? "clone"
|
|
1114
|
+
: event.action === "session.switch"
|
|
1115
|
+
? "switch"
|
|
1116
|
+
: undefined;
|
|
1117
|
+
if (sessionIdentityOperation) {
|
|
1118
|
+
this.assertSessionIdentityOperationIdle(sessionIdentityOperation);
|
|
1113
1119
|
this.sessionIdentityOperationInFlight = true;
|
|
1114
1120
|
}
|
|
1115
1121
|
try {
|
|
@@ -1127,7 +1133,7 @@ export class RoutedSession {
|
|
|
1127
1133
|
return output;
|
|
1128
1134
|
}
|
|
1129
1135
|
finally {
|
|
1130
|
-
if (
|
|
1136
|
+
if (sessionIdentityOperation)
|
|
1131
1137
|
this.sessionIdentityOperationInFlight = false;
|
|
1132
1138
|
}
|
|
1133
1139
|
}
|
|
@@ -6,6 +6,17 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
6
6
|
import { DEFAULT_AGENT_RUNTIME_INSTANCE_ID, DEFAULT_BUILTIN_TOOL_NAMES } from "../../core/profiles.js";
|
|
7
7
|
import { isPiboThinkingLevel } from "../../core/thinking.js";
|
|
8
8
|
import { findPiPackage } from "../../pi-packages/store.js";
|
|
9
|
+
export class CustomAgentTargetReferenceError extends Error {
|
|
10
|
+
targetProfileName;
|
|
11
|
+
dependentProfileNames;
|
|
12
|
+
constructor(targetProfileName, dependentProfileNames) {
|
|
13
|
+
const sortedNames = [...dependentProfileNames].sort((left, right) => left.localeCompare(right));
|
|
14
|
+
super(`Custom agent "${targetProfileName}" is targeted by custom agents: ${sortedNames.join(", ")}`);
|
|
15
|
+
this.name = "CustomAgentTargetReferenceError";
|
|
16
|
+
this.targetProfileName = targetProfileName;
|
|
17
|
+
this.dependentProfileNames = sortedNames;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
9
20
|
const CUSTOM_AGENT_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
21
|
export function previewCustomAgentCreate(input, options = {}) {
|
|
11
22
|
const now = options.now ?? new Date().toISOString();
|
|
@@ -264,10 +275,42 @@ export class CustomAgentStore {
|
|
|
264
275
|
.run(archivedAt, new Date().toISOString(), id);
|
|
265
276
|
return this.get(id);
|
|
266
277
|
}
|
|
278
|
+
listReferencingAgents(id) {
|
|
279
|
+
this.migrateLegacyProfileNames();
|
|
280
|
+
const target = this.get(id);
|
|
281
|
+
if (!target)
|
|
282
|
+
return [];
|
|
283
|
+
const targetNames = new Set([
|
|
284
|
+
target.profileName,
|
|
285
|
+
target.id,
|
|
286
|
+
`custom-agent:${target.id}`,
|
|
287
|
+
...target.profileAliases,
|
|
288
|
+
]);
|
|
289
|
+
return this.list({ includeArchived: true })
|
|
290
|
+
.filter((agent) => agent.id !== id && agent.subagents.some((subagent) => targetNames.has(subagent.targetProfile)))
|
|
291
|
+
.sort((left, right) => left.profileName.localeCompare(right.profileName));
|
|
292
|
+
}
|
|
267
293
|
delete(id) {
|
|
268
|
-
this.db.
|
|
269
|
-
|
|
270
|
-
|
|
294
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
295
|
+
try {
|
|
296
|
+
const target = this.get(id);
|
|
297
|
+
if (!target) {
|
|
298
|
+
this.db.exec("COMMIT");
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const dependents = this.listReferencingAgents(id);
|
|
302
|
+
if (dependents.length > 0) {
|
|
303
|
+
throw new CustomAgentTargetReferenceError(target.profileName, dependents.map((agent) => agent.profileName));
|
|
304
|
+
}
|
|
305
|
+
this.db.prepare("DELETE FROM chat_agent_profile_aliases WHERE agent_id = ?").run(id);
|
|
306
|
+
const result = this.db.prepare("DELETE FROM chat_agents WHERE id = ?").run(id);
|
|
307
|
+
this.db.exec("COMMIT");
|
|
308
|
+
return Number(result.changes ?? 0) > 0;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
this.db.exec("ROLLBACK");
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
271
314
|
}
|
|
272
315
|
close() {
|
|
273
316
|
this.db.close();
|
|
@@ -369,6 +412,15 @@ export class CustomAgentStore {
|
|
|
369
412
|
.prepare("UPDATE chat_agents SET profile_name = ?, display_name = ? WHERE id = ?")
|
|
370
413
|
.run(nextName, nextName, row.id);
|
|
371
414
|
}
|
|
415
|
+
this.db.prepare(`
|
|
416
|
+
DELETE FROM chat_agent_profile_aliases
|
|
417
|
+
WHERE EXISTS (
|
|
418
|
+
SELECT 1
|
|
419
|
+
FROM chat_agents
|
|
420
|
+
WHERE chat_agents.profile_name = chat_agent_profile_aliases.old_profile_name
|
|
421
|
+
AND chat_agents.id != chat_agent_profile_aliases.agent_id
|
|
422
|
+
)
|
|
423
|
+
`).run();
|
|
372
424
|
}
|
|
373
425
|
migrateAgentFolderColumn() {
|
|
374
426
|
if (!this.tableColumns().has("folder_id")) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdirSync, rmSync, statSync } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { mkdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import { piboHomePath } from "../../../core/pibo-home.js";
|
|
6
6
|
export class ChatProjectService {
|
|
@@ -72,7 +72,7 @@ export class ChatProjectService {
|
|
|
72
72
|
const projectFolder = resolve(normalizeProjectFolder(input.projectFolder));
|
|
73
73
|
this.assertNameAvailable(name);
|
|
74
74
|
this.assertFolderAvailable(projectFolder);
|
|
75
|
-
this.ensureProjectFolderUsable(projectFolder);
|
|
75
|
+
this.ensureProjectFolderUsable(projectFolder, input.createFolder === true);
|
|
76
76
|
const now = new Date().toISOString();
|
|
77
77
|
const id = `prj_${randomUUID()}`;
|
|
78
78
|
this.db.prepare(`INSERT INTO projects (id, name, description, project_folder, configuration_status, metadata_json, created_at, updated_at)
|
|
@@ -93,16 +93,26 @@ export class ChatProjectService {
|
|
|
93
93
|
return this.getProject(id, { includeArchived: true });
|
|
94
94
|
}
|
|
95
95
|
deleteProject(id, options) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
97
|
+
try {
|
|
98
|
+
const project = this.requireProject(id, { includeArchived: true });
|
|
99
|
+
if (!project.archivedAt)
|
|
100
|
+
throw new Error("Archive the project before permanently deleting it.");
|
|
101
|
+
if (options.confirmName !== project.name)
|
|
102
|
+
throw new Error(`Type \"${project.name}\" to permanently delete this project.`);
|
|
103
|
+
if (options.deleteFiles)
|
|
104
|
+
this.assertFolderDoesNotOverlapAnotherProject(project);
|
|
105
|
+
this.db.prepare("DELETE FROM project_sessions WHERE project_id = ?").run(id);
|
|
106
|
+
this.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
107
|
+
if (options.deleteFiles)
|
|
108
|
+
rmSync(project.projectFolder, { recursive: true, force: true });
|
|
109
|
+
this.db.exec("COMMIT");
|
|
110
|
+
return { deletedProjectId: id };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.db.exec("ROLLBACK");
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
117
|
listProjectSessions(projectId, options = {}) {
|
|
108
118
|
const rows = this.db.prepare(`SELECT * FROM project_sessions WHERE project_id = ? ${options.includeArchived ? "" : "AND archived = 0"} ORDER BY created_at`).all(projectId);
|
|
@@ -112,6 +122,60 @@ export class ChatProjectService {
|
|
|
112
122
|
const row = this.db.prepare("SELECT * FROM project_sessions WHERE pibo_session_id = ?").get(piboSessionId);
|
|
113
123
|
return row ? projectSessionFromRow(row) : undefined;
|
|
114
124
|
}
|
|
125
|
+
deleteProjectSessions(piboSessionIds) {
|
|
126
|
+
const ids = [...new Set(piboSessionIds.filter(Boolean))];
|
|
127
|
+
if (ids.length === 0)
|
|
128
|
+
return 0;
|
|
129
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
130
|
+
try {
|
|
131
|
+
const deleted = this.deleteProjectSessionsLocked(ids, new Date().toISOString());
|
|
132
|
+
this.db.exec("COMMIT");
|
|
133
|
+
return deleted;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.db.exec("ROLLBACK");
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async deleteProjectSessionWithCanonicalDelete(piboSessionId, deleteCanonicalSession) {
|
|
141
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
142
|
+
try {
|
|
143
|
+
this.deleteProjectSessionsLocked([piboSessionId], new Date().toISOString());
|
|
144
|
+
const result = await deleteCanonicalSession();
|
|
145
|
+
this.db.exec("COMMIT");
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.db.exec("ROLLBACK");
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
deleteProjectSessionsLocked(ids, now) {
|
|
154
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
155
|
+
const affectedProjects = this.db.prepare(`SELECT DISTINCT id FROM projects
|
|
156
|
+
WHERE current_main_session_id IN (${placeholders})
|
|
157
|
+
UNION
|
|
158
|
+
SELECT DISTINCT project_id AS id FROM project_sessions WHERE pibo_session_id IN (${placeholders})`)
|
|
159
|
+
.all(...ids, ...ids);
|
|
160
|
+
const deleted = this.db.prepare(`DELETE FROM project_sessions WHERE pibo_session_id IN (${placeholders})`).run(...ids);
|
|
161
|
+
for (const project of affectedProjects) {
|
|
162
|
+
const current = this.db.prepare("SELECT current_main_session_id FROM projects WHERE id = ?")
|
|
163
|
+
.get(project.id);
|
|
164
|
+
if (!current?.current_main_session_id)
|
|
165
|
+
continue;
|
|
166
|
+
const currentLink = this.db.prepare("SELECT 1 FROM project_sessions WHERE project_id = ? AND pibo_session_id = ?")
|
|
167
|
+
.get(project.id, current.current_main_session_id);
|
|
168
|
+
if (currentLink)
|
|
169
|
+
continue;
|
|
170
|
+
const replacement = this.db.prepare(`SELECT pibo_session_id FROM project_sessions
|
|
171
|
+
WHERE project_id = ? AND kind = 'main' AND archived = 0
|
|
172
|
+
ORDER BY updated_at DESC, created_at DESC, pibo_session_id DESC
|
|
173
|
+
LIMIT 1`).get(project.id);
|
|
174
|
+
this.db.prepare("UPDATE projects SET current_main_session_id = ?, updated_at = ? WHERE id = ?")
|
|
175
|
+
.run(replacement?.pibo_session_id ?? null, now, project.id);
|
|
176
|
+
}
|
|
177
|
+
return Number(deleted.changes ?? 0);
|
|
178
|
+
}
|
|
115
179
|
addProjectSession(input) {
|
|
116
180
|
const existing = this.getProjectSession(input.piboSessionId);
|
|
117
181
|
const now = new Date().toISOString();
|
|
@@ -599,9 +663,17 @@ export class ChatProjectService {
|
|
|
599
663
|
if (existing)
|
|
600
664
|
throw new Error("Project folder already exists");
|
|
601
665
|
}
|
|
602
|
-
|
|
666
|
+
assertFolderDoesNotOverlapAnotherProject(project) {
|
|
667
|
+
const projects = this.db.prepare("SELECT name, project_folder FROM projects WHERE id != ? ORDER BY lower(name), created_at").all(project.id);
|
|
668
|
+
const overlapping = projects.find((other) => pathsOverlap(project.projectFolder, other.project_folder));
|
|
669
|
+
if (overlapping) {
|
|
670
|
+
throw new Error(`Cannot delete project files because its folder overlaps Project \"${overlapping.name}\". Delete without deleting files or choose a non-overlapping folder.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
ensureProjectFolderUsable(folder, createFolder) {
|
|
603
674
|
try {
|
|
604
|
-
|
|
675
|
+
if (createFolder)
|
|
676
|
+
mkdirSync(folder, { recursive: true });
|
|
605
677
|
if (!statSync(folder).isDirectory())
|
|
606
678
|
throw new Error("Project folder is not a directory");
|
|
607
679
|
}
|
|
@@ -770,6 +842,38 @@ function normalizeProjectFolder(value) {
|
|
|
770
842
|
throw new Error("Project folder must be an absolute path, e.g. ~/code/my-project or /home/me/code/my-project");
|
|
771
843
|
return folder;
|
|
772
844
|
}
|
|
845
|
+
function pathsOverlap(left, right) {
|
|
846
|
+
if (pathContains(left, right) || pathContains(right, left))
|
|
847
|
+
return true;
|
|
848
|
+
const canonicalLeft = canonicalProjectPath(left);
|
|
849
|
+
const canonicalRight = canonicalProjectPath(right);
|
|
850
|
+
if (!canonicalLeft || !canonicalRight)
|
|
851
|
+
return true;
|
|
852
|
+
return pathContains(canonicalLeft, canonicalRight) || pathContains(canonicalRight, canonicalLeft);
|
|
853
|
+
}
|
|
854
|
+
function canonicalProjectPath(path) {
|
|
855
|
+
let current = resolve(path);
|
|
856
|
+
const missingSegments = [];
|
|
857
|
+
while (true) {
|
|
858
|
+
try {
|
|
859
|
+
return resolve(realpathSync(current), ...missingSegments);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
863
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
864
|
+
return undefined;
|
|
865
|
+
const parent = dirname(current);
|
|
866
|
+
if (parent === current)
|
|
867
|
+
return undefined;
|
|
868
|
+
missingSegments.unshift(basename(current));
|
|
869
|
+
current = parent;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function pathContains(parent, candidate) {
|
|
874
|
+
const pathFromParent = relative(resolve(parent), resolve(candidate));
|
|
875
|
+
return pathFromParent === "" || (pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent));
|
|
876
|
+
}
|
|
773
877
|
const PROJECT_SESSION_STATES = new Set([
|
|
774
878
|
"simple_chat",
|
|
775
879
|
"workflow",
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { roomWorkspaceFromMetadata } from "../types/rooms.js";
|
|
3
3
|
import { roomFromRow } from "./chat-data-mappers.js";
|
|
4
|
+
export class PiboRoomHierarchyCycleError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Room parent assignment would create a cycle.");
|
|
7
|
+
this.name = "PiboRoomHierarchyCycleError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
4
10
|
export class ChatRoomService {
|
|
5
11
|
store;
|
|
6
12
|
constructor(store) {
|
|
@@ -17,6 +23,9 @@ export class ChatRoomService {
|
|
|
17
23
|
const existing = this.getRoom(id);
|
|
18
24
|
if (!existing)
|
|
19
25
|
return undefined;
|
|
26
|
+
if (input.parentRoomId && this.parentAssignmentCreatesCycle(id, input.parentRoomId)) {
|
|
27
|
+
throw new PiboRoomHierarchyCycleError();
|
|
28
|
+
}
|
|
20
29
|
const metadata = input.metadata ?? existing.metadata;
|
|
21
30
|
const updated = { ...existing, name: input.name ?? existing.name, topic: input.topic === null ? undefined : input.topic ?? existing.topic, parentRoomId: input.parentRoomId === null ? undefined : input.parentRoomId ?? existing.parentRoomId, retentionPolicyId: input.retentionPolicyId === null ? undefined : input.retentionPolicyId ?? existing.retentionPolicyId, metadata, workspace: roomWorkspaceFromMetadata(metadata), updatedAt: new Date().toISOString() };
|
|
22
31
|
this.store.db.prepare("UPDATE rooms SET name = ?, topic = ?, parent_room_id = ?, workspace = ?, retention_policy_id = ?, metadata_json = ?, updated_at = ? WHERE id = ?").run(updated.name, updated.topic ?? null, updated.parentRoomId ?? null, updated.workspace ?? null, updated.retentionPolicyId ?? null, JSON.stringify(updated.metadata), updated.updatedAt, id);
|
|
@@ -49,4 +58,16 @@ export class ChatRoomService {
|
|
|
49
58
|
requireRoom(roomId) { const room = this.getRoom(roomId); if (!room)
|
|
50
59
|
throw new Error("Room not found"); return room; }
|
|
51
60
|
close() { }
|
|
61
|
+
parentAssignmentCreatesCycle(roomId, parentRoomId) {
|
|
62
|
+
const roomsById = new Map(this.listRooms().map((room) => [room.id, room]));
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
let currentId = parentRoomId;
|
|
65
|
+
while (currentId && !visited.has(currentId)) {
|
|
66
|
+
if (currentId === roomId)
|
|
67
|
+
return true;
|
|
68
|
+
visited.add(currentId);
|
|
69
|
+
currentId = roomsById.get(currentId)?.parentRoomId;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
52
73
|
}
|