@pasko70/pibo 1.10.0 → 1.10.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/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-LHRs1Nhr.js → dist-3Sts0Afa.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-4NlLjLs7.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-BA6mhAec.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-BPl-fzRB.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-BPnn9e2B.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BpfBSMzK.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-Cvx5M97R.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-DnXWNQRm.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-hU-2oAb6.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-r31x5Fcc.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-uF6UXzI7.js} +1 -1
- package/dist/apps/chat-ui/assets/index-CaTGYBOS.js +173 -0
- package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-al8DeEjA.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-CK4SMZuu.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli-session/localSessionSource.js +34 -12
- package/dist/compute/resource-health.js +35 -2
- package/dist/core/events.js +6 -1
- package/dist/core/routed-session.js +67 -4
- package/dist/core/session-router.js +23 -6
- package/dist/data/ingest-service.js +3 -1
- package/dist/debug/index.js +4 -0
- package/dist/debug/trace.js +42 -7
- 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/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 +6 -2
- 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-BAMxIaI_.js +0 -41
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(() => {
|
package/dist/loops/store.js
CHANGED
|
@@ -75,13 +75,37 @@ function resourceMetadataJson(resources) {
|
|
|
75
75
|
const normalized = normalizeLoopResourceMetadata(resources);
|
|
76
76
|
return normalized ? JSON.stringify(normalized) : null;
|
|
77
77
|
}
|
|
78
|
+
function parseRunAccounting(json) {
|
|
79
|
+
if (!json)
|
|
80
|
+
return undefined;
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(json);
|
|
83
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function runAccountingJson(accounting) { return accounting ? JSON.stringify(accounting) : null; }
|
|
90
|
+
function normalizeJobState(state, mode, enabled, createdAt) {
|
|
91
|
+
if (mode !== 'goal')
|
|
92
|
+
return state;
|
|
93
|
+
const activeTimeSeconds = Math.max(0, Math.floor(state.activeTimeSeconds ?? state.timeUsedSeconds ?? 0));
|
|
94
|
+
const goalStartedAt = state.goalStartedAt ?? (enabled || (state.completedIterations ?? 0) > 0 || (state.tokensUsed ?? 0) > 0 || (state.goalStatus !== undefined && state.goalStatus !== 'paused') ? createdAt : undefined);
|
|
95
|
+
const normalized = { ...state, activeTimeSeconds, ...(goalStartedAt ? { goalStartedAt } : {}) };
|
|
96
|
+
delete normalized.timeUsedSeconds;
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
78
99
|
function jobFromRow(row) {
|
|
79
100
|
const resources = parseResourceMetadata(row.resource_json);
|
|
80
|
-
|
|
101
|
+
const mode = normalizeLoopMode(row.loop_mode, 'ralph');
|
|
102
|
+
const enabled = row.enabled === 1;
|
|
103
|
+
return { id: row.id, mode, name: row.name, description: row.description ?? undefined, enabled, target: parseLoopTarget(row.target_json), profile: row.profile, prompt: row.prompt, maxIterations: row.max_iterations ?? undefined, tokenBudget: row.token_budget ?? undefined, tokenReserve: row.token_reserve ?? undefined, stopPolicy: parseStopPolicy(row.stop_policy_json), ...parseRuntimeOptions(row.runtime_options_json), ...(resources ? { resources } : {}), state: normalizeJobState(parseJson(row.state_json), mode, enabled, row.created_at), createdAt: row.created_at, updatedAt: row.updated_at };
|
|
81
104
|
}
|
|
82
105
|
function runFromRow(row) {
|
|
83
106
|
const resources = parseResourceMetadata(row.resource_json);
|
|
84
|
-
|
|
107
|
+
const accounting = parseRunAccounting(row.accounting_json);
|
|
108
|
+
return { id: row.id, jobId: row.job_id, piboSessionId: row.pibo_session_id ?? undefined, status: row.status, reason: row.reason ?? undefined, error: row.error ?? undefined, startedAt: row.started_at ?? undefined, completedAt: row.completed_at ?? undefined, ...(accounting ? { accounting } : {}), ...(resources ? { resources } : {}), createdAt: row.created_at, updatedAt: row.updated_at };
|
|
85
109
|
}
|
|
86
110
|
function mergeResourceMetadata(jobResources, runResources) {
|
|
87
111
|
if (!jobResources && !runResources)
|
|
@@ -106,6 +130,13 @@ function normalizeTokenBudget(value) {
|
|
|
106
130
|
throw new Error('tokenBudget must be a positive integer');
|
|
107
131
|
return value;
|
|
108
132
|
}
|
|
133
|
+
function normalizeTokenReserve(value) {
|
|
134
|
+
if (value === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
if (!Number.isInteger(value) || value < 0)
|
|
137
|
+
throw new Error('tokenReserve must be a non-negative integer');
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
109
140
|
function goalStatus(job) {
|
|
110
141
|
if (job.mode !== 'goal')
|
|
111
142
|
return undefined;
|
|
@@ -213,10 +244,15 @@ function validateJobInput(input) {
|
|
|
213
244
|
throw new Error('prompt is required');
|
|
214
245
|
if (input.target.kind === 'room' && !input.target.roomId.trim())
|
|
215
246
|
throw new Error('target.roomId is required');
|
|
216
|
-
if (input.mode === 'ralph' && input.tokenBudget !== undefined)
|
|
217
|
-
throw new Error('tokenBudget
|
|
247
|
+
if (input.mode === 'ralph' && (input.tokenBudget !== undefined || input.tokenReserve !== undefined))
|
|
248
|
+
throw new Error('tokenBudget and tokenReserve are only available for goal mode');
|
|
218
249
|
normalizeMaxIterations(input.maxIterations);
|
|
219
250
|
normalizeTokenBudget(input.tokenBudget);
|
|
251
|
+
normalizeTokenReserve(input.tokenReserve);
|
|
252
|
+
if (input.tokenReserve !== undefined && input.tokenBudget === undefined)
|
|
253
|
+
throw new Error('tokenReserve requires tokenBudget');
|
|
254
|
+
if (input.tokenReserve !== undefined && input.tokenBudget !== undefined && input.tokenReserve >= input.tokenBudget)
|
|
255
|
+
throw new Error('tokenReserve must be smaller than tokenBudget');
|
|
220
256
|
normalizeRuntimeOptions(input);
|
|
221
257
|
normalizeLoopStopPolicy(input.stopPolicy);
|
|
222
258
|
normalizeLoopResourceMetadata(input.resources);
|
|
@@ -246,10 +282,10 @@ export class PiboLoopStore {
|
|
|
246
282
|
const enabled = input.enabled === true;
|
|
247
283
|
const state = {
|
|
248
284
|
completedIterations: 0,
|
|
249
|
-
...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0,
|
|
285
|
+
...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
|
|
250
286
|
...(input.initialPiboSessionId?.trim() ? { lastPiboSessionId: input.initialPiboSessionId.trim() } : {}),
|
|
251
287
|
};
|
|
252
|
-
const job = { id: mode === 'ralph' ? `ralph_${randomUUID()}` : `loop_${randomUUID()}`, mode, name: (input.name ?? defaultName(input.prompt)).trim(), description: input.description?.trim() || undefined, enabled, target, profile: input.profile, prompt: input.prompt, maxIterations: normalizeMaxIterations(input.maxIterations), tokenBudget: normalizeTokenBudget(input.tokenBudget), stopPolicy: normalizeLoopStopPolicy(input.stopPolicy), ...runtimeOptions, ...(resources ? { resources } : {}), state, createdAt: timestamp, updatedAt: timestamp };
|
|
288
|
+
const job = { id: mode === 'ralph' ? `ralph_${randomUUID()}` : `loop_${randomUUID()}`, mode, name: (input.name ?? defaultName(input.prompt)).trim(), description: input.description?.trim() || undefined, enabled, target, profile: input.profile, prompt: input.prompt, maxIterations: normalizeMaxIterations(input.maxIterations), tokenBudget: normalizeTokenBudget(input.tokenBudget), tokenReserve: normalizeTokenReserve(input.tokenReserve), stopPolicy: normalizeLoopStopPolicy(input.stopPolicy), ...runtimeOptions, ...(resources ? { resources } : {}), state, createdAt: timestamp, updatedAt: timestamp };
|
|
253
289
|
this.insertJob(job);
|
|
254
290
|
return this.getJob(job.id);
|
|
255
291
|
}
|
|
@@ -264,6 +300,10 @@ export class PiboLoopStore {
|
|
|
264
300
|
return undefined;
|
|
265
301
|
const timestamp = nowIso(now);
|
|
266
302
|
const state = { ...job.state, goalStatus: status, runningAt: job.state.runningAt };
|
|
303
|
+
if (job.state.runningAt)
|
|
304
|
+
delete state.goalEndedAt;
|
|
305
|
+
else
|
|
306
|
+
state.goalEndedAt = timestamp;
|
|
267
307
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, id);
|
|
268
308
|
return this.getJob(id);
|
|
269
309
|
}
|
|
@@ -272,20 +312,65 @@ export class PiboLoopStore {
|
|
|
272
312
|
if (!job || job.mode !== 'goal')
|
|
273
313
|
return job;
|
|
274
314
|
const tokens = Math.max(0, Math.floor(input.tokens ?? 0));
|
|
275
|
-
const
|
|
315
|
+
const activeTimeSeconds = Math.max(0, Math.floor(input.activeTimeSeconds ?? 0));
|
|
276
316
|
const nextTokens = (job.state.tokensUsed ?? 0) + tokens;
|
|
277
317
|
const currentStatus = goalStatus(job) ?? 'active';
|
|
278
318
|
const budgetLimited = currentStatus === 'active' && job.tokenBudget !== undefined && nextTokens >= job.tokenBudget;
|
|
279
319
|
const state = {
|
|
280
320
|
...job.state,
|
|
281
321
|
tokensUsed: nextTokens,
|
|
282
|
-
|
|
322
|
+
activeTimeSeconds: (job.state.activeTimeSeconds ?? 0) + activeTimeSeconds,
|
|
283
323
|
goalStatus: budgetLimited ? 'budget_limited' : currentStatus,
|
|
284
324
|
};
|
|
285
325
|
const timestamp = nowIso(now);
|
|
326
|
+
if (budgetLimited && !job.state.runningAt)
|
|
327
|
+
state.goalEndedAt = timestamp;
|
|
286
328
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(budgetLimited ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, id);
|
|
287
329
|
return this.getJob(id);
|
|
288
330
|
}
|
|
331
|
+
recordGoalTurnUsage(id, runId, tokens, now = new Date()) {
|
|
332
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
333
|
+
try {
|
|
334
|
+
const job = this.recordGoalProgress(id, { tokens }, now);
|
|
335
|
+
if (job?.mode === 'goal') {
|
|
336
|
+
const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
|
|
337
|
+
if (row) {
|
|
338
|
+
const accounting = parseRunAccounting(row.accounting_json) ?? {};
|
|
339
|
+
const turnTokens = (accounting.tokensUsed ?? 0) + Math.max(0, Math.floor(tokens));
|
|
340
|
+
const budget = accounting.tokenBudget;
|
|
341
|
+
const before = accounting.tokensUsedBefore ?? 0;
|
|
342
|
+
const nextAccounting = { ...accounting, tokensUsed: turnTokens, ...(budget !== undefined ? { overshootTokens: Math.max(0, before + turnTokens - budget) } : {}) };
|
|
343
|
+
this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(nextAccounting), nowIso(now), runId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
this.db.exec('COMMIT');
|
|
347
|
+
return job;
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
this.db.exec('ROLLBACK');
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
recordGoalRunTime(id, runId, activeTimeSeconds, now = new Date()) {
|
|
355
|
+
const seconds = Math.max(0, Math.floor(activeTimeSeconds));
|
|
356
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
357
|
+
try {
|
|
358
|
+
const job = this.recordGoalProgress(id, { activeTimeSeconds: seconds }, now);
|
|
359
|
+
if (job?.mode === 'goal') {
|
|
360
|
+
const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
|
|
361
|
+
if (row) {
|
|
362
|
+
const accounting = { ...(parseRunAccounting(row.accounting_json) ?? {}), activeTimeSeconds: seconds };
|
|
363
|
+
this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(accounting), nowIso(now), runId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.db.exec('COMMIT');
|
|
367
|
+
return job;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
this.db.exec('ROLLBACK');
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
289
374
|
listJobs(input = {}) {
|
|
290
375
|
const clauses = [];
|
|
291
376
|
const values = [];
|
|
@@ -309,16 +394,19 @@ export class PiboLoopStore {
|
|
|
309
394
|
const enabled = patch.enabled ?? existing.enabled;
|
|
310
395
|
let state = mode === existing.mode
|
|
311
396
|
? { ...existing.state }
|
|
312
|
-
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0,
|
|
397
|
+
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
|
|
313
398
|
if (mode === 'goal' && patch.enabled !== undefined) {
|
|
314
399
|
const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
|
|
315
400
|
if (patch.enabled) {
|
|
316
401
|
if (currentGoalStatus === 'complete')
|
|
317
402
|
throw new Error('Completed goals cannot be restarted; create a new goal');
|
|
318
403
|
const nextBudget = hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget;
|
|
319
|
-
|
|
320
|
-
|
|
404
|
+
const nextReserve = hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve;
|
|
405
|
+
if (currentGoalStatus === 'budget_limited' && nextBudget !== undefined && (existing.state.tokensUsed ?? 0) + (nextReserve ?? 0) >= nextBudget)
|
|
406
|
+
throw new Error('Increase or clear the token budget, or lower the token reserve, before resuming this goal');
|
|
321
407
|
state.goalStatus = 'active';
|
|
408
|
+
state.goalStartedAt ??= nowIso(now);
|
|
409
|
+
delete state.goalEndedAt;
|
|
322
410
|
state.stopRequestedAt = undefined;
|
|
323
411
|
state.cancelRequestedAt = undefined;
|
|
324
412
|
}
|
|
@@ -326,7 +414,7 @@ export class PiboLoopStore {
|
|
|
326
414
|
state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
|
|
327
415
|
}
|
|
328
416
|
}
|
|
329
|
-
const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
|
|
417
|
+
const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, tokenReserve: mode === 'ralph' || (hasOwn(patch, 'tokenBudget') && patch.tokenBudget === null && !hasOwn(patch, 'tokenReserve')) ? undefined : hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
|
|
330
418
|
validateJobInput(next);
|
|
331
419
|
this.writeJob(next);
|
|
332
420
|
return this.getJob(id);
|
|
@@ -418,7 +506,7 @@ export class PiboLoopStore {
|
|
|
418
506
|
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
419
507
|
const terminalGoalStatus = job.mode === 'goal' && ['complete', 'blocked', 'budget_limited'].includes(goalStatus(job) ?? '');
|
|
420
508
|
const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
|
|
421
|
-
const state = { ...job.state, runningAt: undefined, completedIterations, lastRunAt: timestamp, lastRunId: input.runId, lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok', lastError: input.error, lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId, consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation };
|
|
509
|
+
const state = { ...job.state, runningAt: undefined, completedIterations, lastRunAt: timestamp, lastRunId: input.runId, lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok', lastError: input.error, lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId, consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation, ...(terminalGoalStatus ? { goalEndedAt: job.state.goalEndedAt ?? timestamp } : {}) };
|
|
422
510
|
this.db.prepare('UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, completed_at = ?, updated_at = ? WHERE id = ?').run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, timestamp, timestamp, input.runId);
|
|
423
511
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(shouldDisable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
|
|
424
512
|
}
|
|
@@ -486,6 +574,15 @@ export class PiboLoopStore {
|
|
|
486
574
|
this.db.exec('COMMIT');
|
|
487
575
|
return undefined;
|
|
488
576
|
}
|
|
577
|
+
if (job.mode === 'goal' && job.tokenBudget !== undefined) {
|
|
578
|
+
const remaining = Math.max(0, job.tokenBudget - (job.state.tokensUsed ?? 0));
|
|
579
|
+
if (remaining <= (job.tokenReserve ?? 0)) {
|
|
580
|
+
const state = { ...job.state, goalStatus: 'budget_limited', goalEndedAt: timestamp };
|
|
581
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, job.id);
|
|
582
|
+
this.db.exec('COMMIT');
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
489
586
|
const run = this.createRunLocked(job, timestamp);
|
|
490
587
|
const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id };
|
|
491
588
|
this.updateJobStateLocked(job.id, state, timestamp);
|
|
@@ -502,13 +599,15 @@ export class PiboLoopStore {
|
|
|
502
599
|
this.ensureJobColumn('loop_mode', "TEXT NOT NULL DEFAULT 'ralph'");
|
|
503
600
|
this.ensureJobColumn('max_iterations', 'INTEGER');
|
|
504
601
|
this.ensureJobColumn('token_budget', 'INTEGER');
|
|
602
|
+
this.ensureJobColumn('token_reserve', 'INTEGER');
|
|
505
603
|
this.ensureJobColumn('runtime_options_json', 'TEXT');
|
|
506
604
|
this.ensureJobColumn('stop_policy_json', 'TEXT');
|
|
507
605
|
this.ensureJobColumn('resource_json', 'TEXT');
|
|
508
606
|
this.ensureRunColumn('resource_json', 'TEXT');
|
|
607
|
+
this.ensureRunColumn('accounting_json', 'TEXT');
|
|
509
608
|
}
|
|
510
609
|
createFreshSchema() {
|
|
511
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS pibo_ralph_jobs (id TEXT PRIMARY KEY, loop_mode TEXT NOT NULL DEFAULT 'goal', name TEXT NOT NULL, description TEXT, enabled INTEGER NOT NULL, target_json TEXT NOT NULL, profile TEXT NOT NULL, prompt TEXT NOT NULL, max_iterations INTEGER, token_budget INTEGER, runtime_options_json TEXT, stop_policy_json TEXT, resource_json TEXT, state_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_jobs_enabled ON pibo_ralph_jobs(enabled, updated_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_runs (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, pibo_session_id TEXT, status TEXT NOT NULL, reason TEXT, error TEXT, resource_json TEXT, started_at TEXT, completed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_runs_job_created ON pibo_ralph_runs(job_id, created_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_run_facts (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, run_id TEXT, pibo_session_id TEXT, type TEXT NOT NULL, source TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_job_created ON pibo_ralph_run_facts(job_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_run_type ON pibo_ralph_run_facts(run_id, type, created_at DESC);`);
|
|
610
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS pibo_ralph_jobs (id TEXT PRIMARY KEY, loop_mode TEXT NOT NULL DEFAULT 'goal', name TEXT NOT NULL, description TEXT, enabled INTEGER NOT NULL, target_json TEXT NOT NULL, profile TEXT NOT NULL, prompt TEXT NOT NULL, max_iterations INTEGER, token_budget INTEGER, token_reserve INTEGER, runtime_options_json TEXT, stop_policy_json TEXT, resource_json TEXT, state_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_jobs_enabled ON pibo_ralph_jobs(enabled, updated_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_runs (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, pibo_session_id TEXT, status TEXT NOT NULL, reason TEXT, error TEXT, accounting_json TEXT, resource_json TEXT, started_at TEXT, completed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_runs_job_created ON pibo_ralph_runs(job_id, created_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_run_facts (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, run_id TEXT, pibo_session_id TEXT, type TEXT NOT NULL, source TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_job_created ON pibo_ralph_run_facts(job_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_run_type ON pibo_ralph_run_facts(run_id, type, created_at DESC);`);
|
|
512
611
|
}
|
|
513
612
|
ensureJobColumn(name, definition) {
|
|
514
613
|
const columns = this.tableColumns('pibo_ralph_jobs');
|
|
@@ -523,9 +622,21 @@ export class PiboLoopStore {
|
|
|
523
622
|
tableColumns(tableName) {
|
|
524
623
|
return new Set(this.db.prepare(`PRAGMA table_info(${tableName})`).all().map((column) => column.name));
|
|
525
624
|
}
|
|
526
|
-
insertJob(job) { this.db.prepare('INSERT INTO pibo_ralph_jobs (id, loop_mode, name, description, enabled, target_json, profile, prompt, max_iterations, token_budget, runtime_options_json, stop_policy_json, resource_json, state_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(job.id, job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.createdAt, job.updatedAt); }
|
|
527
|
-
writeJob(job) { this.db.prepare('UPDATE pibo_ralph_jobs SET loop_mode = ?, name = ?, description = ?, enabled = ?, target_json = ?, profile = ?, prompt = ?, max_iterations = ?, token_budget = ?, runtime_options_json = ?, stop_policy_json = ?, resource_json = ?, state_json = ?, updated_at = ? WHERE id = ?').run(job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.updatedAt, job.id); }
|
|
625
|
+
insertJob(job) { this.db.prepare('INSERT INTO pibo_ralph_jobs (id, loop_mode, name, description, enabled, target_json, profile, prompt, max_iterations, token_budget, token_reserve, runtime_options_json, stop_policy_json, resource_json, state_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(job.id, job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, job.tokenReserve ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.createdAt, job.updatedAt); }
|
|
626
|
+
writeJob(job) { this.db.prepare('UPDATE pibo_ralph_jobs SET loop_mode = ?, name = ?, description = ?, enabled = ?, target_json = ?, profile = ?, prompt = ?, max_iterations = ?, token_budget = ?, token_reserve = ?, runtime_options_json = ?, stop_policy_json = ?, resource_json = ?, state_json = ?, updated_at = ? WHERE id = ?').run(job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, job.tokenReserve ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.updatedAt, job.id); }
|
|
528
627
|
updateJobStateLocked(id, state, updatedAt) { this.db.prepare('UPDATE pibo_ralph_jobs SET state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), updatedAt, id); }
|
|
529
|
-
createRunLocked(job, timestamp) {
|
|
628
|
+
createRunLocked(job, timestamp) {
|
|
629
|
+
const tokensUsedBefore = job.state.tokensUsed ?? 0;
|
|
630
|
+
const accounting = job.mode === 'goal' ? {
|
|
631
|
+
...(job.tokenBudget !== undefined ? { tokenBudget: job.tokenBudget, remainingTokensBefore: Math.max(0, job.tokenBudget - tokensUsedBefore) } : {}),
|
|
632
|
+
...(job.tokenReserve !== undefined ? { tokenReserve: job.tokenReserve } : {}),
|
|
633
|
+
tokensUsedBefore,
|
|
634
|
+
tokensUsed: 0,
|
|
635
|
+
overshootTokens: 0,
|
|
636
|
+
} : undefined;
|
|
637
|
+
const run = { id: job.mode === 'ralph' ? `rrun_${randomUUID()}` : `lrun_${randomUUID()}`, jobId: job.id, status: 'running', startedAt: timestamp, ...(accounting ? { accounting } : {}), ...(job.resources ? { resources: job.resources } : {}), createdAt: timestamp, updatedAt: timestamp };
|
|
638
|
+
this.db.prepare('INSERT INTO pibo_ralph_runs (id, job_id, pibo_session_id, status, reason, error, accounting_json, resource_json, started_at, completed_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(run.id, run.jobId, null, run.status, null, null, runAccountingJson(run.accounting), resourceMetadataJson(run.resources), run.startedAt ?? null, null, run.createdAt, run.updatedAt);
|
|
639
|
+
return run;
|
|
640
|
+
}
|
|
530
641
|
}
|
|
531
642
|
export function createDefaultPiboLoopStore(options = {}) { return new PiboLoopStore(options); }
|
package/dist/loops/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StringEnum, Type } from '@earendil-works/pi-ai';
|
|
2
2
|
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { goalActiveTimeSeconds, goalCanStartNextTurn, goalElapsedWallClockSeconds, goalRemainingTokens } from './accounting.js';
|
|
3
4
|
import { createDefaultPiboLoopStore } from './store.js';
|
|
4
5
|
export const PIBO_GOAL_TOOL_NAMES = ['get_goal', 'create_goal', 'update_goal'];
|
|
5
6
|
let configuredStorePath;
|
|
@@ -33,17 +34,30 @@ function positiveInteger(value, field) {
|
|
|
33
34
|
throw new Error(`${field} must be a positive integer`);
|
|
34
35
|
return value;
|
|
35
36
|
}
|
|
37
|
+
function nonNegativeInteger(value, field) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (!Number.isInteger(value) || value < 0)
|
|
41
|
+
throw new Error(`${field} must be a non-negative integer`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
36
44
|
function goalPayload(job) {
|
|
37
|
-
const tokensUsed = job.state.tokensUsed ?? 0;
|
|
38
45
|
const tokenBudget = job.tokenBudget;
|
|
39
46
|
return {
|
|
40
47
|
goalId: job.id,
|
|
41
48
|
objective: job.prompt,
|
|
42
49
|
status: effectiveGoalStatus(job),
|
|
50
|
+
budgetType: tokenBudget === undefined ? 'unbounded' : 'soft',
|
|
43
51
|
tokenBudget: tokenBudget ?? null,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
tokenReserve: job.tokenReserve ?? 0,
|
|
53
|
+
tokensUsed: job.state.tokensUsed ?? 0,
|
|
54
|
+
remainingTokens: goalRemainingTokens(job) ?? null,
|
|
55
|
+
canStartNextTurn: goalCanStartNextTurn(job),
|
|
56
|
+
activeAgentTimeSeconds: goalActiveTimeSeconds(job),
|
|
57
|
+
elapsedWallClockSeconds: goalElapsedWallClockSeconds(job),
|
|
58
|
+
goalStartedAt: job.state.goalStartedAt ?? null,
|
|
59
|
+
goalEndedAt: job.state.goalEndedAt ?? null,
|
|
60
|
+
wallClockIncludesPausedTime: true,
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
function effectiveGoalStatus(job) {
|
|
@@ -64,7 +78,7 @@ function createGetGoalTool(context, options) {
|
|
|
64
78
|
return defineTool({
|
|
65
79
|
name: 'get_goal',
|
|
66
80
|
label: 'Get Goal',
|
|
67
|
-
description: 'Get the current goal for this Pibo Session, including
|
|
81
|
+
description: 'Get the current goal for this Pibo Session, including soft-budget risk, per-turn reserve, active agent time, and wall-clock elapsed time.',
|
|
68
82
|
promptSnippet: 'Use get_goal when you need the authoritative persisted status or accounting for the current Pibo Session goal.',
|
|
69
83
|
parameters: Type.Object({}),
|
|
70
84
|
async execute() {
|
|
@@ -89,7 +103,8 @@ function createCreateGoalTool(context, options) {
|
|
|
89
103
|
promptSnippet: 'Call create_goal only when the user or system explicitly requests a persistent goal. Do not infer a goal from an ordinary task.',
|
|
90
104
|
parameters: Type.Object({
|
|
91
105
|
objective: Type.String({ description: 'Concrete objective to pursue across automatic continuations.' }),
|
|
92
|
-
token_budget: Type.Optional(Type.Number({ description: 'Optional
|
|
106
|
+
token_budget: Type.Optional(Type.Number({ description: 'Optional soft token budget. Usage arrives after each model response, so the final turn can overshoot.' })),
|
|
107
|
+
token_reserve: Type.Optional(Type.Number({ description: 'Optional non-negative minimum remaining tokens required before Pibo starts another turn.' })),
|
|
93
108
|
}),
|
|
94
109
|
async execute(_toolCallId, params) {
|
|
95
110
|
try {
|
|
@@ -98,6 +113,7 @@ function createCreateGoalTool(context, options) {
|
|
|
98
113
|
if (!objective)
|
|
99
114
|
throw new Error('objective is required');
|
|
100
115
|
const tokenBudget = positiveInteger(params.token_budget, 'token_budget');
|
|
116
|
+
const tokenReserve = nonNegativeInteger(params.token_reserve, 'token_reserve');
|
|
101
117
|
return await withStore(options, (store) => {
|
|
102
118
|
const existing = store.getLatestGoalForSession(session.piboSessionId);
|
|
103
119
|
if (existing && effectiveGoalStatus(existing) !== 'complete') {
|
|
@@ -110,6 +126,7 @@ function createCreateGoalTool(context, options) {
|
|
|
110
126
|
profile: session.profileName,
|
|
111
127
|
prompt: objective,
|
|
112
128
|
tokenBudget,
|
|
129
|
+
tokenReserve,
|
|
113
130
|
initialPiboSessionId: session.piboSessionId,
|
|
114
131
|
});
|
|
115
132
|
return toolResult({ ok: true, goal: goalPayload(job) });
|
|
@@ -147,7 +164,7 @@ function createUpdateGoalTool(context, options) {
|
|
|
147
164
|
ok: true,
|
|
148
165
|
goal: goalPayload(job),
|
|
149
166
|
...(status === 'complete' && job.tokenBudget !== undefined
|
|
150
|
-
? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported tokens consumed before the current model turn finishes` }
|
|
167
|
+
? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported tokens consumed against a soft budget before the current model turn finishes` }
|
|
151
168
|
: {}),
|
|
152
169
|
});
|
|
153
170
|
});
|