@pasko70/pibo 1.10.0 → 1.11.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/dist/apps/chat/chat-request-normalizers.js +7 -0
- package/dist/apps/chat/data/chat-data-mappers.js +2 -0
- package/dist/apps/chat/loop-api.js +11 -6
- package/dist/apps/chat/web-app.js +30 -10
- package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-0E9FVJ6k.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BvqC0hRM.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-C2BRHNXr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-C57jNmjf.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-CBL8UXjd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-CI-LPHjw.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-LHRs1Nhr.js → dist-CMRFkfl5.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-CpZPhD2y.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-CuCZ-3_K.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-CzC5kPWJ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-D9hLnn1T.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BEvjPUor.js +173 -0
- package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-DNeE4HrG.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-BLZRi4Ka.js +41 -0
- package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
- 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-1.11.0.vsix +0 -0
- package/dist/auth/better-auth.js +49 -1
- package/dist/auth/cli.js +220 -0
- package/dist/auth/machine-keys.js +258 -0
- package/dist/auth/machine-session.js +123 -0
- package/dist/bin/pibo.js +0 -0
- package/dist/bin/rg.js +0 -0
- package/dist/cli-session/localSessionSource.js +34 -12
- package/dist/cli.js +6 -0
- package/dist/compute/cli.js +7 -0
- package/dist/compute/resource-health.js +61 -4
- package/dist/config/config.js +5 -0
- package/dist/core/events.js +6 -1
- package/dist/core/routed-session.js +67 -4
- package/dist/core/session-router.js +40 -6
- package/dist/data/ingest-service.js +3 -1
- package/dist/data/schema.js +4 -0
- package/dist/debug/index.js +4 -0
- package/dist/debug/trace-status.js +25 -0
- package/dist/debug/trace.js +51 -17
- package/dist/index.js +1 -0
- package/dist/loops/accounting.js +19 -0
- package/dist/loops/cli.js +22 -6
- package/dist/loops/prompts.js +4 -1
- package/dist/loops/service.js +97 -8
- package/dist/loops/store.js +128 -17
- package/dist/loops/tools.js +24 -7
- package/dist/reliability/store.js +34 -7
- package/dist/resources/cli.js +15 -0
- package/dist/resources/lifecycle.js +28 -4
- package/dist/resources/reaper.js +1 -0
- package/dist/runs/lifecycle.js +59 -0
- package/dist/runs/registry.js +47 -1
- package/dist/runs/tools.js +31 -13
- package/dist/session-ui/terminalRows.js +66 -2
- package/dist/shared/trace-async-agent-runs.js +4 -4
- package/dist/shared/trace-event-projection.js +59 -19
- package/dist/shared/trace-nodes.js +5 -0
- package/dist/shared/trace-page-merge.js +15 -0
- package/dist/shared/trace-run-notifications.js +3 -1
- package/dist/signals/projector.js +16 -3
- package/dist/tools/browser-pool.js +50 -0
- package/dist/tools/browser-use-leases.js +12 -8
- package/dist/tools/guides.js +8 -1
- package/dist/tools/index.js +1 -0
- package/package.json +2 -1
- package/skills/builtin/loop/SKILL.md +12 -3
- package/dist/apps/chat-ui/assets/index-8W_yMHQI.js +0 -173
- package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
- package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function resolveDebugTraceSessionStatus(sessionStatus, eventTypes) {
|
|
2
|
+
for (let index = eventTypes.length - 1; index >= 0; index -= 1) {
|
|
3
|
+
switch (eventTypes[index]) {
|
|
4
|
+
case "session_error":
|
|
5
|
+
return { status: "error", source: "event-log" };
|
|
6
|
+
case "message_started":
|
|
7
|
+
return { status: "running", source: "event-log" };
|
|
8
|
+
case "message_finished":
|
|
9
|
+
return { status: "idle", source: "event-log" };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
status: sessionStatus === "running" || sessionStatus === "error" ? sessionStatus : "idle",
|
|
14
|
+
source: "session-store",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function summarizeDebugTraceStatus(sessionStatus, nodeStatuses) {
|
|
18
|
+
const errorNodeCount = nodeStatuses.filter((status) => status === "error").length;
|
|
19
|
+
if (sessionStatus === "error")
|
|
20
|
+
return { status: "error", errorNodeCount };
|
|
21
|
+
if (sessionStatus === "running" || nodeStatuses.some((status) => status === "running")) {
|
|
22
|
+
return { status: "running", errorNodeCount };
|
|
23
|
+
}
|
|
24
|
+
return { status: "done", errorNodeCount };
|
|
25
|
+
}
|
package/dist/debug/trace.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { buildTraceView } from "../apps/chat/trace.js";
|
|
2
2
|
import { normalizeSessionErrorDetails } from "../core/session-errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { compareTraceNodes } from "../shared/trace-nodes.js";
|
|
4
4
|
import { openReadOnlyDebugDatabase, withStorePath } from "./sql.js";
|
|
5
5
|
import { formatNextCommands } from "./next-commands.js";
|
|
6
|
+
import { resolveDebugTraceSessionStatus, summarizeDebugTraceStatus } from "./trace-status.js";
|
|
6
7
|
export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
7
8
|
if (!stores.sessions.exists)
|
|
8
9
|
throw new Error(`Debug store "sessions" not found at ${stores.sessions.path}`);
|
|
@@ -16,27 +17,32 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
|
16
17
|
throw new Error(`Pibo session "${piboSessionId}" not found`);
|
|
17
18
|
const session = sessionFromRow(sessionRow);
|
|
18
19
|
const sessions = sessionsDb.prepare("SELECT * FROM sessions").all().map(sessionFromRow);
|
|
20
|
+
const adapterIssues = [];
|
|
19
21
|
const events = tableExists(chatDb, "event_log")
|
|
20
22
|
? chatDb
|
|
21
23
|
.prepare("SELECT stream_id, session_id, session_sequence, event_id, type, created_at, preview_text, attributes_json FROM event_log WHERE session_id = ? ORDER BY stream_id ASC")
|
|
22
|
-
.all(piboSessionId).map(eventFromRow).filter((event) => event !== undefined)
|
|
24
|
+
.all(piboSessionId).map((row) => eventFromRow(row, adapterIssues)).filter((event) => event !== undefined)
|
|
23
25
|
: [];
|
|
26
|
+
const sessionStatus = resolveDebugTraceSessionStatus(sessionRow.status, events.map((event) => event.type));
|
|
24
27
|
const view = await buildTraceView({
|
|
25
28
|
session,
|
|
26
29
|
sessions,
|
|
27
30
|
events,
|
|
28
|
-
status:
|
|
31
|
+
status: sessionStatus.status,
|
|
29
32
|
});
|
|
30
33
|
const rows = flattenTraceNodes(view.nodes);
|
|
34
|
+
const statusSummary = summarizeDebugTraceStatus(sessionStatus.status, rows.map((node) => node.status));
|
|
31
35
|
const filtered = options.runningOnly ? rows.filter((node) => node.status === "running") : rows;
|
|
32
36
|
return {
|
|
33
37
|
piboSessionId: view.piboSessionId,
|
|
34
38
|
piSessionId: view.piSessionId,
|
|
35
39
|
title: view.title,
|
|
36
|
-
status:
|
|
40
|
+
status: statusSummary.status,
|
|
41
|
+
statusSource: sessionStatus.source,
|
|
42
|
+
errorNodeCount: statusSummary.errorNodeCount,
|
|
37
43
|
nodes: filtered,
|
|
38
44
|
rawNodeCount: rows.length,
|
|
39
|
-
...(options.check ? { checks: checkTraceView(view) } : {}),
|
|
45
|
+
...(options.check ? { checks: checkTraceView(view, adapterIssues) } : {}),
|
|
40
46
|
nextCommands: buildTraceNextCommands(view.piboSessionId, filtered),
|
|
41
47
|
};
|
|
42
48
|
}
|
|
@@ -68,6 +74,8 @@ export function formatDebugTrace(result, options = {}) {
|
|
|
68
74
|
`piSessionId: ${result.piSessionId}`,
|
|
69
75
|
`title: ${result.title}`,
|
|
70
76
|
`status: ${result.status}`,
|
|
77
|
+
`statusSource: ${result.statusSource}`,
|
|
78
|
+
`nodeErrors: ${result.errorNodeCount}`,
|
|
71
79
|
"",
|
|
72
80
|
];
|
|
73
81
|
if (result.nodes.length === 0) {
|
|
@@ -160,18 +168,11 @@ function flattenTraceNodes(nodes, depth = 0) {
|
|
|
160
168
|
...flattenTraceNodes(node.children, depth + 1),
|
|
161
169
|
]);
|
|
162
170
|
}
|
|
163
|
-
function
|
|
164
|
-
const
|
|
165
|
-
if (rows.some((node) => node.status === "error"))
|
|
166
|
-
return "error";
|
|
167
|
-
if (rows.some((node) => node.status === "running"))
|
|
168
|
-
return "running";
|
|
169
|
-
return "done";
|
|
170
|
-
}
|
|
171
|
-
function checkTraceView(view) {
|
|
172
|
-
const issues = [];
|
|
171
|
+
export function checkTraceView(view, adapterIssues = []) {
|
|
172
|
+
const issues = [...adapterIssues];
|
|
173
173
|
const all = flattenPiboTraceNodes(view.nodes);
|
|
174
174
|
const ids = new Set();
|
|
175
|
+
const stableKeyOwners = new Map();
|
|
175
176
|
for (const node of all) {
|
|
176
177
|
if (ids.has(node.id)) {
|
|
177
178
|
issues.push({
|
|
@@ -206,6 +207,20 @@ function checkTraceView(view) {
|
|
|
206
207
|
message: "Trace node has no conceptual stable key.",
|
|
207
208
|
});
|
|
208
209
|
}
|
|
210
|
+
else {
|
|
211
|
+
const existingOwner = stableKeyOwners.get(node.stableKey);
|
|
212
|
+
if (existingOwner && existingOwner !== node.id) {
|
|
213
|
+
issues.push({
|
|
214
|
+
severity: "warning",
|
|
215
|
+
code: "duplicate_stable_key",
|
|
216
|
+
nodeId: node.id,
|
|
217
|
+
message: `Stable key "${node.stableKey}" is already used by node "${existingOwner}".`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
stableKeyOwners.set(node.stableKey, node.id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
for (const node of all) {
|
|
211
226
|
if (node.parentId && !ids.has(node.parentId)) {
|
|
@@ -235,7 +250,7 @@ function checkSiblingOrder(nodes, issues) {
|
|
|
235
250
|
checkSiblingOrder(node.children, issues);
|
|
236
251
|
}
|
|
237
252
|
function compareOrder(left, right) {
|
|
238
|
-
return
|
|
253
|
+
return compareTraceNodes(left, right);
|
|
239
254
|
}
|
|
240
255
|
function flattenPiboTraceNodes(nodes) {
|
|
241
256
|
return nodes.flatMap((node) => [node, ...flattenPiboTraceNodes(node.children)]);
|
|
@@ -288,10 +303,18 @@ function sessionFromRow(row) {
|
|
|
288
303
|
updatedAt: row.updated_at,
|
|
289
304
|
};
|
|
290
305
|
}
|
|
291
|
-
function eventFromRow(row) {
|
|
306
|
+
function eventFromRow(row, issues) {
|
|
292
307
|
const payload = outputPayloadFromV2Row(row);
|
|
293
308
|
if (!payload)
|
|
294
309
|
return undefined;
|
|
310
|
+
if ((row.type === "assistant_delta" || row.type === "thinking_delta") && !nonEmptyEventText(payload)) {
|
|
311
|
+
issues.push({
|
|
312
|
+
severity: "warning",
|
|
313
|
+
code: "missing_delta_text",
|
|
314
|
+
nodeId: row.event_id ?? String(row.stream_id),
|
|
315
|
+
message: `Persisted ${row.type} event has no readable text payload.`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
295
318
|
return {
|
|
296
319
|
id: String(row.stream_id),
|
|
297
320
|
piboSessionId: row.session_id ?? undefined,
|
|
@@ -315,12 +338,16 @@ function outputPayloadFromV2Row(row) {
|
|
|
315
338
|
const base = { piboSessionId, eventId: row.event_id ?? undefined };
|
|
316
339
|
if (row.type === "assistant_message")
|
|
317
340
|
return compactObject({ ...base, type: "assistant_message", text: row.preview_text ?? "" });
|
|
341
|
+
if (row.type === "assistant_delta")
|
|
342
|
+
return compactObject({ ...base, type: "assistant_delta", text: inlineTextPayload(inlinePayload) ?? row.preview_text ?? "" });
|
|
318
343
|
if (row.type === "message_started")
|
|
319
344
|
return compactObject({ ...base, type: "message_started", text: row.preview_text ?? "" });
|
|
320
345
|
if (row.type === "message_finished")
|
|
321
346
|
return compactObject({ ...base, type: "message_finished" });
|
|
322
347
|
if (row.type === "thinking_started")
|
|
323
348
|
return compactObject({ ...base, type: "thinking_started" });
|
|
349
|
+
if (row.type === "thinking_delta")
|
|
350
|
+
return compactObject({ ...base, type: "thinking_delta", text: inlineTextPayload(inlinePayload) ?? row.preview_text ?? "" });
|
|
324
351
|
if (row.type === "thinking_finished")
|
|
325
352
|
return compactObject({ ...base, type: "thinking_finished", text: row.preview_text ?? "" });
|
|
326
353
|
if (row.type === "tool_call")
|
|
@@ -337,6 +364,13 @@ function outputPayloadFromV2Row(row) {
|
|
|
337
364
|
}
|
|
338
365
|
return compactObject({ ...base, type: row.type });
|
|
339
366
|
}
|
|
367
|
+
function inlineTextPayload(value) {
|
|
368
|
+
return typeof value === "string" ? value : undefined;
|
|
369
|
+
}
|
|
370
|
+
function nonEmptyEventText(event) {
|
|
371
|
+
const text = event.text;
|
|
372
|
+
return typeof text === "string" && text.length > 0;
|
|
373
|
+
}
|
|
340
374
|
function stringAttribute(attributes, key) {
|
|
341
375
|
const value = attributes[key];
|
|
342
376
|
return typeof value === "string" ? value : undefined;
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { LOCAL_TUI_CHANNEL_NAME, LocalRoutedTuiClient, createLocalRoutedTuiClien
|
|
|
19
19
|
export { createWebHostChannel, DEFAULT_WEB_CHANNEL_HOST, DEFAULT_WEB_CHANNEL_PORT, WEB_CHANNEL_NAME } from "./web/channel.js";
|
|
20
20
|
export { sendGatewayEvent, sendGatewayMessageAndWaitForReply } from "./gateway/request.js";
|
|
21
21
|
export { createSubagentToolDefinitions, createSubagentToolName, } from "./subagents/tool.js";
|
|
22
|
+
export { PiboSteeringUnavailableError } from "./core/events.js";
|
|
22
23
|
export { InMemoryPiboSessionStore, createPiSessionId, createPiboSessionId, createPiboSession, } from "./sessions/store.js";
|
|
23
24
|
export { runPiboCli } from "./cli.js";
|
|
24
25
|
export { DEFAULT_PIBO_CONFIG_PATH, PIBO_CONFIG_KEYS, deletePiboConfigValue, getDefaultPiboConfigPath, getDisplayPiboConfigValue, getPiboConfigValue, isPiboConfigKeySecret, loadPiboConfig, redactPiboConfig, savePiboConfig, setPiboConfigValue, } from "./config/config.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function goalActiveTimeSeconds(job) {
|
|
2
|
+
return Math.max(0, Math.floor(job.state.activeTimeSeconds ?? job.state.timeUsedSeconds ?? 0));
|
|
3
|
+
}
|
|
4
|
+
export function goalElapsedWallClockSeconds(job, now = new Date()) {
|
|
5
|
+
if (job.mode !== 'goal' || !job.state.goalStartedAt)
|
|
6
|
+
return 0;
|
|
7
|
+
const startedAt = Date.parse(job.state.goalStartedAt);
|
|
8
|
+
const endedAt = job.state.goalEndedAt ? Date.parse(job.state.goalEndedAt) : now.getTime();
|
|
9
|
+
if (!Number.isFinite(startedAt) || !Number.isFinite(endedAt))
|
|
10
|
+
return 0;
|
|
11
|
+
return Math.max(0, Math.floor((endedAt - startedAt) / 1000));
|
|
12
|
+
}
|
|
13
|
+
export function goalRemainingTokens(job) {
|
|
14
|
+
return job.tokenBudget === undefined ? undefined : Math.max(0, job.tokenBudget - (job.state.tokensUsed ?? 0));
|
|
15
|
+
}
|
|
16
|
+
export function goalCanStartNextTurn(job) {
|
|
17
|
+
const remaining = goalRemainingTokens(job);
|
|
18
|
+
return remaining === undefined || remaining > (job.tokenReserve ?? 0);
|
|
19
|
+
}
|
package/dist/loops/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { goalActiveTimeSeconds, goalElapsedWallClockSeconds } from './accounting.js';
|
|
3
4
|
import { createDefaultPiboLoopStore } from './store.js';
|
|
4
5
|
import { createBuiltInLoopStopConditions } from './stopping.js';
|
|
5
6
|
import { DEFAULT_PIBO_PROFILE_NAME } from '../plugins/builtin.js';
|
|
@@ -38,6 +39,9 @@ function maxIterations(value) { if (value === undefined)
|
|
|
38
39
|
function tokenBudget(value) { if (value === undefined)
|
|
39
40
|
return undefined; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1)
|
|
40
41
|
throw new Error('--token-budget must be a positive integer'); return parsed; }
|
|
42
|
+
function tokenReserve(value) { if (value === undefined)
|
|
43
|
+
return undefined; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 0)
|
|
44
|
+
throw new Error('--token-reserve must be a non-negative integer'); return parsed; }
|
|
41
45
|
function loopMode(value) { if (value === undefined)
|
|
42
46
|
return undefined; if (value !== 'goal' && value !== 'ralph')
|
|
43
47
|
throw new Error('--mode must be goal or ralph'); return value; }
|
|
@@ -110,8 +114,16 @@ export function formatLoopResourceSummary(resources) {
|
|
|
110
114
|
parts.push('next=pibo compute reap --dry-run --include-dev');
|
|
111
115
|
return parts.length ? parts.join(';') : '-';
|
|
112
116
|
}
|
|
113
|
-
function formatLoopJobLine(job) {
|
|
114
|
-
|
|
117
|
+
function formatLoopJobLine(job) {
|
|
118
|
+
const goal = job.mode === 'goal' ? job.state.goalStatus ?? (job.enabled ? 'active' : 'paused') : '-';
|
|
119
|
+
const budget = job.mode === 'goal' ? job.tokenBudget === undefined ? 'unbounded' : `soft:${job.state.tokensUsed ?? 0}/${job.tokenBudget};reserve=${job.tokenReserve ?? 0}` : '-';
|
|
120
|
+
const time = job.mode === 'goal' ? `activeAgent=${goalActiveTimeSeconds(job)}s;elapsedWall=${goalElapsedWallClockSeconds(job)}s;paused=included` : '-';
|
|
121
|
+
return `${job.id}\t${job.mode}\t${job.enabled ? 'running' : 'stopped'}\t${job.state.runningAt ? 'active' : '-'}\tgoal=${goal}\tbudget=${budget}\ttime=${time}\tresources=${formatLoopResourceSummary(job.resources)}\t${job.name}`;
|
|
122
|
+
}
|
|
123
|
+
function formatLoopRunLine(run) {
|
|
124
|
+
const accounting = run.accounting ? `tokens=${run.accounting.tokensUsed ?? 0};remainingBefore=${run.accounting.remainingTokensBefore ?? 'unbounded'};overshoot=${run.accounting.overshootTokens ?? 0};activeAgent=${run.accounting.activeTimeSeconds ?? 0}s` : '-';
|
|
125
|
+
return `${run.id}\t${run.jobId}\t${run.status}\t${run.piboSessionId ?? '-'}\t${run.completedAt ?? '-'}\taccounting=${accounting}\tresources=${formatLoopResourceSummary(run.resources)}`;
|
|
126
|
+
}
|
|
115
127
|
export async function runLoopCli(argv = process.argv, defaults = {}) {
|
|
116
128
|
const program = new Command();
|
|
117
129
|
program.name(defaults.commandName ?? 'pibo loop').description('Manage continuous Pibo loops').helpOption('-h, --help');
|
|
@@ -127,13 +139,13 @@ export async function runLoopCli(argv = process.argv, defaults = {}) {
|
|
|
127
139
|
else
|
|
128
140
|
for (const job of jobs)
|
|
129
141
|
console.log(formatLoopJobLine(job)); store.close(); });
|
|
130
|
-
program.command('add').description(defaults.mode === 'ralph' ? 'Create a Ralph job' : 'Create a Loop job').option('--template <id>', 'Built-in job template id').option('--mode <mode>', 'Loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile', DEFAULT_PIBO_PROFILE_NAME).option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', '
|
|
142
|
+
program.command('add').description(defaults.mode === 'ralph' ? 'Create a Ralph job' : 'Create a Loop job').option('--template <id>', 'Built-in job template id').option('--mode <mode>', 'Loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile', DEFAULT_PIBO_PROFILE_NAME).option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', 'Set a soft Goal token budget; the final turn can overshoot').option('--token-reserve <n>', 'Require more than n tokens to remain before starting another Goal turn').option('--model <provider/model>', 'Runtime model override, for example openai/gpt-5').option('--thinking <level>', 'Runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode')
|
|
131
143
|
.option('--start', 'Start immediately').option('--json', 'Print JSON').action((options) => { const base = templatePatch(options.template); const prompt = options.prompt ?? base.prompt; if (typeof prompt !== 'string' || !prompt.trim())
|
|
132
|
-
throw new Error('Choose --template <id> or provide --prompt <text>'); const input = { mode: loopMode(options.mode) ?? base.mode ?? defaults.mode ?? 'goal', name: options.name ?? base.name, description: options.description ?? base.description, enabled: options.start === true, target: targetFromOptions(options), profile: options.profile, prompt, maxIterations: options.maxIterations !== undefined ? maxIterations(options.maxIterations) : typeof base.maxIterations === 'number' ? base.maxIterations : undefined, tokenBudget: tokenBudget(options.tokenBudget), stopPolicy: base.stopPolicy ?? undefined }; applyRuntimeCreateOptions(input, options); const store = createDefaultPiboLoopStore({ path: program.opts().store }); const job = store.createJob(input); if (options.json)
|
|
144
|
+
throw new Error('Choose --template <id> or provide --prompt <text>'); const input = { mode: loopMode(options.mode) ?? base.mode ?? defaults.mode ?? 'goal', name: options.name ?? base.name, description: options.description ?? base.description, enabled: options.start === true, target: targetFromOptions(options), profile: options.profile, prompt, maxIterations: options.maxIterations !== undefined ? maxIterations(options.maxIterations) : typeof base.maxIterations === 'number' ? base.maxIterations : undefined, tokenBudget: tokenBudget(options.tokenBudget), tokenReserve: tokenReserve(options.tokenReserve), stopPolicy: base.stopPolicy ?? undefined }; applyRuntimeCreateOptions(input, options); const store = createDefaultPiboLoopStore({ path: program.opts().store }); const job = store.createJob(input); if (options.json)
|
|
133
145
|
printJson(job);
|
|
134
146
|
else
|
|
135
147
|
console.log(`${job.id}\t${job.enabled ? 'running' : 'stopped'}\t${job.name}`); store.close(); });
|
|
136
|
-
program.command('edit').argument('<id>', defaults.mode === 'ralph' ? 'Ralph job id' : 'Loop job id').description(defaults.mode === 'ralph' ? 'Update a Ralph job' : 'Update a Loop job').option('--template <id>', 'Apply a built-in job template before explicit overrides').option('--mode <mode>', 'Set loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile').option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', 'Set Goal token budget').option('--clear-token-budget', 'Clear Goal token budget').option('--model <provider/model>', 'Set runtime model override, for example openai/gpt-5').option('--clear-model', 'Clear runtime model override').option('--thinking <level>', 'Set runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--clear-thinking', 'Clear runtime thinking level override').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode').option('--clear-fast', 'Clear runtime fast mode override').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboLoopStore({ path: program.opts().store }); const patch = { ...templatePatch(options.template) }; if (options.mode !== undefined)
|
|
148
|
+
program.command('edit').argument('<id>', defaults.mode === 'ralph' ? 'Ralph job id' : 'Loop job id').description(defaults.mode === 'ralph' ? 'Update a Ralph job' : 'Update a Loop job').option('--template <id>', 'Apply a built-in job template before explicit overrides').option('--mode <mode>', 'Set loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile').option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', 'Set soft Goal token budget').option('--clear-token-budget', 'Clear Goal token budget').option('--token-reserve <n>', 'Set pre-turn minimum remaining tokens').option('--clear-token-reserve', 'Clear Goal token reserve').option('--model <provider/model>', 'Set runtime model override, for example openai/gpt-5').option('--clear-model', 'Clear runtime model override').option('--thinking <level>', 'Set runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--clear-thinking', 'Clear runtime thinking level override').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode').option('--clear-fast', 'Clear runtime fast mode override').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboLoopStore({ path: program.opts().store }); const patch = { ...templatePatch(options.template) }; if (options.mode !== undefined)
|
|
137
149
|
patch.mode = loopMode(options.mode); if (options.name !== undefined)
|
|
138
150
|
patch.name = options.name; if (options.description !== undefined)
|
|
139
151
|
patch.description = options.description; if (options.profile !== undefined)
|
|
@@ -143,7 +155,11 @@ export async function runLoopCli(argv = process.argv, defaults = {}) {
|
|
|
143
155
|
throw new Error('Choose either --token-budget or --clear-token-budget, not both'); if (options.clearTokenBudget)
|
|
144
156
|
patch.tokenBudget = null;
|
|
145
157
|
else if (options.tokenBudget !== undefined)
|
|
146
|
-
patch.tokenBudget = tokenBudget(options.tokenBudget);
|
|
158
|
+
patch.tokenBudget = tokenBudget(options.tokenBudget); if (options.tokenReserve !== undefined && options.clearTokenReserve)
|
|
159
|
+
throw new Error('Choose either --token-reserve or --clear-token-reserve, not both'); if (options.clearTokenReserve)
|
|
160
|
+
patch.tokenReserve = null;
|
|
161
|
+
else if (options.tokenReserve !== undefined)
|
|
162
|
+
patch.tokenReserve = tokenReserve(options.tokenReserve); applyRuntimePatchOptions(patch, options); const target = maybeTargetFromOptions(options); if (target)
|
|
147
163
|
patch.target = target; if (Object.keys(patch).length === 0)
|
|
148
164
|
throw new Error('No Loop job update fields provided'); const job = store.updateJob(id, patch); if (!job)
|
|
149
165
|
throw new Error('Loop job not found'); if (options.json)
|
package/dist/loops/prompts.js
CHANGED
|
@@ -21,6 +21,7 @@ function buildGoalTurnPrompt(job, continuation, goalToolsAvailable) {
|
|
|
21
21
|
const tokensUsed = job.state.tokensUsed ?? 0;
|
|
22
22
|
const tokenBudget = job.tokenBudget;
|
|
23
23
|
const remainingTokens = tokenBudget === undefined ? 'unbounded' : String(Math.max(0, tokenBudget - tokensUsed));
|
|
24
|
+
const tokenReserve = job.tokenReserve ?? 0;
|
|
24
25
|
return [
|
|
25
26
|
continuation ? 'Continue working toward the active Pibo loop goal.' : 'Start working toward the active Pibo loop goal.',
|
|
26
27
|
'',
|
|
@@ -36,8 +37,10 @@ function buildGoalTurnPrompt(job, continuation, goalToolsAvailable) {
|
|
|
36
37
|
'- Temporary rough edges are acceptable while work moves toward the requested end state. Completion still requires the requested end state to be true and verified.',
|
|
37
38
|
'',
|
|
38
39
|
'Budget:',
|
|
40
|
+
'- Budget enforcement: soft; model usage is reported after a response and the current turn can overshoot the limit.',
|
|
39
41
|
`- Reported tokens used before this turn: ${tokensUsed}`,
|
|
40
|
-
`-
|
|
42
|
+
`- Soft token budget: ${tokenBudget ?? 'none'}`,
|
|
43
|
+
`- Pre-turn token reserve: ${tokenReserve}`,
|
|
41
44
|
`- Reported tokens remaining before this turn: ${remainingTokens}`,
|
|
42
45
|
'',
|
|
43
46
|
'Work from evidence:',
|
package/dist/loops/service.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getDefaultPiboWorkspace } from '../core/workspace.js';
|
|
|
5
5
|
import { PiboDataStore } from '../data/pibo-store.js';
|
|
6
6
|
import { ChatRoomService } from '../apps/chat/data/room-service.js';
|
|
7
7
|
import { isPiboRoomArchived } from '../apps/chat/types/rooms.js';
|
|
8
|
-
import { browserPoolPaths, releaseBrowserPoolLease } from '../tools/browser-pool.js';
|
|
8
|
+
import { acquireBrowserPoolLease, browserPoolPaths, releaseBrowserPoolLease, restartRecordedBrowserPoolChrome } from '../tools/browser-pool.js';
|
|
9
9
|
import { createDefaultPiboLoopStore } from './store.js';
|
|
10
10
|
import { createBuiltInLoopStopConditions, evaluateLoopStopPolicy } from './stopping.js';
|
|
11
11
|
import { buildLoopTurnPrompt } from './prompts.js';
|
|
@@ -48,6 +48,8 @@ export class PiboLoopService {
|
|
|
48
48
|
activeRuns = 0;
|
|
49
49
|
stopped = true;
|
|
50
50
|
cancelledRuns = new Set();
|
|
51
|
+
browserLeaseHeartbeatTimers = new Map();
|
|
52
|
+
browserLeaseHeartbeatWork = new Map();
|
|
51
53
|
unsubscribeProductEvents;
|
|
52
54
|
unsubscribeOutputEvents;
|
|
53
55
|
constructor(options) {
|
|
@@ -62,7 +64,8 @@ export class PiboLoopService {
|
|
|
62
64
|
start() { if (!this.stopped)
|
|
63
65
|
return; this.stopped = false; this.store.recoverInterruptedRuns(); this.unsubscribeProductEvents = this.options.context.subscribeProductEvents?.((event) => this.handleProductEvent(event)); this.unsubscribeOutputEvents = this.options.context.subscribe((event) => this.handleOutputEvent(event)); this.arm(250); }
|
|
64
66
|
stop() { this.stopped = true; if (this.timer)
|
|
65
|
-
clearTimeout(this.timer); this.timer = undefined;
|
|
67
|
+
clearTimeout(this.timer); this.timer = undefined; for (const timer of this.browserLeaseHeartbeatTimers.values())
|
|
68
|
+
clearInterval(timer); this.browserLeaseHeartbeatTimers.clear(); this.unsubscribeProductEvents?.(); this.unsubscribeProductEvents = undefined; this.unsubscribeOutputEvents?.(); this.unsubscribeOutputEvents = undefined; this.dataStore.close(); this.store.close(); }
|
|
66
69
|
status() { return { enabled: !this.stopped, ...this.store.status() }; }
|
|
67
70
|
async startJob(id) { const job = this.store.updateJob(id, { enabled: true }); if (!job)
|
|
68
71
|
return undefined; const reserved = await this.reserveAfterBeforeRunEvaluation(job); if (!reserved)
|
|
@@ -109,7 +112,70 @@ export class PiboLoopService {
|
|
|
109
112
|
return undefined;
|
|
110
113
|
}
|
|
111
114
|
this.store.applyStopEvaluation({ jobId: fresh.id, evaluation, conditionStates, disable: false });
|
|
112
|
-
|
|
115
|
+
if (fresh.mode === 'goal' && !await this.renewGoalBrowserLeases(fresh))
|
|
116
|
+
return undefined;
|
|
117
|
+
const reserved = this.store.reserveRun(fresh.id);
|
|
118
|
+
if (reserved)
|
|
119
|
+
this.startGoalBrowserLeaseHeartbeat(reserved.job, reserved.run);
|
|
120
|
+
return reserved;
|
|
121
|
+
}
|
|
122
|
+
startGoalBrowserLeaseHeartbeat(job, run) {
|
|
123
|
+
if (job.mode !== 'goal' || (job.resources?.browserLeaseIds?.length ?? 0) === 0)
|
|
124
|
+
return;
|
|
125
|
+
const renew = () => {
|
|
126
|
+
if (this.browserLeaseHeartbeatWork.has(run.id))
|
|
127
|
+
return;
|
|
128
|
+
const work = this.renewGoalBrowserLeases(this.store.getJob(job.id) ?? job, run).finally(() => { this.browserLeaseHeartbeatWork.delete(run.id); });
|
|
129
|
+
this.browserLeaseHeartbeatWork.set(run.id, work);
|
|
130
|
+
};
|
|
131
|
+
const timer = setInterval(renew, Math.max(10, this.options.resourceCleanup?.browserLeaseRenewIntervalMs ?? 5 * 60_000));
|
|
132
|
+
this.browserLeaseHeartbeatTimers.set(run.id, timer);
|
|
133
|
+
}
|
|
134
|
+
async stopGoalBrowserLeaseHeartbeat(runId) {
|
|
135
|
+
const timer = this.browserLeaseHeartbeatTimers.get(runId);
|
|
136
|
+
if (timer)
|
|
137
|
+
clearInterval(timer);
|
|
138
|
+
this.browserLeaseHeartbeatTimers.delete(runId);
|
|
139
|
+
await this.browserLeaseHeartbeatWork.get(runId);
|
|
140
|
+
}
|
|
141
|
+
async renewGoalBrowserLeases(job, run) {
|
|
142
|
+
const resources = mergeRunResources(job.resources, run?.resources);
|
|
143
|
+
const leaseIds = resources?.browserLeaseIds ?? [];
|
|
144
|
+
if (!resources || leaseIds.length === 0)
|
|
145
|
+
return true;
|
|
146
|
+
const workerId = resources.workerId || process.env.PIBO_BROWSER_POOL_WORKER_ID || process.env.PIBO_COMPUTE_WORKER_ID || process.env.HOSTNAME || 'local';
|
|
147
|
+
const poolId = this.options.resourceCleanup?.browserPoolId || process.env.PIBO_BROWSER_POOL_ID || 'default';
|
|
148
|
+
const rootDir = this.options.resourceCleanup?.browserPoolRootDir || process.env.PIBO_BROWSER_POOL_ROOT || join(process.env.BROWSER_USE_HOME || join(homedir(), '.browser-use'), 'pibo-browser-pool');
|
|
149
|
+
const identity = { workerId, poolId };
|
|
150
|
+
const paths = browserPoolPaths(rootDir, identity);
|
|
151
|
+
const acquire = this.options.resourceCleanup?.acquireBrowserPoolLease ?? acquireBrowserPoolLease;
|
|
152
|
+
let retainedUntil;
|
|
153
|
+
for (const leaseId of leaseIds) {
|
|
154
|
+
try {
|
|
155
|
+
const result = await acquire(paths, identity, {
|
|
156
|
+
leaseId,
|
|
157
|
+
holder: run ? `loop:${job.id}:run:${run.id}` : `loop:${job.id}`,
|
|
158
|
+
idleTimeoutMs: this.options.resourceCleanup?.browserLeaseIdleTimeoutMs,
|
|
159
|
+
startBrowser: restartRecordedBrowserPoolChrome,
|
|
160
|
+
lockOptions: { holder: run ? `loop:${job.id}:run:${run.id}` : `loop:${job.id}` },
|
|
161
|
+
});
|
|
162
|
+
if (!result.acquired)
|
|
163
|
+
throw new Error(result.staleReason);
|
|
164
|
+
retainedUntil = result.state.idleExpiresAt ?? retainedUntil;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const reason = `Browser lease ${leaseId} could not be renewed or reacquired: ${errorMessage(error)}. Authenticated browser access requires operator attention.`;
|
|
168
|
+
this.markRunResourcesDirty(job, reason);
|
|
169
|
+
this.store.updateGoalStatus(job.id, 'blocked');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const updatedAt = new Date().toISOString();
|
|
174
|
+
const next = clearDirtyReason({ ...resources, workerId, cleanupState: 'active', retainedUntil, updatedAt });
|
|
175
|
+
this.store.updateJobResources(job.id, next);
|
|
176
|
+
if (run)
|
|
177
|
+
this.store.updateRunResources({ jobId: job.id, runId: run.id, resources: next });
|
|
178
|
+
return true;
|
|
113
179
|
}
|
|
114
180
|
async abortCancelRequestedJobs() { for (const job of this.store.listJobs({ includeDisabled: true })) {
|
|
115
181
|
if (job.state.cancelRequestedAt)
|
|
@@ -134,17 +200,25 @@ export class PiboLoopService {
|
|
|
134
200
|
}
|
|
135
201
|
async executeReserved(job, run) {
|
|
136
202
|
this.activeRuns += 1;
|
|
203
|
+
const activeStartedAt = Date.now();
|
|
204
|
+
let activeTimeRecorded = false;
|
|
205
|
+
const recordActiveTime = () => {
|
|
206
|
+
if (activeTimeRecorded)
|
|
207
|
+
return;
|
|
208
|
+
activeTimeRecorded = true;
|
|
209
|
+
this.recordGoalRunTime(job, run, activeStartedAt);
|
|
210
|
+
};
|
|
137
211
|
try {
|
|
138
212
|
const result = await this.executeJob(job, run);
|
|
213
|
+
recordActiveTime();
|
|
139
214
|
const cancelled = this.cancelledRuns.delete(run.id);
|
|
140
|
-
if (job.mode === 'goal')
|
|
141
|
-
this.store.recordGoalProgress(job.id, { timeUsedSeconds: result.timeUsedSeconds });
|
|
142
215
|
const outcome = { status: cancelled ? 'cancelled' : 'ok', piboSessionId: result.piboSessionId, finalAnswer: result.finalAnswer };
|
|
143
216
|
const { evaluation, conditionStates } = await this.evaluateStopPolicy(this.store.getJob(job.id) ?? job, 'after-run', run, outcome);
|
|
144
217
|
this.store.completeRun({ jobId: job.id, runId: run.id, status: outcome.status, piboSessionId: result.piboSessionId, reason: cancelled ? 'cancelled' : evaluation.reason, stopAfterRun: evaluation.finalAction !== 'continue', stopEvaluation: evaluation, conditionStates });
|
|
145
218
|
await this.cleanupRunResources(job, run);
|
|
146
219
|
}
|
|
147
220
|
catch (error) {
|
|
221
|
+
recordActiveTime();
|
|
148
222
|
const cancelled = this.cancelledRuns.delete(run.id);
|
|
149
223
|
const message = errorMessage(error);
|
|
150
224
|
const fatalProfileError = !cancelled && isUnknownProfileErrorMessage(message);
|
|
@@ -160,6 +234,11 @@ export class PiboLoopService {
|
|
|
160
234
|
this.activeRuns -= 1;
|
|
161
235
|
}
|
|
162
236
|
}
|
|
237
|
+
recordGoalRunTime(job, run, startedAt) {
|
|
238
|
+
if (job.mode !== 'goal')
|
|
239
|
+
return;
|
|
240
|
+
this.store.recordGoalRunTime(job.id, run.id, Math.max(0, Math.ceil((Date.now() - startedAt) / 1000)));
|
|
241
|
+
}
|
|
163
242
|
markRunResourcesDirty(job, dirtyReason) {
|
|
164
243
|
const latestJob = this.store.getJob(job.id) ?? job;
|
|
165
244
|
const runId = latestJob.state.lastRunId ?? job.state.lastRunId;
|
|
@@ -178,12 +257,20 @@ export class PiboLoopService {
|
|
|
178
257
|
}
|
|
179
258
|
}
|
|
180
259
|
async cleanupRunResources(job, run) {
|
|
260
|
+
await this.stopGoalBrowserLeaseHeartbeat(run.id);
|
|
181
261
|
const latestJob = this.store.getJob(job.id) ?? job;
|
|
182
262
|
const latestRun = this.store.listRuns({ jobId: job.id, limit: 100 }).find((candidate) => candidate.id === run.id) ?? run;
|
|
183
263
|
const resources = mergeRunResources(latestJob.resources, latestRun.resources);
|
|
184
264
|
const leaseIds = resources?.browserLeaseIds ?? [];
|
|
185
265
|
if (!resources || leaseIds.length === 0)
|
|
186
266
|
return;
|
|
267
|
+
const goalStatus = latestJob.mode === 'goal' ? latestJob.state.goalStatus ?? (latestJob.enabled ? 'active' : 'paused') : undefined;
|
|
268
|
+
if (latestJob.mode === 'goal' && latestJob.enabled && goalStatus === 'active') {
|
|
269
|
+
const retained = clearDirtyReason({ ...resources, cleanupState: 'retained', updatedAt: new Date().toISOString() });
|
|
270
|
+
this.store.updateRunResources({ jobId: job.id, runId: run.id, resources: retained });
|
|
271
|
+
this.store.updateJobResources(job.id, retained);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
187
274
|
const workerId = resources.workerId || process.env.PIBO_BROWSER_POOL_WORKER_ID || process.env.PIBO_COMPUTE_WORKER_ID || process.env.HOSTNAME || 'local';
|
|
188
275
|
const poolId = this.options.resourceCleanup?.browserPoolId || process.env.PIBO_BROWSER_POOL_ID || 'default';
|
|
189
276
|
const rootDir = this.options.resourceCleanup?.browserPoolRootDir || process.env.PIBO_BROWSER_POOL_ROOT || join(process.env.BROWSER_USE_HOME || join(homedir(), '.browser-use'), 'pibo-browser-pool');
|
|
@@ -228,7 +315,10 @@ export class PiboLoopService {
|
|
|
228
315
|
const status = job.state.goalStatus ?? (job.enabled ? 'active' : 'paused');
|
|
229
316
|
if (!job.state.runningAt && status !== 'active')
|
|
230
317
|
return;
|
|
231
|
-
|
|
318
|
+
if (job.state.runningAt && job.state.lastRunId)
|
|
319
|
+
this.store.recordGoalTurnUsage(job.id, job.state.lastRunId, event.totalTokens);
|
|
320
|
+
else
|
|
321
|
+
this.store.recordGoalProgress(job.id, { tokens: event.totalTokens });
|
|
232
322
|
}
|
|
233
323
|
handleProductEvent(event) {
|
|
234
324
|
if (event.type !== 'pibo.loop.fact' && event.type !== 'loop.fact' && event.type !== 'pibo.ralph.fact' && event.type !== 'ralph.fact')
|
|
@@ -304,7 +394,6 @@ export class PiboLoopService {
|
|
|
304
394
|
} const room = this.roomService.ensureDefaultRoom({ name: 'Shared Chat' }); return { roomId: room.id, workspace: room.workspace ?? getDefaultPiboWorkspace() }; }
|
|
305
395
|
async emitMessageAndWait(piboSessionId, text) {
|
|
306
396
|
const eventId = `loop_msg_${randomUUID()}`;
|
|
307
|
-
const startedAt = Date.now();
|
|
308
397
|
return await new Promise((resolve, reject) => {
|
|
309
398
|
let settled = false;
|
|
310
399
|
let deltaAnswer = '';
|
|
@@ -323,7 +412,7 @@ export class PiboLoopService {
|
|
|
323
412
|
if (error)
|
|
324
413
|
reject(error);
|
|
325
414
|
else
|
|
326
|
-
resolve({ finalAnswer: finalAnswer || deltaAnswer
|
|
415
|
+
resolve({ finalAnswer: finalAnswer || deltaAnswer });
|
|
327
416
|
};
|
|
328
417
|
if (this.runTimeoutMs !== undefined) {
|
|
329
418
|
timeout = setTimeout(() => {
|