@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.
Files changed (139) hide show
  1. package/README.md +183 -183
  2. package/context/codex-base-prompt.md +148 -148
  3. package/context/compute-worker.md +23 -23
  4. package/context/pibo-compaction-prompt.md +100 -100
  5. package/context/pibo-native-tooling.md +18 -18
  6. package/context/pibo-system-prompt.md +77 -77
  7. package/dist/apps/chat/agent-profiles.js +2 -2
  8. package/dist/apps/chat/agent-store.js +263 -250
  9. package/dist/apps/chat/chat-request-normalizers.js +10 -0
  10. package/dist/apps/chat/data/project-service.js +168 -168
  11. package/dist/apps/chat/data/read-state-service.js +18 -18
  12. package/dist/apps/chat/data/session-query-service.js +25 -25
  13. package/dist/apps/chat/data/timeline-query-service.js +19 -19
  14. package/dist/apps/chat/loop-api.js +176 -0
  15. package/dist/apps/chat/static-assets.js +854 -854
  16. package/dist/apps/chat/web-app.js +6 -6
  17. package/dist/apps/chat/workflow-manual-trigger-runtime.js +149 -46
  18. package/dist/apps/chat/workflow-persistence.js +255 -255
  19. package/dist/apps/chat-ui/assets/{dist-yCYNNb5d.js → dist-BwKObYnX.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-BCB6zezO.js → dist-CKtT8YGm.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-CnVsqwSG.js → dist-CS7wdk0Z.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-HqTN67dc.js → dist-D-cxLQO1.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-BNMu92bb.js → dist-DUlaXAk7.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-CzE6k3F3.js → dist-DlATLa-U.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-Dq4GxJi3.js → dist-GdEM8UW1.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-BZ2eTC4f.js → dist-LHRs1Nhr.js} +1 -1
  27. package/dist/apps/chat-ui/assets/{dist-D811wJeV.js → dist-Y-AA2omI.js} +1 -1
  28. package/dist/apps/chat-ui/assets/{dist-WyXdYl-w.js → dist-nOLTkZrJ.js} +1 -1
  29. package/dist/apps/chat-ui/assets/{dist-BHa-kcGl.js → dist-wE9nop9V.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{index-DwHJfmiF.js → index-8W_yMHQI.js} +6 -6
  31. package/dist/apps/chat-ui/index.html +17 -17
  32. package/dist/apps/chat-ui/manifest.webmanifest +25 -25
  33. package/dist/apps/chat-ui/sw.js +47 -47
  34. package/dist/apps/chat-vscode-web/index.html +12 -12
  35. package/dist/apps/cli-ui/cliSessionsCommand.js +23 -23
  36. package/dist/apps/context-files-ui/index.html +11 -11
  37. package/dist/cli.js +61 -44
  38. package/dist/compute/cli.js +54 -54
  39. package/dist/core/routed-session.js +30 -1
  40. package/dist/core/runtime.js +6 -1
  41. package/dist/core/session-router.js +4 -2
  42. package/dist/cron/cli.js +15 -15
  43. package/dist/cron/store.js +49 -49
  44. package/dist/data/cli.js +23 -23
  45. package/dist/data/event-log.js +23 -23
  46. package/dist/data/message-store.js +27 -27
  47. package/dist/data/navigation-store.js +9 -9
  48. package/dist/data/observation-store.js +4 -4
  49. package/dist/data/payload-store.js +17 -17
  50. package/dist/data/schema.js +433 -433
  51. package/dist/data/session-store.js +4 -4
  52. package/dist/data/telemetry-queries.js +54 -54
  53. package/dist/data/telemetry.js +197 -197
  54. package/dist/debug/events.js +12 -12
  55. package/dist/debug/failures.js +6 -6
  56. package/dist/debug/index.js +227 -227
  57. package/dist/debug/messages.js +6 -6
  58. package/dist/debug/pty.js +124 -124
  59. package/dist/debug/session.js +29 -29
  60. package/dist/debug/tools.js +5 -5
  61. package/dist/debug/web-snapshot-browser-scripts.js +294 -294
  62. package/dist/debug/web-streaming-browser-library.js +925 -925
  63. package/dist/debug/web-streaming-browser-scripts.js +232 -232
  64. package/dist/debug/web-streaming-provider-telemetry.js +4 -4
  65. package/dist/debug/web.js +93 -93
  66. package/dist/gateway/cli.js +19 -19
  67. package/dist/gateway/server.js +4 -2
  68. package/dist/gateway/web.js +2 -2
  69. package/dist/loops/channel.js +8 -0
  70. package/dist/loops/cli.js +208 -0
  71. package/dist/loops/plugin.js +16 -0
  72. package/dist/loops/prompts.js +83 -0
  73. package/dist/loops/service.js +357 -0
  74. package/dist/loops/stopping.js +170 -0
  75. package/dist/loops/store.js +531 -0
  76. package/dist/loops/templates.js +232 -0
  77. package/dist/loops/tools.js +167 -0
  78. package/dist/loops/types.js +1 -0
  79. package/dist/mcp/config-command.js +53 -53
  80. package/dist/mcp/index.js +21 -21
  81. package/dist/mcp/registry.js +11 -11
  82. package/dist/pi-packages/cli.js +11 -11
  83. package/dist/plugins/builtin.js +8 -0
  84. package/dist/plugins/context-files-store.js +110 -110
  85. package/dist/plugins/context-files.js +4 -4
  86. package/dist/plugins/registry.js +23 -12
  87. package/dist/ralph/cli.js +18 -18
  88. package/dist/ralph/templates.js +140 -140
  89. package/dist/reliability/store.js +226 -226
  90. package/dist/sessions/pibo-data-store.js +16 -16
  91. package/dist/sessions/sqlite-store.js +53 -53
  92. package/dist/setup/cli.js +58 -58
  93. package/dist/tools/agent-browser-wrapper.js +80 -80
  94. package/dist/tools/browser-use-cdp.js +12 -12
  95. package/dist/tools/browser-use-wrapper.js +762 -762
  96. package/dist/tools/guides.js +589 -538
  97. package/dist/tools/index.js +122 -99
  98. package/dist/tools/registry.js +21 -3
  99. package/dist/tools/runtime/node-worker-source.js +205 -205
  100. package/dist/tools/runtime/python-worker-source.js +177 -177
  101. package/dist/vscode/cli.js +9 -9
  102. package/dist/web-annotations/cdp.js +900 -900
  103. package/dist/web-annotations/store.js +96 -96
  104. package/docs/README.md +23 -23
  105. package/docs/ops/install-developer-host.md +112 -112
  106. package/docs/ops/install-user-host.md +96 -96
  107. package/docs/ops/upgrade-user-to-developer-host.md +69 -69
  108. package/docs/ops/vscode-extension-release.md +160 -160
  109. package/package.json +98 -95
  110. package/skills/builtin/graphify/SKILL.md +52 -52
  111. package/skills/builtin/loop/SKILL.md +69 -0
  112. package/skills/builtin/pi-agent-harness/SKILL.md +319 -319
  113. package/skills/builtin/pi-agent-harness/agents/openai.yaml +4 -4
  114. package/skills/builtin/pibo-docker-system/SKILL.md +170 -170
  115. package/skills/builtin/pibo-spec-writing/SKILL.md +330 -330
  116. package/skills/builtin/prd/SKILL.md +143 -143
  117. package/skills/builtin/ralph-loop/SKILL.md +361 -359
  118. package/skills/builtin/ralph-prd-json/SKILL.md +123 -123
  119. package/skills/builtin/skill-creator/LICENSE.txt +201 -201
  120. package/skills/builtin/skill-creator/SKILL.md +513 -513
  121. package/skills/builtin/skill-creator/agents/analyzer.md +274 -274
  122. package/skills/builtin/skill-creator/agents/comparator.md +202 -202
  123. package/skills/builtin/skill-creator/agents/grader.md +223 -223
  124. package/skills/builtin/skill-creator/assets/eval_review.html +146 -146
  125. package/skills/builtin/skill-creator/eval-viewer/generate_review.py +471 -471
  126. package/skills/builtin/skill-creator/eval-viewer/viewer.html +1325 -1325
  127. package/skills/builtin/skill-creator/references/schemas.md +430 -430
  128. package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +401 -401
  129. package/skills/builtin/skill-creator/scripts/generate_report.py +326 -326
  130. package/skills/builtin/skill-creator/scripts/improve_description.py +247 -247
  131. package/skills/builtin/skill-creator/scripts/package_skill.py +136 -136
  132. package/skills/builtin/skill-creator/scripts/quick_validate.py +102 -102
  133. package/skills/builtin/skill-creator/scripts/run_eval.py +310 -310
  134. package/skills/builtin/skill-creator/scripts/run_loop.py +328 -328
  135. package/skills/builtin/skill-creator/scripts/utils.py +47 -47
  136. package/skills/builtin/web-annotations/SKILL.md +93 -93
  137. package/src/mcp/LICENSE.mcp-cli +21 -21
  138. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  139. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.9.13.vsix +0 -0
@@ -0,0 +1,357 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { getDefaultPiboWorkspace } from '../core/workspace.js';
5
+ import { PiboDataStore } from '../data/pibo-store.js';
6
+ import { ChatRoomService } from '../apps/chat/data/room-service.js';
7
+ import { isPiboRoomArchived } from '../apps/chat/types/rooms.js';
8
+ import { browserPoolPaths, releaseBrowserPoolLease } from '../tools/browser-pool.js';
9
+ import { createDefaultPiboLoopStore } from './store.js';
10
+ import { createBuiltInLoopStopConditions, evaluateLoopStopPolicy } from './stopping.js';
11
+ import { buildLoopTurnPrompt } from './prompts.js';
12
+ const CHAT_WEB_CHANNEL = 'pibo.chat-web';
13
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
14
+ class LoopRunTimeoutError extends Error {
15
+ abortFailed;
16
+ constructor(message, abortFailed = false) {
17
+ super(message);
18
+ this.abortFailed = abortFailed;
19
+ this.name = 'LoopRunTimeoutError';
20
+ }
21
+ }
22
+ function isUnknownProfileErrorMessage(message) { return /^Unknown profile "[^"]+"/.test(message); }
23
+ function isJsonObject(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
24
+ function mergeRunResources(jobResources, runResources) {
25
+ if (!jobResources && !runResources)
26
+ return undefined;
27
+ const browserLeaseIds = [...new Set([...(jobResources?.browserLeaseIds ?? []), ...(runResources?.browserLeaseIds ?? [])])];
28
+ return {
29
+ ...(jobResources ?? {}),
30
+ ...(runResources ?? {}),
31
+ ...(browserLeaseIds.length ? { browserLeaseIds } : {}),
32
+ };
33
+ }
34
+ function clearDirtyReason(resources) {
35
+ const clean = { ...resources };
36
+ delete clean.dirtyReason;
37
+ return clean;
38
+ }
39
+ export class PiboLoopService {
40
+ options;
41
+ store;
42
+ dataStore;
43
+ roomService;
44
+ intervalMs;
45
+ maxConcurrentRuns;
46
+ runTimeoutMs;
47
+ timer;
48
+ activeRuns = 0;
49
+ stopped = true;
50
+ cancelledRuns = new Set();
51
+ unsubscribeProductEvents;
52
+ unsubscribeOutputEvents;
53
+ constructor(options) {
54
+ this.options = options;
55
+ this.store = options.store ?? createDefaultPiboLoopStore();
56
+ this.dataStore = new PiboDataStore(options.dataStorePath, { payloadRootDir: options.dataPayloadRootDir });
57
+ this.roomService = new ChatRoomService(this.dataStore);
58
+ this.intervalMs = options.intervalMs ?? 5_000;
59
+ this.maxConcurrentRuns = Math.max(1, options.maxConcurrentRuns ?? 2);
60
+ this.runTimeoutMs = options.runTimeoutMs;
61
+ }
62
+ start() { if (!this.stopped)
63
+ 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
+ stop() { this.stopped = true; if (this.timer)
65
+ clearTimeout(this.timer); this.timer = undefined; this.unsubscribeProductEvents?.(); this.unsubscribeProductEvents = undefined; this.unsubscribeOutputEvents?.(); this.unsubscribeOutputEvents = undefined; this.dataStore.close(); this.store.close(); }
66
+ status() { return { enabled: !this.stopped, ...this.store.status() }; }
67
+ async startJob(id) { const job = this.store.updateJob(id, { enabled: true }); if (!job)
68
+ return undefined; const reserved = await this.reserveAfterBeforeRunEvaluation(job); if (!reserved)
69
+ return undefined; void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon()); return reserved.run; }
70
+ stopJob(id) { const job = this.store.requestStop(id); this.armSoon(); return job; }
71
+ async cancelJob(id) {
72
+ const job = this.store.requestCancel(id);
73
+ if (!job)
74
+ return undefined;
75
+ await this.abortJobIfRunning(job);
76
+ this.armSoon();
77
+ return this.store.getJob(id);
78
+ }
79
+ arm(delayMs) { if (this.stopped)
80
+ return; if (this.timer)
81
+ clearTimeout(this.timer); this.timer = setTimeout(() => void this.tick(), delayMs ?? this.intervalMs); }
82
+ armSoon() { this.arm(250); }
83
+ async tick() { if (this.stopped)
84
+ return; try {
85
+ await this.abortCancelRequestedJobs();
86
+ const capacity = this.maxConcurrentRuns - this.activeRuns;
87
+ if (capacity > 0) {
88
+ const jobs = this.store.listJobs({ includeDisabled: false }).filter((job) => !job.state.runningAt).slice(0, capacity);
89
+ for (const job of jobs) {
90
+ const reserved = await this.reserveAfterBeforeRunEvaluation(job);
91
+ if (reserved)
92
+ void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon());
93
+ }
94
+ }
95
+ }
96
+ catch (error) {
97
+ console.error('[loop] scheduler tick failed', error);
98
+ }
99
+ finally {
100
+ this.arm();
101
+ } }
102
+ async reserveAfterBeforeRunEvaluation(job) {
103
+ const fresh = this.store.getJob(job.id) ?? job;
104
+ if (!fresh.enabled || fresh.state.runningAt)
105
+ return undefined;
106
+ const { evaluation, conditionStates } = await this.evaluateStopPolicy(fresh, 'before-run');
107
+ if (evaluation.finalAction !== 'continue') {
108
+ this.store.applyStopEvaluation({ jobId: fresh.id, evaluation, conditionStates, disable: true });
109
+ return undefined;
110
+ }
111
+ this.store.applyStopEvaluation({ jobId: fresh.id, evaluation, conditionStates, disable: false });
112
+ return this.store.reserveRun(fresh.id);
113
+ }
114
+ async abortCancelRequestedJobs() { for (const job of this.store.listJobs({ includeDisabled: true })) {
115
+ if (job.state.cancelRequestedAt)
116
+ await this.abortJobIfRunning(job);
117
+ } }
118
+ async abortJobIfRunning(job) {
119
+ if (!job.state.runningAt || !job.state.lastRunId)
120
+ return;
121
+ const sessionId = job.state.lastPiboSessionId;
122
+ if (!sessionId) {
123
+ this.markRunResourcesDirty(job, 'Cancel requested but active session is unavailable; browser lease may still exist');
124
+ return;
125
+ }
126
+ this.cancelledRuns.add(job.state.lastRunId);
127
+ try {
128
+ await this.options.context.emit({ type: 'execution', piboSessionId: sessionId, action: 'abort', id: `loop_cancel_${randomUUID()}` });
129
+ }
130
+ catch (error) {
131
+ this.cancelledRuns.delete(job.state.lastRunId);
132
+ this.markRunResourcesDirty(job, `Cancel requested but abort failed: ${errorMessage(error)}`);
133
+ }
134
+ }
135
+ async executeReserved(job, run) {
136
+ this.activeRuns += 1;
137
+ try {
138
+ const result = await this.executeJob(job, run);
139
+ const cancelled = this.cancelledRuns.delete(run.id);
140
+ if (job.mode === 'goal')
141
+ this.store.recordGoalProgress(job.id, { timeUsedSeconds: result.timeUsedSeconds });
142
+ const outcome = { status: cancelled ? 'cancelled' : 'ok', piboSessionId: result.piboSessionId, finalAnswer: result.finalAnswer };
143
+ const { evaluation, conditionStates } = await this.evaluateStopPolicy(this.store.getJob(job.id) ?? job, 'after-run', run, outcome);
144
+ 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
+ await this.cleanupRunResources(job, run);
146
+ }
147
+ catch (error) {
148
+ const cancelled = this.cancelledRuns.delete(run.id);
149
+ const message = errorMessage(error);
150
+ const fatalProfileError = !cancelled && isUnknownProfileErrorMessage(message);
151
+ const timeoutAbortFailed = error instanceof LoopRunTimeoutError && error.abortFailed;
152
+ const outcome = { status: cancelled ? 'cancelled' : 'error', error: cancelled ? undefined : message };
153
+ const { evaluation, conditionStates } = await this.evaluateStopPolicy(this.store.getJob(job.id) ?? job, 'after-run', run, outcome);
154
+ this.store.completeRun({ jobId: job.id, runId: run.id, status: outcome.status, error: outcome.error, reason: cancelled ? 'cancelled' : fatalProfileError ? 'unknown-profile' : timeoutAbortFailed ? 'timeout-abort-failed' : evaluation.reason, stopAfterRun: fatalProfileError || timeoutAbortFailed || evaluation.finalAction !== 'continue', stopEvaluation: evaluation, conditionStates });
155
+ await this.cleanupRunResources(job, run);
156
+ if (!cancelled)
157
+ console.error(`[loop] job ${job.id} failed`, error);
158
+ }
159
+ finally {
160
+ this.activeRuns -= 1;
161
+ }
162
+ }
163
+ markRunResourcesDirty(job, dirtyReason) {
164
+ const latestJob = this.store.getJob(job.id) ?? job;
165
+ const runId = latestJob.state.lastRunId ?? job.state.lastRunId;
166
+ const latestRun = runId ? this.store.listRuns({ jobId: latestJob.id, limit: 100 }).find((candidate) => candidate.id === runId) : undefined;
167
+ const resources = mergeRunResources(latestJob.resources, latestRun?.resources);
168
+ if (!resources || (!resources.workerId && (resources.browserLeaseIds ?? []).length === 0))
169
+ return;
170
+ const nextResources = { ...resources, cleanupState: 'dirty', dirtyReason, updatedAt: new Date().toISOString() };
171
+ try {
172
+ if (latestRun)
173
+ this.store.updateRunResources({ jobId: latestJob.id, runId: latestRun.id, resources: nextResources });
174
+ this.store.updateJobResources(latestJob.id, nextResources);
175
+ }
176
+ catch (error) {
177
+ console.error(`[loop] failed to mark resource cleanup dirty for job ${latestJob.id}`, error);
178
+ }
179
+ }
180
+ async cleanupRunResources(job, run) {
181
+ const latestJob = this.store.getJob(job.id) ?? job;
182
+ const latestRun = this.store.listRuns({ jobId: job.id, limit: 100 }).find((candidate) => candidate.id === run.id) ?? run;
183
+ const resources = mergeRunResources(latestJob.resources, latestRun.resources);
184
+ const leaseIds = resources?.browserLeaseIds ?? [];
185
+ if (!resources || leaseIds.length === 0)
186
+ return;
187
+ const workerId = resources.workerId || process.env.PIBO_BROWSER_POOL_WORKER_ID || process.env.PIBO_COMPUTE_WORKER_ID || process.env.HOSTNAME || 'local';
188
+ const poolId = this.options.resourceCleanup?.browserPoolId || process.env.PIBO_BROWSER_POOL_ID || 'default';
189
+ 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');
190
+ const identity = { workerId, poolId };
191
+ const paths = browserPoolPaths(rootDir, identity);
192
+ const release = this.options.resourceCleanup?.releaseBrowserPoolLease ?? releaseBrowserPoolLease;
193
+ let dirtyReason;
194
+ for (const leaseId of leaseIds) {
195
+ try {
196
+ const result = await release(paths, identity, { leaseId, lockOptions: { holder: `loop:${run.id}` } });
197
+ if (result.cleanupStatus === 'failed' || result.state.state === 'dirty' || (!result.released && !!result.state.activeLeaseId))
198
+ dirtyReason = result.lastError || `Browser lease ${leaseId} cleanup failed`;
199
+ }
200
+ catch (error) {
201
+ dirtyReason = `Browser lease ${leaseId} cleanup failed: ${errorMessage(error)}`;
202
+ }
203
+ if (dirtyReason)
204
+ break;
205
+ }
206
+ const updatedAt = new Date().toISOString();
207
+ const nextResources = dirtyReason
208
+ ? { ...resources, workerId, cleanupState: 'dirty', dirtyReason, updatedAt }
209
+ : clearDirtyReason({ ...resources, workerId, cleanupState: 'released', updatedAt });
210
+ try {
211
+ this.store.updateRunResources({ jobId: job.id, runId: run.id, resources: nextResources });
212
+ this.store.updateJobResources(job.id, nextResources);
213
+ }
214
+ catch (error) {
215
+ console.error(`[loop] failed to record resource cleanup for run ${run.id}`, error);
216
+ }
217
+ }
218
+ async evaluateStopPolicy(job, phase, run, outcome) {
219
+ return await evaluateLoopStopPolicy({ job, phase, definitions: this.getStopConditionDefinitions(), facts: this.store.createFactReader(job), run, outcome });
220
+ }
221
+ getStopConditionDefinitions() { return this.options.context.getLoopStopConditionDefinitions?.() ?? this.options.context.getRalphStopConditionDefinitions?.() ?? createBuiltInLoopStopConditions(); }
222
+ handleOutputEvent(event) {
223
+ if (event.type !== 'assistant_usage')
224
+ return;
225
+ const job = this.store.getLatestGoalForSession(event.piboSessionId);
226
+ if (!job)
227
+ return;
228
+ const status = job.state.goalStatus ?? (job.enabled ? 'active' : 'paused');
229
+ if (!job.state.runningAt && status !== 'active')
230
+ return;
231
+ this.store.recordGoalProgress(job.id, { tokens: event.totalTokens });
232
+ }
233
+ handleProductEvent(event) {
234
+ if (event.type !== 'pibo.loop.fact' && event.type !== 'loop.fact' && event.type !== 'pibo.ralph.fact' && event.type !== 'ralph.fact')
235
+ return;
236
+ const payload = event.payload;
237
+ if (!isJsonObject(payload.payload))
238
+ return;
239
+ if (typeof payload.jobId !== 'string' || typeof payload.type !== 'string')
240
+ return;
241
+ const source = payload.source === 'pi-extension' || payload.source === 'tool' || payload.source === 'plugin' || payload.source === 'pibo' ? payload.source : 'plugin';
242
+ try {
243
+ this.store.appendRunFact({ jobId: payload.jobId, runId: typeof payload.runId === 'string' ? payload.runId : undefined, piboSessionId: typeof payload.piboSessionId === 'string' ? payload.piboSessionId : undefined, type: payload.type, source: source, payload: payload.payload });
244
+ }
245
+ catch (error) {
246
+ console.error('[loop] failed to append run fact', error);
247
+ }
248
+ }
249
+ async executeJob(job, run) {
250
+ const reusableSessionId = job.mode === 'goal' ? job.state.lastPiboSessionId : undefined;
251
+ const reusableSession = reusableSessionId ? this.options.context.getSession(reusableSessionId) : undefined;
252
+ const continuation = reusableSession !== undefined;
253
+ const session = reusableSession ?? this.createLoopSession(job, run);
254
+ if (reusableSession) {
255
+ this.options.context.updateSession?.(session.id, {
256
+ title: job.name,
257
+ metadata: { ...(session.metadata ?? {}), loopRunId: run.id, loopMode: job.mode },
258
+ ...(job.modelOverride ? { activeModel: { ...job.modelOverride } } : {}),
259
+ });
260
+ }
261
+ this.store.attachRunSession(job.id, run.id, session.id);
262
+ const result = await this.emitMessageAndWait(session.id, buildLoopTurnPrompt(job, continuation, this.goalToolsAvailable(job)));
263
+ return { piboSessionId: session.id, ...result };
264
+ }
265
+ createLoopSession(job, run) {
266
+ const target = this.resolveTarget(job);
267
+ return this.options.context.createSession({
268
+ channel: CHAT_WEB_CHANNEL,
269
+ kind: 'loop',
270
+ profile: job.profile,
271
+ workspace: target.workspace ?? getDefaultPiboWorkspace(),
272
+ title: job.name,
273
+ activeModel: job.modelOverride ? { ...job.modelOverride } : undefined,
274
+ metadata: {
275
+ ...(target.metadata ?? {}),
276
+ chatRoomId: target.roomId,
277
+ loopJobId: job.id,
278
+ loopRunId: run.id,
279
+ loopMode: job.mode,
280
+ loopTargetKind: job.target.kind,
281
+ ...(job.mode === 'ralph' ? { ralphJobId: job.id, ralphRunId: run.id } : {}),
282
+ ...(job.thinkingLevel ? { initialThinkingLevel: job.thinkingLevel } : {}),
283
+ ...(job.fastMode !== undefined ? { initialFastMode: job.fastMode } : {}),
284
+ },
285
+ });
286
+ }
287
+ goalToolsAvailable(job) {
288
+ if (job.mode !== 'goal')
289
+ return false;
290
+ try {
291
+ return this.options.context.createProfile?.(job.profile).toolPackages.goalControl !== false;
292
+ }
293
+ catch {
294
+ return true;
295
+ }
296
+ }
297
+ resolveTarget(job) { if (job.target.kind === 'room') {
298
+ const room = this.roomService.getRoom(job.target.roomId);
299
+ if (!room)
300
+ throw new Error('Target room no longer exists');
301
+ if (isPiboRoomArchived(room))
302
+ throw new Error('Target room is archived');
303
+ return { roomId: room.id, workspace: room.workspace ?? getDefaultPiboWorkspace() };
304
+ } const room = this.roomService.ensureDefaultRoom({ name: 'Shared Chat' }); return { roomId: room.id, workspace: room.workspace ?? getDefaultPiboWorkspace() }; }
305
+ async emitMessageAndWait(piboSessionId, text) {
306
+ const eventId = `loop_msg_${randomUUID()}`;
307
+ const startedAt = Date.now();
308
+ return await new Promise((resolve, reject) => {
309
+ let settled = false;
310
+ let deltaAnswer = '';
311
+ let finalAnswer = '';
312
+ let lastSessionError;
313
+ let timingOut = false;
314
+ let unsubscribe;
315
+ let timeout;
316
+ const finish = (error) => {
317
+ if (settled)
318
+ return;
319
+ settled = true;
320
+ if (timeout)
321
+ clearTimeout(timeout);
322
+ unsubscribe?.();
323
+ if (error)
324
+ reject(error);
325
+ else
326
+ resolve({ finalAnswer: finalAnswer || deltaAnswer, timeUsedSeconds: Math.max(0, Math.ceil((Date.now() - startedAt) / 1000)) });
327
+ };
328
+ if (this.runTimeoutMs !== undefined) {
329
+ timeout = setTimeout(() => {
330
+ timingOut = true;
331
+ const message = lastSessionError ? `Loop run timed out after session error: ${lastSessionError}` : 'Loop run timed out';
332
+ void this.options.context.emit({ type: 'execution', piboSessionId, action: 'abort', id: `loop_timeout_${randomUUID()}` })
333
+ .then(() => finish(new LoopRunTimeoutError(message)), (abortError) => finish(new LoopRunTimeoutError(`${message}; session abort failed: ${errorMessage(abortError)}`, true)));
334
+ }, this.runTimeoutMs);
335
+ }
336
+ unsubscribe = this.options.context.subscribe((event) => {
337
+ if (timingOut || event.piboSessionId !== piboSessionId)
338
+ return;
339
+ if ('eventId' in event && event.eventId !== eventId)
340
+ return;
341
+ if (event.type === 'assistant_delta')
342
+ deltaAnswer += event.text;
343
+ if (event.type === 'assistant_message') {
344
+ finalAnswer = event.text;
345
+ lastSessionError = undefined;
346
+ }
347
+ if (event.type === 'message_finished')
348
+ finish(lastSessionError ? new Error(lastSessionError) : undefined);
349
+ if (event.type === 'session_error') {
350
+ lastSessionError = event.error;
351
+ finish(new Error(event.error));
352
+ }
353
+ });
354
+ this.options.context.emit({ type: 'message', piboSessionId, id: eventId, source: 'service', text }).catch((error) => finish(error instanceof Error ? error : new Error(String(error))));
355
+ });
356
+ }
357
+ }
@@ -0,0 +1,170 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export const PROMISE_COMPLETE_STOP_TOKEN = '<promise>COMPLETE</promise>';
3
+ export const MAX_ITERATIONS_STOP_CONDITION = 'pibo.loop.max-iterations';
4
+ export const PROMISE_COMPLETE_STOP_CONDITION = 'pibo.loop.promise-complete';
5
+ export const FACT_COUNT_STOP_CONDITION = 'pibo.loop.fact-count';
6
+ export const GOAL_STATUS_STOP_CONDITION = 'pibo.loop.goal-status';
7
+ function hasPromiseCompleteMarker(finalAnswer) { return finalAnswer.split(/\r?\n/).some((line) => line.trim() === PROMISE_COMPLETE_STOP_TOKEN); }
8
+ function isObject(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
9
+ function stringOption(options, key) { const value = options?.[key]; return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
10
+ function numberOption(options, key) { const value = options?.[key]; return typeof value === 'number' && Number.isFinite(value) ? value : undefined; }
11
+ function boolOption(options, key) { const value = options?.[key]; return typeof value === 'boolean' ? value : undefined; }
12
+ export function getDefaultLoopStopPolicy(job) {
13
+ const conditions = [];
14
+ if (job.maxIterations !== undefined)
15
+ conditions.push({ id: 'max-iterations', type: MAX_ITERATIONS_STOP_CONDITION });
16
+ if (job.mode === 'goal')
17
+ conditions.push({ id: 'goal-status', type: GOAL_STATUS_STOP_CONDITION });
18
+ else
19
+ conditions.push({ id: 'promise-complete', type: PROMISE_COMPLETE_STOP_CONDITION });
20
+ return { mode: 'any', conditions };
21
+ }
22
+ export function getEffectiveLoopStopPolicy(job) {
23
+ return job.stopPolicy ?? getDefaultLoopStopPolicy(job);
24
+ }
25
+ export function createBuiltInLoopStopConditions() {
26
+ return [
27
+ {
28
+ type: MAX_ITERATIONS_STOP_CONDITION,
29
+ name: 'Max iterations',
30
+ description: 'Stops a Loop job after its configured number of completed run attempts, regardless of outcome.',
31
+ phases: ['before-run', 'after-run'],
32
+ evaluate(context) {
33
+ const maxIterations = context.job.maxIterations;
34
+ if (maxIterations === undefined)
35
+ return { action: 'continue' };
36
+ const current = context.job.state.completedIterations ?? 0;
37
+ const completed = context.phase === 'after-run' && context.outcome ? current + 1 : current;
38
+ if (completed >= maxIterations)
39
+ return { action: 'stop-after-run', reason: 'max-iterations', details: { maxIterations, completedIterations: completed } };
40
+ return { action: 'continue', details: { maxIterations, completedIterations: completed } };
41
+ },
42
+ },
43
+ {
44
+ type: PROMISE_COMPLETE_STOP_CONDITION,
45
+ name: 'Promise complete token',
46
+ description: 'Stops after a successful run whose final answer contains the promise-complete marker on its own line.',
47
+ phases: ['after-run'],
48
+ evaluate(context) {
49
+ const finalAnswer = context.outcome?.finalAnswer ?? '';
50
+ if (context.outcome?.status === 'ok' && hasPromiseCompleteMarker(finalAnswer))
51
+ return { action: 'stop-after-run', reason: 'promise-complete', details: { token: PROMISE_COMPLETE_STOP_TOKEN } };
52
+ return { action: 'continue' };
53
+ },
54
+ },
55
+ {
56
+ type: GOAL_STATUS_STOP_CONDITION,
57
+ name: 'Goal status',
58
+ description: 'Stops a Goal loop after native tooling or accounting marks it complete, blocked, or budget limited.',
59
+ phases: ['before-run', 'after-run'],
60
+ evaluate(context) {
61
+ if (context.job.mode !== 'goal')
62
+ return { action: 'continue' };
63
+ const status = context.job.state.goalStatus ?? (context.job.enabled ? 'active' : 'paused');
64
+ if (status === 'complete' || status === 'blocked' || status === 'budget_limited')
65
+ return { action: 'stop-after-run', reason: `goal-${status}`, details: { status } };
66
+ return { action: 'continue', details: { status } };
67
+ },
68
+ },
69
+ {
70
+ type: FACT_COUNT_STOP_CONDITION,
71
+ name: 'Run fact count',
72
+ description: 'Stops when the current run or job history contains at least a configured number of matching Loop run facts.',
73
+ phases: ['after-run'],
74
+ defaultOptions: { factType: 'example.fact', threshold: 1, currentRunOnly: true },
75
+ optionsSchema: {
76
+ type: 'object',
77
+ properties: {
78
+ factType: { type: 'string', description: 'Fact type to count.' },
79
+ threshold: { type: 'number', description: 'Stop after at least this many facts.' },
80
+ currentRunOnly: { type: 'boolean', description: 'Only count facts for the current run.' },
81
+ reason: { type: 'string', description: 'Optional stop reason.' },
82
+ },
83
+ },
84
+ evaluate(context) {
85
+ const factType = stringOption(context.instance.options, 'factType');
86
+ if (!factType)
87
+ return { action: 'continue', details: { missingOption: 'factType' } };
88
+ const threshold = Math.max(1, Math.floor(numberOption(context.instance.options, 'threshold') ?? 1));
89
+ const currentRunOnly = boolOption(context.instance.options, 'currentRunOnly') ?? true;
90
+ const runId = currentRunOnly ? context.run?.id : undefined;
91
+ const count = context.facts.count({ type: factType, runId });
92
+ if (count >= threshold)
93
+ return { action: 'stop-after-run', reason: stringOption(context.instance.options, 'reason') ?? `fact:${factType}`, details: { factType, threshold, count, currentRunOnly } };
94
+ return { action: 'continue', details: { factType, threshold, count, currentRunOnly } };
95
+ },
96
+ },
97
+ ];
98
+ }
99
+ function severity(action) {
100
+ if (action === 'cancel-current-run')
101
+ return 2;
102
+ if (action === 'stop-after-run')
103
+ return 1;
104
+ return 0;
105
+ }
106
+ function highest(left, right) { return severity(right) > severity(left) ? right : left; }
107
+ function sanitizeDecision(value) {
108
+ if (!isObject(value))
109
+ throw new Error('condition returned a non-object decision');
110
+ if (value.action !== 'continue' && value.action !== 'stop-after-run' && value.action !== 'cancel-current-run')
111
+ throw new Error('condition returned an invalid action');
112
+ return { action: value.action, reason: typeof value.reason === 'string' ? value.reason : undefined, details: isObject(value.details) ? value.details : undefined, nextState: isObject(value.nextState) ? value.nextState : undefined };
113
+ }
114
+ async function withTimeout(promise, timeoutMs, signal) {
115
+ if (!timeoutMs || timeoutMs <= 0)
116
+ return await promise;
117
+ return await new Promise((resolve, reject) => {
118
+ const timeout = setTimeout(() => reject(new Error(`condition timed out after ${timeoutMs}ms`)), timeoutMs);
119
+ if (signal)
120
+ signal.addEventListener('abort', () => reject(new Error('condition aborted')), { once: true });
121
+ promise.then((value) => { clearTimeout(timeout); resolve(value); }, (error) => { clearTimeout(timeout); reject(error); });
122
+ });
123
+ }
124
+ export async function evaluateLoopStopPolicy(input) {
125
+ const now = (input.now ?? new Date()).toISOString();
126
+ const policy = getEffectiveLoopStopPolicy(input.job);
127
+ const definitions = new Map(input.definitions.map((definition) => [definition.type, definition]));
128
+ const decisions = [];
129
+ const conditionStates = { ...(input.job.state.conditionStates ?? {}) };
130
+ let finalAction = 'continue';
131
+ for (const instance of policy.conditions) {
132
+ const base = { id: instance.id, type: instance.type, phase: input.phase };
133
+ if (instance.enabled === false) {
134
+ decisions.push({ ...base, action: 'continue', skipped: true, reason: 'disabled' });
135
+ continue;
136
+ }
137
+ const compatibleType = instance.type.startsWith('pibo.ralph.') ? instance.type.replace('pibo.ralph.', 'pibo.loop.') : instance.type;
138
+ const definition = definitions.get(instance.type) ?? definitions.get(compatibleType);
139
+ if (!definition) {
140
+ decisions.push({ ...base, action: 'continue', skipped: true, error: `Unknown Loop stop condition type: ${instance.type}` });
141
+ continue;
142
+ }
143
+ if (!definition.phases.includes(input.phase)) {
144
+ decisions.push({ ...base, action: 'continue', skipped: true, reason: 'unsupported-phase' });
145
+ continue;
146
+ }
147
+ try {
148
+ const state = conditionStates[instance.id] ?? {};
149
+ const decision = sanitizeDecision(await withTimeout(Promise.resolve(definition.evaluate({ phase: input.phase, job: input.job, policy, instance, state, now, run: input.run, outcome: input.outcome, facts: input.facts, signal: input.signal })), instance.timeoutMs ?? definition.timeoutMs, input.signal));
150
+ if (decision.nextState)
151
+ conditionStates[instance.id] = decision.nextState;
152
+ decisions.push({ ...base, action: decision.action, reason: decision.reason, details: decision.details });
153
+ }
154
+ catch (error) {
155
+ const failClosed = instance.failClosed ?? definition.failClosedDefault ?? false;
156
+ const action = failClosed ? 'stop-after-run' : 'continue';
157
+ decisions.push({ ...base, action, reason: failClosed ? 'condition-error-fail-closed' : 'condition-error', error: error instanceof Error ? error.message : String(error) });
158
+ }
159
+ }
160
+ const active = decisions.filter((decision) => !decision.skipped);
161
+ if (policy.mode === 'all' && active.length > 0) {
162
+ const allStop = active.every((decision) => severity(decision.action) >= severity('stop-after-run'));
163
+ finalAction = allStop ? active.reduce((action, decision) => highest(action, decision.action), 'stop-after-run') : 'continue';
164
+ }
165
+ else {
166
+ finalAction = active.reduce((action, decision) => highest(action, decision.action), 'continue');
167
+ }
168
+ const contributing = finalAction === 'continue' ? active.find((decision) => decision.error || decision.reason) : active.find((decision) => decision.action === finalAction) ?? active.find((decision) => severity(decision.action) > 0);
169
+ return { evaluation: { id: `rse_${randomUUID()}`, phase: input.phase, at: now, mode: policy.mode, finalAction, reason: contributing?.reason ?? (contributing?.error ? 'condition-error' : undefined), decisions }, conditionStates };
170
+ }