@pasko70/pibo 1.9.13 → 1.10.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/README.md +183 -183
- package/context/codex-base-prompt.md +148 -148
- package/context/compute-worker.md +23 -23
- package/context/pibo-compaction-prompt.md +100 -100
- package/context/pibo-native-tooling.md +18 -18
- package/context/pibo-system-prompt.md +77 -77
- package/dist/apps/chat/agent-profiles.js +2 -2
- package/dist/apps/chat/agent-store.js +263 -250
- package/dist/apps/chat/chat-request-normalizers.js +10 -0
- package/dist/apps/chat/data/project-service.js +168 -168
- package/dist/apps/chat/data/read-state-service.js +18 -18
- package/dist/apps/chat/data/session-query-service.js +25 -25
- package/dist/apps/chat/data/timeline-query-service.js +19 -19
- package/dist/apps/chat/loop-api.js +176 -0
- package/dist/apps/chat/static-assets.js +854 -854
- package/dist/apps/chat/web-app.js +6 -6
- package/dist/apps/chat/workflow-manual-trigger-runtime.js +149 -46
- package/dist/apps/chat/workflow-persistence.js +255 -255
- package/dist/apps/chat-ui/assets/{dist-yCYNNb5d.js → dist-BwKObYnX.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BCB6zezO.js → dist-CKtT8YGm.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CnVsqwSG.js → dist-CS7wdk0Z.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-HqTN67dc.js → dist-D-cxLQO1.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BNMu92bb.js → dist-DUlaXAk7.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CzE6k3F3.js → dist-DlATLa-U.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dq4GxJi3.js → dist-GdEM8UW1.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BZ2eTC4f.js → dist-LHRs1Nhr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D811wJeV.js → dist-Y-AA2omI.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-WyXdYl-w.js → dist-nOLTkZrJ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BHa-kcGl.js → dist-wE9nop9V.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-DwHJfmiF.js → index-8W_yMHQI.js} +6 -6
- package/dist/apps/chat-ui/index.html +17 -17
- package/dist/apps/chat-ui/manifest.webmanifest +25 -25
- package/dist/apps/chat-ui/sw.js +47 -47
- package/dist/apps/chat-vscode-web/index.html +12 -12
- package/dist/apps/cli-ui/cliSessionsCommand.js +23 -23
- package/dist/apps/context-files-ui/index.html +11 -11
- package/dist/cli.js +61 -44
- package/dist/compute/cli.js +54 -54
- package/dist/core/routed-session.js +30 -1
- package/dist/core/runtime.js +6 -1
- package/dist/core/session-router.js +4 -2
- package/dist/cron/cli.js +15 -15
- package/dist/cron/store.js +49 -49
- package/dist/data/cli.js +23 -23
- package/dist/data/event-log.js +23 -23
- package/dist/data/message-store.js +27 -27
- package/dist/data/navigation-store.js +9 -9
- package/dist/data/observation-store.js +4 -4
- package/dist/data/payload-store.js +17 -17
- package/dist/data/schema.js +433 -433
- package/dist/data/session-store.js +4 -4
- package/dist/data/telemetry-queries.js +54 -54
- package/dist/data/telemetry.js +197 -197
- package/dist/debug/events.js +12 -12
- package/dist/debug/failures.js +6 -6
- package/dist/debug/index.js +227 -227
- package/dist/debug/messages.js +6 -6
- package/dist/debug/pty.js +124 -124
- package/dist/debug/session.js +29 -29
- package/dist/debug/tools.js +5 -5
- package/dist/debug/web-snapshot-browser-scripts.js +294 -294
- package/dist/debug/web-streaming-browser-library.js +925 -925
- package/dist/debug/web-streaming-browser-scripts.js +232 -232
- package/dist/debug/web-streaming-provider-telemetry.js +4 -4
- package/dist/debug/web.js +93 -93
- package/dist/gateway/cli.js +19 -19
- package/dist/gateway/server.js +4 -2
- package/dist/gateway/web.js +2 -2
- package/dist/loops/channel.js +8 -0
- package/dist/loops/cli.js +208 -0
- package/dist/loops/plugin.js +16 -0
- package/dist/loops/prompts.js +83 -0
- package/dist/loops/service.js +357 -0
- package/dist/loops/stopping.js +170 -0
- package/dist/loops/store.js +531 -0
- package/dist/loops/templates.js +232 -0
- package/dist/loops/tools.js +167 -0
- package/dist/loops/types.js +1 -0
- package/dist/mcp/config-command.js +53 -53
- package/dist/mcp/index.js +21 -21
- package/dist/mcp/registry.js +11 -11
- package/dist/pi-packages/cli.js +11 -11
- package/dist/plugins/builtin.js +8 -0
- package/dist/plugins/context-files-store.js +110 -110
- package/dist/plugins/context-files.js +4 -4
- package/dist/plugins/registry.js +23 -12
- package/dist/ralph/cli.js +18 -18
- package/dist/ralph/templates.js +140 -140
- package/dist/reliability/store.js +226 -226
- package/dist/sessions/pibo-data-store.js +16 -16
- package/dist/sessions/sqlite-store.js +53 -53
- package/dist/setup/cli.js +58 -58
- package/dist/tools/agent-browser-wrapper.js +80 -80
- package/dist/tools/browser-use-cdp.js +12 -12
- package/dist/tools/browser-use-wrapper.js +762 -762
- package/dist/tools/guides.js +589 -538
- package/dist/tools/index.js +122 -99
- package/dist/tools/registry.js +21 -3
- package/dist/tools/runtime/node-worker-source.js +205 -205
- package/dist/tools/runtime/python-worker-source.js +177 -177
- package/dist/vscode/cli.js +9 -9
- package/dist/web-annotations/cdp.js +900 -900
- package/dist/web-annotations/store.js +96 -96
- package/docs/README.md +23 -23
- package/docs/ops/install-developer-host.md +112 -112
- package/docs/ops/install-user-host.md +96 -96
- package/docs/ops/upgrade-user-to-developer-host.md +69 -69
- package/docs/ops/vscode-extension-release.md +160 -160
- package/package.json +98 -95
- package/skills/builtin/graphify/SKILL.md +52 -52
- package/skills/builtin/loop/SKILL.md +69 -0
- package/skills/builtin/pi-agent-harness/SKILL.md +319 -319
- package/skills/builtin/pi-agent-harness/agents/openai.yaml +4 -4
- package/skills/builtin/pibo-docker-system/SKILL.md +170 -170
- package/skills/builtin/pibo-spec-writing/SKILL.md +330 -330
- package/skills/builtin/prd/SKILL.md +143 -143
- package/skills/builtin/ralph-loop/SKILL.md +361 -359
- package/skills/builtin/ralph-prd-json/SKILL.md +123 -123
- package/skills/builtin/skill-creator/LICENSE.txt +201 -201
- package/skills/builtin/skill-creator/SKILL.md +513 -513
- package/skills/builtin/skill-creator/agents/analyzer.md +274 -274
- package/skills/builtin/skill-creator/agents/comparator.md +202 -202
- package/skills/builtin/skill-creator/agents/grader.md +223 -223
- package/skills/builtin/skill-creator/assets/eval_review.html +146 -146
- package/skills/builtin/skill-creator/eval-viewer/generate_review.py +471 -471
- package/skills/builtin/skill-creator/eval-viewer/viewer.html +1325 -1325
- package/skills/builtin/skill-creator/references/schemas.md +430 -430
- package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +401 -401
- package/skills/builtin/skill-creator/scripts/generate_report.py +326 -326
- package/skills/builtin/skill-creator/scripts/improve_description.py +247 -247
- package/skills/builtin/skill-creator/scripts/package_skill.py +136 -136
- package/skills/builtin/skill-creator/scripts/quick_validate.py +102 -102
- package/skills/builtin/skill-creator/scripts/run_eval.py +310 -310
- package/skills/builtin/skill-creator/scripts/run_loop.py +328 -328
- package/skills/builtin/skill-creator/scripts/utils.py +47 -47
- package/skills/builtin/web-annotations/SKILL.md +93 -93
- package/src/mcp/LICENSE.mcp-cli +21 -21
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.9.13.vsix +0 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { piboHomePath } from '../core/pibo-home.js';
|
|
6
|
+
import { isPiboThinkingLevel } from '../core/thinking.js';
|
|
7
|
+
function nowIso(now = new Date()) { return now.toISOString(); }
|
|
8
|
+
function parseJson(json) { return JSON.parse(json); }
|
|
9
|
+
function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
|
|
10
|
+
function normalizeLoopMode(value, fallback = 'goal') { return value === 'ralph' ? 'ralph' : value === 'goal' ? 'goal' : fallback; }
|
|
11
|
+
function isJsonObject(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
|
|
12
|
+
const cleanupStates = new Set(['none', 'active', 'released', 'retained', 'dirty']);
|
|
13
|
+
function normalizeLoopTarget(target) {
|
|
14
|
+
if (target.kind === 'room')
|
|
15
|
+
return { kind: 'room', roomId: String(target.roomId ?? '').trim() };
|
|
16
|
+
return { kind: 'default-chat' };
|
|
17
|
+
}
|
|
18
|
+
function parseLoopTarget(json) { return normalizeLoopTarget(parseJson(json)); }
|
|
19
|
+
function targetJson(target) { return JSON.stringify(normalizeLoopTarget(target)); }
|
|
20
|
+
function optionalString(value, field) {
|
|
21
|
+
if (value === undefined || value === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof value !== 'string')
|
|
24
|
+
throw new Error(`${field} must be a string`);
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed || undefined;
|
|
27
|
+
}
|
|
28
|
+
function normalizeBrowserLeaseIds(value) {
|
|
29
|
+
if (value === undefined || value === null)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!Array.isArray(value))
|
|
32
|
+
throw new Error('resources.browserLeaseIds must be an array');
|
|
33
|
+
const ids = [...new Set(value.map((item) => optionalString(item, 'resources.browserLeaseIds[]')).filter((item) => !!item))];
|
|
34
|
+
return ids.length ? ids : undefined;
|
|
35
|
+
}
|
|
36
|
+
export function normalizeLoopResourceMetadata(value) {
|
|
37
|
+
if (value === undefined || value === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
40
|
+
throw new Error('resources must be an object');
|
|
41
|
+
const cleanupState = optionalString(value.cleanupState, 'resources.cleanupState');
|
|
42
|
+
if (cleanupState && !cleanupStates.has(cleanupState))
|
|
43
|
+
throw new Error('resources.cleanupState must be one of none, active, released, retained, dirty');
|
|
44
|
+
const resources = {};
|
|
45
|
+
const workerId = optionalString(value.workerId, 'resources.workerId');
|
|
46
|
+
const browserLeaseIds = normalizeBrowserLeaseIds(value.browserLeaseIds);
|
|
47
|
+
const retainedUntil = optionalString(value.retainedUntil, 'resources.retainedUntil');
|
|
48
|
+
const dirtyReason = optionalString(value.dirtyReason, 'resources.dirtyReason');
|
|
49
|
+
const updatedAt = optionalString(value.updatedAt, 'resources.updatedAt');
|
|
50
|
+
if (workerId)
|
|
51
|
+
resources.workerId = workerId;
|
|
52
|
+
if (browserLeaseIds)
|
|
53
|
+
resources.browserLeaseIds = browserLeaseIds;
|
|
54
|
+
if (cleanupState)
|
|
55
|
+
resources.cleanupState = cleanupState;
|
|
56
|
+
if (retainedUntil)
|
|
57
|
+
resources.retainedUntil = retainedUntil;
|
|
58
|
+
if (dirtyReason)
|
|
59
|
+
resources.dirtyReason = dirtyReason;
|
|
60
|
+
if (updatedAt)
|
|
61
|
+
resources.updatedAt = updatedAt;
|
|
62
|
+
return Object.keys(resources).length ? resources : undefined;
|
|
63
|
+
}
|
|
64
|
+
function parseResourceMetadata(json) {
|
|
65
|
+
if (!json)
|
|
66
|
+
return undefined;
|
|
67
|
+
try {
|
|
68
|
+
return normalizeLoopResourceMetadata(JSON.parse(json));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function resourceMetadataJson(resources) {
|
|
75
|
+
const normalized = normalizeLoopResourceMetadata(resources);
|
|
76
|
+
return normalized ? JSON.stringify(normalized) : null;
|
|
77
|
+
}
|
|
78
|
+
function jobFromRow(row) {
|
|
79
|
+
const resources = parseResourceMetadata(row.resource_json);
|
|
80
|
+
return { id: row.id, mode: normalizeLoopMode(row.loop_mode, 'ralph'), name: row.name, description: row.description ?? undefined, enabled: row.enabled === 1, target: parseLoopTarget(row.target_json), profile: row.profile, prompt: row.prompt, maxIterations: row.max_iterations ?? undefined, tokenBudget: row.token_budget ?? undefined, stopPolicy: parseStopPolicy(row.stop_policy_json), ...parseRuntimeOptions(row.runtime_options_json), ...(resources ? { resources } : {}), state: parseJson(row.state_json), createdAt: row.created_at, updatedAt: row.updated_at };
|
|
81
|
+
}
|
|
82
|
+
function runFromRow(row) {
|
|
83
|
+
const resources = parseResourceMetadata(row.resource_json);
|
|
84
|
+
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, ...(resources ? { resources } : {}), createdAt: row.created_at, updatedAt: row.updated_at };
|
|
85
|
+
}
|
|
86
|
+
function mergeResourceMetadata(jobResources, runResources) {
|
|
87
|
+
if (!jobResources && !runResources)
|
|
88
|
+
return undefined;
|
|
89
|
+
const browserLeaseIds = [...new Set([...(jobResources?.browserLeaseIds ?? []), ...(runResources?.browserLeaseIds ?? [])])];
|
|
90
|
+
return { ...(jobResources ?? {}), ...(runResources ?? {}), ...(browserLeaseIds.length ? { browserLeaseIds } : {}) };
|
|
91
|
+
}
|
|
92
|
+
function factFromRow(row) {
|
|
93
|
+
return { id: row.id, jobId: row.job_id, runId: row.run_id ?? undefined, piboSessionId: row.pibo_session_id ?? undefined, type: row.type, source: row.source, payload: parseJson(row.payload_json), createdAt: row.created_at };
|
|
94
|
+
}
|
|
95
|
+
function normalizeMaxIterations(value) {
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!Number.isInteger(value) || value < 1)
|
|
99
|
+
throw new Error('maxIterations must be a positive integer');
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function normalizeTokenBudget(value) {
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!Number.isInteger(value) || value < 1)
|
|
106
|
+
throw new Error('tokenBudget must be a positive integer');
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function goalStatus(job) {
|
|
110
|
+
if (job.mode !== 'goal')
|
|
111
|
+
return undefined;
|
|
112
|
+
return job.state.goalStatus ?? (job.enabled ? 'active' : 'paused');
|
|
113
|
+
}
|
|
114
|
+
function normalizeModelOverride(value) {
|
|
115
|
+
if (value === undefined || value === null)
|
|
116
|
+
return undefined;
|
|
117
|
+
const provider = value.provider.trim();
|
|
118
|
+
const id = value.id.trim();
|
|
119
|
+
if (!provider || !id)
|
|
120
|
+
throw new Error('modelOverride must include provider and id');
|
|
121
|
+
return { provider, id };
|
|
122
|
+
}
|
|
123
|
+
function normalizeThinkingLevel(value) {
|
|
124
|
+
if (value === undefined || value === null)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (!isPiboThinkingLevel(value))
|
|
127
|
+
throw new Error('thinkingLevel must be one of off, minimal, low, medium, high, xhigh, max');
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
function normalizeFastMode(value) {
|
|
131
|
+
if (value === undefined || value === null)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (typeof value !== 'boolean')
|
|
134
|
+
throw new Error('fastMode must be a boolean');
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function normalizeRuntimeOptions(input) {
|
|
138
|
+
return { modelOverride: normalizeModelOverride(input.modelOverride), thinkingLevel: normalizeThinkingLevel(input.thinkingLevel), fastMode: normalizeFastMode(input.fastMode) };
|
|
139
|
+
}
|
|
140
|
+
function parseRuntimeOptions(json) {
|
|
141
|
+
if (!json)
|
|
142
|
+
return {};
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(json);
|
|
145
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
146
|
+
return {};
|
|
147
|
+
return normalizeRuntimeOptions(parsed);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function runtimeOptionsJson(job) {
|
|
154
|
+
const options = normalizeRuntimeOptions(job);
|
|
155
|
+
const json = {};
|
|
156
|
+
if (options.modelOverride)
|
|
157
|
+
json.modelOverride = options.modelOverride;
|
|
158
|
+
if (options.thinkingLevel)
|
|
159
|
+
json.thinkingLevel = options.thinkingLevel;
|
|
160
|
+
if (options.fastMode !== undefined)
|
|
161
|
+
json.fastMode = options.fastMode;
|
|
162
|
+
return Object.keys(json).length ? JSON.stringify(json) : null;
|
|
163
|
+
}
|
|
164
|
+
function hasOwn(object, key) { return Object.prototype.hasOwnProperty.call(object, key); }
|
|
165
|
+
export function normalizeLoopStopPolicy(value) {
|
|
166
|
+
if (value === undefined || value === null)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
169
|
+
throw new Error('stopPolicy must be an object');
|
|
170
|
+
if (value.mode !== 'any' && value.mode !== 'all')
|
|
171
|
+
throw new Error('stopPolicy.mode must be any or all');
|
|
172
|
+
if (!Array.isArray(value.conditions))
|
|
173
|
+
throw new Error('stopPolicy.conditions must be an array');
|
|
174
|
+
const ids = new Set();
|
|
175
|
+
return { mode: value.mode, conditions: value.conditions.map((condition, index) => {
|
|
176
|
+
if (!condition || typeof condition !== 'object' || Array.isArray(condition))
|
|
177
|
+
throw new Error(`stopPolicy.conditions[${index}] must be an object`);
|
|
178
|
+
const id = typeof condition.id === 'string' && condition.id.trim() ? condition.id.trim() : `condition-${index + 1}`;
|
|
179
|
+
const type = typeof condition.type === 'string' ? condition.type.trim() : '';
|
|
180
|
+
if (!type)
|
|
181
|
+
throw new Error(`stopPolicy.conditions[${index}].type is required`);
|
|
182
|
+
if (ids.has(id))
|
|
183
|
+
throw new Error(`Duplicate Loop stop condition id: ${id}`);
|
|
184
|
+
ids.add(id);
|
|
185
|
+
if (condition.options !== undefined && !isJsonObject(condition.options))
|
|
186
|
+
throw new Error(`stopPolicy.conditions[${index}].options must be an object`);
|
|
187
|
+
if (condition.enabled !== undefined && typeof condition.enabled !== 'boolean')
|
|
188
|
+
throw new Error(`stopPolicy.conditions[${index}].enabled must be a boolean`);
|
|
189
|
+
if (condition.failClosed !== undefined && typeof condition.failClosed !== 'boolean')
|
|
190
|
+
throw new Error(`stopPolicy.conditions[${index}].failClosed must be a boolean`);
|
|
191
|
+
if (condition.timeoutMs !== undefined && (!Number.isInteger(condition.timeoutMs) || condition.timeoutMs < 1))
|
|
192
|
+
throw new Error(`stopPolicy.conditions[${index}].timeoutMs must be a positive integer`);
|
|
193
|
+
return { id, type, ...(condition.enabled !== undefined ? { enabled: condition.enabled } : {}), ...(condition.options ? { options: condition.options } : {}), ...(condition.failClosed !== undefined ? { failClosed: condition.failClosed } : {}), ...(condition.timeoutMs !== undefined ? { timeoutMs: condition.timeoutMs } : {}) };
|
|
194
|
+
}) };
|
|
195
|
+
}
|
|
196
|
+
function parseStopPolicy(json) {
|
|
197
|
+
if (!json)
|
|
198
|
+
return undefined;
|
|
199
|
+
try {
|
|
200
|
+
return normalizeLoopStopPolicy(JSON.parse(json));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function stopPolicyJson(policy) { return policy ? JSON.stringify(normalizeLoopStopPolicy(policy)) : null; }
|
|
207
|
+
function validateJobInput(input) {
|
|
208
|
+
if (input.mode !== undefined && input.mode !== 'goal' && input.mode !== 'ralph')
|
|
209
|
+
throw new Error('mode must be goal or ralph');
|
|
210
|
+
if (!input.profile.trim())
|
|
211
|
+
throw new Error('profile is required');
|
|
212
|
+
if (!input.prompt.trim())
|
|
213
|
+
throw new Error('prompt is required');
|
|
214
|
+
if (input.target.kind === 'room' && !input.target.roomId.trim())
|
|
215
|
+
throw new Error('target.roomId is required');
|
|
216
|
+
if (input.mode === 'ralph' && input.tokenBudget !== undefined)
|
|
217
|
+
throw new Error('tokenBudget is only available for goal mode');
|
|
218
|
+
normalizeMaxIterations(input.maxIterations);
|
|
219
|
+
normalizeTokenBudget(input.tokenBudget);
|
|
220
|
+
normalizeRuntimeOptions(input);
|
|
221
|
+
normalizeLoopStopPolicy(input.stopPolicy);
|
|
222
|
+
normalizeLoopResourceMetadata(input.resources);
|
|
223
|
+
}
|
|
224
|
+
export class PiboLoopStore {
|
|
225
|
+
db;
|
|
226
|
+
constructor(options = {}) {
|
|
227
|
+
const dbPath = options.path ?? piboHomePath('pibo-ralph.sqlite');
|
|
228
|
+
const resolved = dbPath === ':memory:' ? dbPath : resolve(dbPath);
|
|
229
|
+
if (resolved !== ':memory:')
|
|
230
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
231
|
+
this.db = new DatabaseSync(resolved);
|
|
232
|
+
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
233
|
+
this.db.exec('PRAGMA foreign_keys = ON');
|
|
234
|
+
if (resolved !== ':memory:')
|
|
235
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
236
|
+
this.applySchema();
|
|
237
|
+
}
|
|
238
|
+
close() { this.db.close(); }
|
|
239
|
+
createJob(input, now = new Date()) {
|
|
240
|
+
const target = normalizeLoopTarget(input.target);
|
|
241
|
+
validateJobInput({ ...input, target });
|
|
242
|
+
const timestamp = nowIso(now);
|
|
243
|
+
const runtimeOptions = normalizeRuntimeOptions(input);
|
|
244
|
+
const resources = normalizeLoopResourceMetadata(input.resources);
|
|
245
|
+
const mode = normalizeLoopMode(input.mode);
|
|
246
|
+
const enabled = input.enabled === true;
|
|
247
|
+
const state = {
|
|
248
|
+
completedIterations: 0,
|
|
249
|
+
...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, timeUsedSeconds: 0 } : {}),
|
|
250
|
+
...(input.initialPiboSessionId?.trim() ? { lastPiboSessionId: input.initialPiboSessionId.trim() } : {}),
|
|
251
|
+
};
|
|
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 };
|
|
253
|
+
this.insertJob(job);
|
|
254
|
+
return this.getJob(job.id);
|
|
255
|
+
}
|
|
256
|
+
getJob(id) { const row = this.db.prepare('SELECT * FROM pibo_ralph_jobs WHERE id = ?').get(id); return row ? jobFromRow(row) : undefined; }
|
|
257
|
+
getLatestGoalForSession(piboSessionId) {
|
|
258
|
+
const row = this.db.prepare("SELECT * FROM pibo_ralph_jobs WHERE loop_mode = 'goal' AND json_extract(state_json, '$.lastPiboSessionId') = ? ORDER BY created_at DESC LIMIT 1").get(piboSessionId);
|
|
259
|
+
return row ? jobFromRow(row) : undefined;
|
|
260
|
+
}
|
|
261
|
+
updateGoalStatus(id, status, now = new Date()) {
|
|
262
|
+
const job = this.getJob(id);
|
|
263
|
+
if (!job || job.mode !== 'goal')
|
|
264
|
+
return undefined;
|
|
265
|
+
const timestamp = nowIso(now);
|
|
266
|
+
const state = { ...job.state, goalStatus: status, runningAt: job.state.runningAt };
|
|
267
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, id);
|
|
268
|
+
return this.getJob(id);
|
|
269
|
+
}
|
|
270
|
+
recordGoalProgress(id, input, now = new Date()) {
|
|
271
|
+
const job = this.getJob(id);
|
|
272
|
+
if (!job || job.mode !== 'goal')
|
|
273
|
+
return job;
|
|
274
|
+
const tokens = Math.max(0, Math.floor(input.tokens ?? 0));
|
|
275
|
+
const timeUsedSeconds = Math.max(0, Math.floor(input.timeUsedSeconds ?? 0));
|
|
276
|
+
const nextTokens = (job.state.tokensUsed ?? 0) + tokens;
|
|
277
|
+
const currentStatus = goalStatus(job) ?? 'active';
|
|
278
|
+
const budgetLimited = currentStatus === 'active' && job.tokenBudget !== undefined && nextTokens >= job.tokenBudget;
|
|
279
|
+
const state = {
|
|
280
|
+
...job.state,
|
|
281
|
+
tokensUsed: nextTokens,
|
|
282
|
+
timeUsedSeconds: (job.state.timeUsedSeconds ?? 0) + timeUsedSeconds,
|
|
283
|
+
goalStatus: budgetLimited ? 'budget_limited' : currentStatus,
|
|
284
|
+
};
|
|
285
|
+
const timestamp = nowIso(now);
|
|
286
|
+
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
|
+
return this.getJob(id);
|
|
288
|
+
}
|
|
289
|
+
listJobs(input = {}) {
|
|
290
|
+
const clauses = [];
|
|
291
|
+
const values = [];
|
|
292
|
+
if (!input.includeDisabled)
|
|
293
|
+
clauses.push('enabled = 1');
|
|
294
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
295
|
+
return this.db.prepare(`SELECT * FROM pibo_ralph_jobs ${where} ORDER BY updated_at DESC, id ASC`).all(...values).map(jobFromRow);
|
|
296
|
+
}
|
|
297
|
+
updateJob(id, patch, now = new Date()) {
|
|
298
|
+
const existing = this.getJob(id);
|
|
299
|
+
if (!existing)
|
|
300
|
+
return undefined;
|
|
301
|
+
const runtimeOptions = normalizeRuntimeOptions({
|
|
302
|
+
modelOverride: hasOwn(patch, 'modelOverride') ? patch.modelOverride : existing.modelOverride,
|
|
303
|
+
thinkingLevel: hasOwn(patch, 'thinkingLevel') ? patch.thinkingLevel : existing.thinkingLevel,
|
|
304
|
+
fastMode: hasOwn(patch, 'fastMode') ? patch.fastMode : existing.fastMode,
|
|
305
|
+
});
|
|
306
|
+
const stopPolicy = hasOwn(patch, 'stopPolicy') ? normalizeLoopStopPolicy(patch.stopPolicy ?? undefined) : existing.stopPolicy;
|
|
307
|
+
const target = patch.target ? normalizeLoopTarget(patch.target) : existing.target;
|
|
308
|
+
const mode = patch.mode !== undefined ? normalizeLoopMode(patch.mode) : existing.mode;
|
|
309
|
+
const enabled = patch.enabled ?? existing.enabled;
|
|
310
|
+
let state = mode === existing.mode
|
|
311
|
+
? { ...existing.state }
|
|
312
|
+
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, timeUsedSeconds: 0 } : {}) };
|
|
313
|
+
if (mode === 'goal' && patch.enabled !== undefined) {
|
|
314
|
+
const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
|
|
315
|
+
if (patch.enabled) {
|
|
316
|
+
if (currentGoalStatus === 'complete')
|
|
317
|
+
throw new Error('Completed goals cannot be restarted; create a new goal');
|
|
318
|
+
const nextBudget = hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget;
|
|
319
|
+
if (currentGoalStatus === 'budget_limited' && nextBudget !== undefined && (existing.state.tokensUsed ?? 0) >= nextBudget)
|
|
320
|
+
throw new Error('Increase or clear the token budget before resuming this goal');
|
|
321
|
+
state.goalStatus = 'active';
|
|
322
|
+
state.stopRequestedAt = undefined;
|
|
323
|
+
state.cancelRequestedAt = undefined;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
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) };
|
|
330
|
+
validateJobInput(next);
|
|
331
|
+
this.writeJob(next);
|
|
332
|
+
return this.getJob(id);
|
|
333
|
+
}
|
|
334
|
+
updateJobResources(id, resources, now = new Date()) {
|
|
335
|
+
const existing = this.getJob(id);
|
|
336
|
+
if (!existing)
|
|
337
|
+
return undefined;
|
|
338
|
+
const timestamp = nowIso(now);
|
|
339
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET resource_json = ?, updated_at = ? WHERE id = ?').run(resourceMetadataJson(resources), timestamp, id);
|
|
340
|
+
return this.getJob(id);
|
|
341
|
+
}
|
|
342
|
+
updateRunResources(input, now = new Date()) {
|
|
343
|
+
const clauses = ['id = ?'];
|
|
344
|
+
const values = [input.runId];
|
|
345
|
+
if (input.jobId) {
|
|
346
|
+
clauses.push('job_id = ?');
|
|
347
|
+
values.push(input.jobId);
|
|
348
|
+
}
|
|
349
|
+
const timestamp = nowIso(now);
|
|
350
|
+
const result = this.db.prepare(`UPDATE pibo_ralph_runs SET resource_json = ?, updated_at = ? WHERE ${clauses.join(' AND ')}`).run(resourceMetadataJson(input.resources), timestamp, ...values);
|
|
351
|
+
if (Number(result.changes ?? 0) === 0)
|
|
352
|
+
return undefined;
|
|
353
|
+
const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ?').get(input.runId);
|
|
354
|
+
return row ? runFromRow(row) : undefined;
|
|
355
|
+
}
|
|
356
|
+
removeJob(id) { const result = this.db.prepare('DELETE FROM pibo_ralph_jobs WHERE id = ?').run(id); return Number(result.changes ?? 0) > 0; }
|
|
357
|
+
listRuns(input = {}) {
|
|
358
|
+
const clauses = [];
|
|
359
|
+
const values = [];
|
|
360
|
+
if (input.jobId) {
|
|
361
|
+
clauses.push('job_id = ?');
|
|
362
|
+
values.push(input.jobId);
|
|
363
|
+
}
|
|
364
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
365
|
+
return this.db.prepare(`SELECT * FROM pibo_ralph_runs ${where} ORDER BY created_at DESC LIMIT ?`).all(...values, Math.max(1, Math.min(input.limit ?? 100, 500))).map(runFromRow);
|
|
366
|
+
}
|
|
367
|
+
reserveRun(id, now = new Date()) { this.updateJob(id, { enabled: true }, now); return this.reserveJob(id, now); }
|
|
368
|
+
reserveDueRuns(limit, now = new Date()) {
|
|
369
|
+
const rows = this.db.prepare('SELECT * FROM pibo_ralph_jobs WHERE enabled = 1 ORDER BY updated_at ASC').all();
|
|
370
|
+
const result = [];
|
|
371
|
+
for (const row of rows) {
|
|
372
|
+
if (result.length >= limit)
|
|
373
|
+
break;
|
|
374
|
+
const reserved = this.reserveJob(row.id, now);
|
|
375
|
+
if (reserved)
|
|
376
|
+
result.push(reserved);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
attachRunSession(jobId, runId, piboSessionId, now = new Date()) {
|
|
381
|
+
const timestamp = nowIso(now);
|
|
382
|
+
const job = this.getJob(jobId);
|
|
383
|
+
if (!job)
|
|
384
|
+
return;
|
|
385
|
+
this.db.prepare('UPDATE pibo_ralph_runs SET pibo_session_id = ?, updated_at = ? WHERE id = ?').run(piboSessionId, timestamp, runId);
|
|
386
|
+
this.updateJobStateLocked(jobId, { ...job.state, lastPiboSessionId: piboSessionId }, timestamp);
|
|
387
|
+
}
|
|
388
|
+
requestStop(id, now = new Date()) {
|
|
389
|
+
const job = this.getJob(id);
|
|
390
|
+
if (!job)
|
|
391
|
+
return undefined;
|
|
392
|
+
const state = { ...job.state, ...(job.mode === 'goal' && goalStatus(job) === 'active' ? { goalStatus: 'paused' } : {}), stopRequestedAt: nowIso(now) };
|
|
393
|
+
this.writeJob({ ...job, enabled: false, state, updatedAt: nowIso(now) });
|
|
394
|
+
return this.getJob(id);
|
|
395
|
+
}
|
|
396
|
+
requestCancel(id, now = new Date()) {
|
|
397
|
+
const job = this.getJob(id);
|
|
398
|
+
if (!job)
|
|
399
|
+
return undefined;
|
|
400
|
+
const state = { ...job.state, ...(job.mode === 'goal' && goalStatus(job) === 'active' ? { goalStatus: 'paused' } : {}), stopRequestedAt: nowIso(now), cancelRequestedAt: nowIso(now) };
|
|
401
|
+
this.writeJob({ ...job, enabled: false, state, updatedAt: nowIso(now) });
|
|
402
|
+
return this.getJob(id);
|
|
403
|
+
}
|
|
404
|
+
applyStopEvaluation(input, now = new Date()) {
|
|
405
|
+
const job = this.getJob(input.jobId);
|
|
406
|
+
if (!job)
|
|
407
|
+
return;
|
|
408
|
+
const timestamp = nowIso(now);
|
|
409
|
+
const state = { ...job.state, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.evaluation };
|
|
410
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(input.disable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
|
|
411
|
+
}
|
|
412
|
+
completeRun(input, now = new Date()) {
|
|
413
|
+
const timestamp = nowIso(now);
|
|
414
|
+
const job = this.getJob(input.jobId);
|
|
415
|
+
if (!job)
|
|
416
|
+
return;
|
|
417
|
+
const completedIterations = (job.state.completedIterations ?? 0) + 1;
|
|
418
|
+
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
419
|
+
const terminalGoalStatus = job.mode === 'goal' && ['complete', 'blocked', 'budget_limited'].includes(goalStatus(job) ?? '');
|
|
420
|
+
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 };
|
|
422
|
+
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
|
+
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
|
+
}
|
|
425
|
+
appendRunFact(input) {
|
|
426
|
+
if (!input.jobId.trim())
|
|
427
|
+
throw new Error('fact jobId is required');
|
|
428
|
+
if (!input.type.trim())
|
|
429
|
+
throw new Error('fact type is required');
|
|
430
|
+
if (!isJsonObject(input.payload))
|
|
431
|
+
throw new Error('fact payload must be an object');
|
|
432
|
+
const payloadJson = JSON.stringify(input.payload);
|
|
433
|
+
if (payloadJson.length > 16_384)
|
|
434
|
+
throw new Error('fact payload is too large');
|
|
435
|
+
const fact = { id: input.id ?? `rfact_${randomUUID()}`, jobId: input.jobId, runId: input.runId, piboSessionId: input.piboSessionId, type: input.type.trim(), source: input.source, payload: input.payload, createdAt: input.createdAt ?? nowIso() };
|
|
436
|
+
this.db.prepare('INSERT INTO pibo_ralph_run_facts (id, job_id, run_id, pibo_session_id, type, source, payload_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(fact.id, fact.jobId, fact.runId ?? null, fact.piboSessionId ?? null, fact.type, fact.source, payloadJson, fact.createdAt);
|
|
437
|
+
return fact;
|
|
438
|
+
}
|
|
439
|
+
listRunFacts(input = {}) {
|
|
440
|
+
const clauses = [];
|
|
441
|
+
const values = [];
|
|
442
|
+
if (input.jobId) {
|
|
443
|
+
clauses.push('job_id = ?');
|
|
444
|
+
values.push(input.jobId);
|
|
445
|
+
}
|
|
446
|
+
if (input.runId) {
|
|
447
|
+
clauses.push('run_id = ?');
|
|
448
|
+
values.push(input.runId);
|
|
449
|
+
}
|
|
450
|
+
if (input.type) {
|
|
451
|
+
clauses.push('type = ?');
|
|
452
|
+
values.push(input.type);
|
|
453
|
+
}
|
|
454
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
455
|
+
return this.db.prepare(`SELECT * FROM pibo_ralph_run_facts ${where} ORDER BY created_at DESC LIMIT ?`).all(...values, Math.max(1, Math.min(input.limit ?? 100, 500))).map(factFromRow);
|
|
456
|
+
}
|
|
457
|
+
createFactReader(job) {
|
|
458
|
+
return { list: (input = {}) => this.listRunFacts({ jobId: job.id, type: input.type, runId: input.runId, limit: input.limit }), count: (input = {}) => this.listRunFacts({ jobId: job.id, type: input.type, runId: input.runId, limit: 500 }).length };
|
|
459
|
+
}
|
|
460
|
+
recoverInterruptedRuns(cutoff = new Date(Date.now() - 5 * 60_000)) { const cutoffIso = nowIso(cutoff); const rows = this.db.prepare("SELECT * FROM pibo_ralph_jobs WHERE json_extract(state_json, '$.runningAt') IS NOT NULL").all(); let recovered = 0; for (const row of rows) {
|
|
461
|
+
const job = jobFromRow(row);
|
|
462
|
+
if (!job.state.runningAt || job.state.runningAt > cutoffIso)
|
|
463
|
+
continue;
|
|
464
|
+
if (job.state.lastRunId) {
|
|
465
|
+
this.completeRun({ jobId: job.id, runId: job.state.lastRunId, status: 'error', error: 'Loop run was interrupted by gateway restart', reason: 'interrupted' });
|
|
466
|
+
this.markInterruptedRunResourcesDirty(job, job.state.lastRunId);
|
|
467
|
+
}
|
|
468
|
+
recovered += 1;
|
|
469
|
+
} return recovered; }
|
|
470
|
+
status() { const jobs = this.listJobs({ includeDisabled: false }); return { jobs: jobs.length, running: jobs.filter((job) => job.state.runningAt).length }; }
|
|
471
|
+
markInterruptedRunResourcesDirty(job, runId) {
|
|
472
|
+
const runRow = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, job.id);
|
|
473
|
+
const resources = mergeResourceMetadata(job.resources, runRow ? runFromRow(runRow).resources : undefined);
|
|
474
|
+
if (!resources || (!resources.workerId && (resources.browserLeaseIds ?? []).length === 0))
|
|
475
|
+
return;
|
|
476
|
+
const nextResources = { ...resources, cleanupState: 'dirty', dirtyReason: 'Loop run was interrupted before browser resource cleanup could be confirmed', updatedAt: nowIso() };
|
|
477
|
+
this.updateRunResources({ jobId: job.id, runId, resources: nextResources });
|
|
478
|
+
this.updateJobResources(job.id, nextResources);
|
|
479
|
+
}
|
|
480
|
+
reserveJob(id, now = new Date()) {
|
|
481
|
+
const timestamp = nowIso(now);
|
|
482
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
483
|
+
try {
|
|
484
|
+
const job = this.getJob(id);
|
|
485
|
+
if (!job || !job.enabled || job.state.runningAt) {
|
|
486
|
+
this.db.exec('COMMIT');
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
const run = this.createRunLocked(job, timestamp);
|
|
490
|
+
const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id };
|
|
491
|
+
this.updateJobStateLocked(job.id, state, timestamp);
|
|
492
|
+
this.db.exec('COMMIT');
|
|
493
|
+
return { job: { ...job, state, updatedAt: timestamp }, run };
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
this.db.exec('ROLLBACK');
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
applySchema() {
|
|
501
|
+
this.createFreshSchema();
|
|
502
|
+
this.ensureJobColumn('loop_mode', "TEXT NOT NULL DEFAULT 'ralph'");
|
|
503
|
+
this.ensureJobColumn('max_iterations', 'INTEGER');
|
|
504
|
+
this.ensureJobColumn('token_budget', 'INTEGER');
|
|
505
|
+
this.ensureJobColumn('runtime_options_json', 'TEXT');
|
|
506
|
+
this.ensureJobColumn('stop_policy_json', 'TEXT');
|
|
507
|
+
this.ensureJobColumn('resource_json', 'TEXT');
|
|
508
|
+
this.ensureRunColumn('resource_json', 'TEXT');
|
|
509
|
+
}
|
|
510
|
+
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);`);
|
|
512
|
+
}
|
|
513
|
+
ensureJobColumn(name, definition) {
|
|
514
|
+
const columns = this.tableColumns('pibo_ralph_jobs');
|
|
515
|
+
if (!columns.has(name))
|
|
516
|
+
this.db.exec(`ALTER TABLE pibo_ralph_jobs ADD COLUMN ${name} ${definition}`);
|
|
517
|
+
}
|
|
518
|
+
ensureRunColumn(name, definition) {
|
|
519
|
+
const columns = this.tableColumns('pibo_ralph_runs');
|
|
520
|
+
if (!columns.has(name))
|
|
521
|
+
this.db.exec(`ALTER TABLE pibo_ralph_runs ADD COLUMN ${name} ${definition}`);
|
|
522
|
+
}
|
|
523
|
+
tableColumns(tableName) {
|
|
524
|
+
return new Set(this.db.prepare(`PRAGMA table_info(${tableName})`).all().map((column) => column.name));
|
|
525
|
+
}
|
|
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); }
|
|
528
|
+
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) { const run = { id: job.mode === 'ralph' ? `rrun_${randomUUID()}` : `lrun_${randomUUID()}`, jobId: job.id, status: 'running', startedAt: timestamp, ...(job.resources ? { resources: job.resources } : {}), createdAt: timestamp, updatedAt: timestamp }; this.db.prepare('INSERT INTO pibo_ralph_runs (id, job_id, pibo_session_id, status, reason, error, resource_json, started_at, completed_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(run.id, run.jobId, null, run.status, null, null, resourceMetadataJson(run.resources), run.startedAt ?? null, null, run.createdAt, run.updatedAt); return run; }
|
|
530
|
+
}
|
|
531
|
+
export function createDefaultPiboLoopStore(options = {}) { return new PiboLoopStore(options); }
|