@pasko70/pibo 1.9.11 → 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 (53) hide show
  1. package/dist/apps/chat/agent-profiles.js +2 -2
  2. package/dist/apps/chat/agent-store.js +16 -3
  3. package/dist/apps/chat/chat-request-normalizers.js +10 -0
  4. package/dist/apps/chat/data/timeline-query-service.js +11 -0
  5. package/dist/apps/chat/loop-api.js +176 -0
  6. package/dist/apps/chat/trace.js +2 -0
  7. package/dist/apps/chat/web-app.js +13 -6
  8. package/dist/apps/chat/workflow-manual-trigger-runtime.js +149 -46
  9. package/dist/apps/chat-ui/assets/{dist-yCYNNb5d.js → dist-BwKObYnX.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-BCB6zezO.js → dist-CKtT8YGm.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-CnVsqwSG.js → dist-CS7wdk0Z.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-HqTN67dc.js → dist-D-cxLQO1.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-BNMu92bb.js → dist-DUlaXAk7.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CzE6k3F3.js → dist-DlATLa-U.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-Dq4GxJi3.js → dist-GdEM8UW1.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-BZ2eTC4f.js → dist-LHRs1Nhr.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-D811wJeV.js → dist-Y-AA2omI.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-WyXdYl-w.js → dist-nOLTkZrJ.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-BHa-kcGl.js → dist-wE9nop9V.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{index-DwHJfmiF.js → index-8W_yMHQI.js} +6 -6
  21. package/dist/apps/chat-ui/index.html +1 -1
  22. package/dist/cli.js +23 -6
  23. package/dist/core/routed-session.js +30 -1
  24. package/dist/core/runtime.js +6 -1
  25. package/dist/core/session-router.js +4 -2
  26. package/dist/data/ingest-service.js +7 -0
  27. package/dist/data/message-store.js +11 -0
  28. package/dist/data/schema.js +2 -0
  29. package/dist/gateway/server.js +4 -2
  30. package/dist/gateway/web.js +2 -2
  31. package/dist/loops/channel.js +8 -0
  32. package/dist/loops/cli.js +208 -0
  33. package/dist/loops/plugin.js +16 -0
  34. package/dist/loops/prompts.js +83 -0
  35. package/dist/loops/service.js +357 -0
  36. package/dist/loops/stopping.js +170 -0
  37. package/dist/loops/store.js +531 -0
  38. package/dist/loops/templates.js +232 -0
  39. package/dist/loops/tools.js +167 -0
  40. package/dist/loops/types.js +1 -0
  41. package/dist/plugins/builtin.js +8 -0
  42. package/dist/plugins/registry.js +23 -12
  43. package/dist/resources/lifecycle.js +4 -6
  44. package/dist/resources/reaper-state.js +30 -3
  45. package/dist/resources/reaper.js +43 -15
  46. package/dist/shared/trace-engine.js +3 -2
  47. package/dist/shared/trace-event-projection.js +26 -0
  48. package/dist/tools/guides.js +51 -0
  49. package/dist/tools/index.js +23 -0
  50. package/dist/tools/registry.js +21 -3
  51. package/package.json +5 -2
  52. package/skills/builtin/loop/SKILL.md +69 -0
  53. package/skills/builtin/ralph-loop/SKILL.md +4 -2
@@ -3,7 +3,7 @@ export function createCustomAgentProfileDefinition(agent, options = {}) {
3
3
  const shouldWarnMissingReferences = options.missingReferenceMode !== "silent";
4
4
  return {
5
5
  name: agent.profileName,
6
- aliases: uniqueAliases([agent.id, `custom-agent:${agent.id}`, ...agent.profileAliases], agent.profileName),
6
+ aliases: uniqueAliases([agent.id, `custom-agent:${agent.id}`, ...(agent.profileAliases ?? [])], agent.profileName),
7
7
  description: agent.description || agent.displayName,
8
8
  create(context) {
9
9
  const builder = new InitialSessionContextBuilder(agent.profileName)
@@ -12,7 +12,7 @@ export function createCustomAgentProfileDefinition(agent, options = {}) {
12
12
  .withAutoContextFiles(agent.autoContextFiles)
13
13
  .withMcpServers(agent.mcpServers)
14
14
  .withPiPackages(agent.piPackages.map((id) => ({ id })))
15
- .withToolPackages({ runControl: agent.runControl });
15
+ .withToolPackages({ runControl: agent.runControl, goalControl: agent.goalControl ?? true });
16
16
  if (agent.mainModel)
17
17
  builder.withMainModel(agent.mainModel);
18
18
  if (agent.subagentModel)
@@ -42,6 +42,7 @@ export class CustomAgentStore {
42
42
  builtin_tool_names_json TEXT NOT NULL DEFAULT '["read","bash","edit","write"]',
43
43
  auto_context_files INTEGER NOT NULL DEFAULT 1,
44
44
  run_control INTEGER NOT NULL,
45
+ goal_control INTEGER NOT NULL DEFAULT 1,
45
46
  created_at TEXT NOT NULL,
46
47
  updated_at TEXT NOT NULL,
47
48
  archived_at TEXT
@@ -57,6 +58,7 @@ export class CustomAgentStore {
57
58
  this.migrateThinkingLevelColumn();
58
59
  this.migrateThinkingOptionColumns();
59
60
  this.migrateBuiltinToolNamesColumn();
61
+ this.migrateGoalControlColumn();
60
62
  this.migrateAgentHistory();
61
63
  this.migrateLegacyProfileNames();
62
64
  this.migrateDuplicateProfileNames();
@@ -104,6 +106,7 @@ export class CustomAgentStore {
104
106
  builtinToolNames: sanitizeBuiltinToolNames(input.builtinToolNames),
105
107
  autoContextFiles: input.autoContextFiles ?? true,
106
108
  runControl: input.runControl ?? false,
109
+ goalControl: input.goalControl ?? true,
107
110
  createdAt: now,
108
111
  updatedAt: now,
109
112
  };
@@ -143,6 +146,7 @@ export class CustomAgentStore {
143
146
  builtinToolNames: input.builtinToolNames ? sanitizeBuiltinToolNames(input.builtinToolNames) : existing.builtinToolNames,
144
147
  autoContextFiles: input.autoContextFiles ?? existing.autoContextFiles,
145
148
  runControl: input.runControl ?? existing.runControl,
149
+ goalControl: input.goalControl ?? existing.goalControl,
146
150
  updatedAt: new Date().toISOString(),
147
151
  };
148
152
  this.db
@@ -169,10 +173,11 @@ export class CustomAgentStore {
169
173
  builtin_tool_names_json = ?,
170
174
  auto_context_files = ?,
171
175
  run_control = ?,
176
+ goal_control = ?,
172
177
  updated_at = ?
173
178
  WHERE id = ?
174
179
  `)
175
- .run(updated.profileName, updated.displayName, updated.description ?? null, JSON.stringify(updated.nativeTools), JSON.stringify(updated.skills), JSON.stringify(updated.contextFiles), JSON.stringify(sanitizeSubagents(updated.subagents)), JSON.stringify(updated.mcpServers), JSON.stringify(updated.piPackages), updated.mainModel ? JSON.stringify(updated.mainModel) : null, updated.subagentModel ? JSON.stringify(updated.subagentModel) : null, updated.thinkingLevel ?? null, updated.mainThinkingLevel ?? null, updated.subagentThinkingLevel ?? null, serializeBoolean(updated.fast), serializeBoolean(updated.mainFast), serializeBoolean(updated.subagentFast), updated.builtinTools, JSON.stringify(updated.builtinToolNames), updated.autoContextFiles ? 1 : 0, updated.runControl ? 1 : 0, updated.updatedAt, id);
180
+ .run(updated.profileName, updated.displayName, updated.description ?? null, JSON.stringify(updated.nativeTools), JSON.stringify(updated.skills), JSON.stringify(updated.contextFiles), JSON.stringify(sanitizeSubagents(updated.subagents)), JSON.stringify(updated.mcpServers), JSON.stringify(updated.piPackages), updated.mainModel ? JSON.stringify(updated.mainModel) : null, updated.subagentModel ? JSON.stringify(updated.subagentModel) : null, updated.thinkingLevel ?? null, updated.mainThinkingLevel ?? null, updated.subagentThinkingLevel ?? null, serializeBoolean(updated.fast), serializeBoolean(updated.mainFast), serializeBoolean(updated.subagentFast), updated.builtinTools, JSON.stringify(updated.builtinToolNames), updated.autoContextFiles ? 1 : 0, updated.runControl ? 1 : 0, updated.goalControl ? 1 : 0, updated.updatedAt, id);
176
181
  return this.get(id);
177
182
  }
178
183
  setArchived(id, archived) {
@@ -220,12 +225,13 @@ export class CustomAgentStore {
220
225
  builtin_tool_names_json,
221
226
  auto_context_files,
222
227
  run_control,
228
+ goal_control,
223
229
  created_at,
224
230
  updated_at,
225
231
  archived_at
226
- ) VALUES (${Array.from({ length: 25 }, () => "?").join(", ")})
232
+ ) VALUES (${Array.from({ length: 26 }, () => "?").join(", ")})
227
233
  `)
228
- .run(agent.id, agent.profileName, agent.displayName, agent.description ?? null, JSON.stringify(agent.nativeTools), JSON.stringify(agent.skills), JSON.stringify(agent.contextFiles), JSON.stringify(sanitizeSubagents(agent.subagents)), JSON.stringify(agent.mcpServers), JSON.stringify(agent.piPackages), agent.mainModel ? JSON.stringify(agent.mainModel) : null, agent.subagentModel ? JSON.stringify(agent.subagentModel) : null, agent.thinkingLevel ?? null, agent.mainThinkingLevel ?? null, agent.subagentThinkingLevel ?? null, serializeBoolean(agent.fast), serializeBoolean(agent.mainFast), serializeBoolean(agent.subagentFast), agent.builtinTools, JSON.stringify(agent.builtinToolNames), agent.autoContextFiles ? 1 : 0, agent.runControl ? 1 : 0, agent.createdAt, agent.updatedAt, agent.archivedAt ?? null);
234
+ .run(agent.id, agent.profileName, agent.displayName, agent.description ?? null, JSON.stringify(agent.nativeTools), JSON.stringify(agent.skills), JSON.stringify(agent.contextFiles), JSON.stringify(sanitizeSubagents(agent.subagents)), JSON.stringify(agent.mcpServers), JSON.stringify(agent.piPackages), agent.mainModel ? JSON.stringify(agent.mainModel) : null, agent.subagentModel ? JSON.stringify(agent.subagentModel) : null, agent.thinkingLevel ?? null, agent.mainThinkingLevel ?? null, agent.subagentThinkingLevel ?? null, serializeBoolean(agent.fast), serializeBoolean(agent.mainFast), serializeBoolean(agent.subagentFast), agent.builtinTools, JSON.stringify(agent.builtinToolNames), agent.autoContextFiles ? 1 : 0, agent.runControl ? 1 : 0, agent.goalControl ? 1 : 0, agent.createdAt, agent.updatedAt, agent.archivedAt ?? null);
229
235
  }
230
236
  requireProfileNameAvailable(profileName, currentId) {
231
237
  const row = this.db.prepare("SELECT id FROM chat_agents WHERE profile_name = ?").get(profileName);
@@ -380,6 +386,12 @@ export class CustomAgentStore {
380
386
  this.db.prepare("ALTER TABLE chat_agents ADD COLUMN builtin_tool_names_json TEXT NOT NULL DEFAULT '[\"read\",\"bash\",\"edit\",\"write\"]'").run();
381
387
  }
382
388
  }
389
+ migrateGoalControlColumn() {
390
+ const columns = new Set(this.db.prepare("PRAGMA table_info(chat_agents)").all().map((column) => column.name));
391
+ if (!columns.has("goal_control")) {
392
+ this.db.prepare("ALTER TABLE chat_agents ADD COLUMN goal_control INTEGER NOT NULL DEFAULT 1").run();
393
+ }
394
+ }
383
395
  migrateAgentHistory() {
384
396
  this.db.exec(`
385
397
  CREATE TABLE IF NOT EXISTS chat_agent_events (
@@ -542,6 +554,7 @@ function agentFromRow(row, profileAliases) {
542
554
  builtinToolNames: sanitizeBuiltinToolNames(parseStringArray(row.builtin_tool_names_json)),
543
555
  autoContextFiles: row.auto_context_files !== 0,
544
556
  runControl: row.run_control === 1,
557
+ goalControl: row.goal_control !== 0,
545
558
  createdAt: row.created_at,
546
559
  updatedAt: row.updated_at,
547
560
  archivedAt: row.archived_at ?? undefined,
@@ -193,6 +193,13 @@ export function normalizeRunControl(value) {
193
193
  throw new PiboWebHttpError("runControl must be a boolean", 400);
194
194
  return value;
195
195
  }
196
+ export function normalizeGoalControl(value) {
197
+ if (value === undefined)
198
+ return true;
199
+ if (typeof value !== "boolean")
200
+ throw new PiboWebHttpError("goalControl must be a boolean", 400);
201
+ return value;
202
+ }
196
203
  export function normalizeOptionalBoolean(value, fieldName) {
197
204
  if (value === undefined || value === null)
198
205
  return undefined;
@@ -611,6 +618,7 @@ export function createAgentInput(body) {
611
618
  builtinToolNames: normalizeBuiltinToolNames(body.builtinToolNames),
612
619
  autoContextFiles: normalizeAutoContextFiles(body.autoContextFiles),
613
620
  runControl: normalizeRunControl(body.runControl),
621
+ goalControl: normalizeGoalControl(body.goalControl),
614
622
  };
615
623
  }
616
624
  export function createAgentUpdate(body) {
@@ -655,6 +663,8 @@ export function createAgentUpdate(body) {
655
663
  update.autoContextFiles = normalizeAutoContextFiles(body.autoContextFiles);
656
664
  if (body.runControl !== undefined)
657
665
  update.runControl = normalizeRunControl(body.runControl);
666
+ if (body.goalControl !== undefined)
667
+ update.goalControl = normalizeGoalControl(body.goalControl);
658
668
  if (Object.keys(update).length === 0 && body.archived === undefined) {
659
669
  throw new PiboWebHttpError("No agent update fields provided", 400);
660
670
  }
@@ -1,3 +1,4 @@
1
+ import { messageTurnTimingsFromEvents } from "../../../shared/trace-event-projection.js";
1
2
  import { storedChatEventFromV2Row, storedPiboEventFromV2Row } from "./chat-data-mappers.js";
2
3
  export class ChatTimelineQueryService {
3
4
  store;
@@ -30,6 +31,16 @@ export class ChatTimelineQueryService {
30
31
  listAllSessionEvents(piboSessionId) {
31
32
  return this.listTraceEvents({ piboSessionId, limit: 10000, includeLive: true });
32
33
  }
34
+ listMessageTurnTimings(piboSessionId) {
35
+ const rows = this.store.db.prepare(`
36
+ SELECT * FROM event_log
37
+ WHERE session_id = ?
38
+ AND type IN ('message_started', 'message_finished')
39
+ ORDER BY session_sequence ASC, stream_id ASC
40
+ `).all(piboSessionId);
41
+ const events = rows.map(storedPiboEventFromV2Row).filter((event) => event !== undefined);
42
+ return messageTurnTimingsFromEvents(events);
43
+ }
33
44
  listTraceEvents(input) {
34
45
  const piboSessionId = typeof input === "string" ? input : input.piboSessionId;
35
46
  const limit = Math.max(1, Math.min((typeof input === "string" ? undefined : input.limit) ?? 2000, 10000));
@@ -0,0 +1,176 @@
1
+ import { PiboWebHttpError, readJsonBody, responseJson } from '../../web/http.js';
2
+ import { getPiboLoopService } from '../../loops/channel.js';
3
+ import { listLoopJobTemplates } from '../../loops/templates.js';
4
+ import { isPiboThinkingLevel } from '../../core/thinking.js';
5
+ import { normalizeLoopStopPolicy } from '../../loops/store.js';
6
+ import { isPiboRoomArchived } from './types/rooms.js';
7
+ const CHAT_WEB_API_PREFIX = '/api/chat';
8
+ function requireSameOriginJsonRequest(request) { const contentType = request.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase(); if (contentType !== 'application/json')
9
+ throw new PiboWebHttpError('Content-Type must be application/json', 415); const origin = request.headers.get('origin'); if (!origin)
10
+ throw new PiboWebHttpError('Origin header is required', 403); if (origin !== new URL(request.url).origin)
11
+ throw new PiboWebHttpError('Origin is not allowed', 403); }
12
+ function accessDenied(error) { throw new PiboWebHttpError(error instanceof Error ? error.message : 'Access denied', 403); }
13
+ function normalizeString(value, field, options = {}) { if (value === undefined || value === null) {
14
+ if (options.required)
15
+ throw new PiboWebHttpError(`${field} is required`, 400);
16
+ return undefined;
17
+ } if (typeof value !== 'string')
18
+ throw new PiboWebHttpError(`${field} must be a string`, 400); const normalized = value.trim(); if (!normalized && options.required)
19
+ throw new PiboWebHttpError(`${field} is required`, 400); if (options.max && normalized.length > options.max)
20
+ throw new PiboWebHttpError(`${field} is too long`, 400); return normalized || undefined; }
21
+ function normalizeEnabled(value) { if (value === undefined)
22
+ return undefined; if (typeof value !== 'boolean')
23
+ throw new PiboWebHttpError('enabled must be a boolean', 400); return value; }
24
+ function normalizeMode(value, fallback) { if (value === undefined || value === null || value === '')
25
+ return fallback; if (value !== 'goal' && value !== 'ralph')
26
+ throw new PiboWebHttpError('mode must be goal or ralph', 400); return value; }
27
+ function normalizeMaxIterations(value) { if (value === undefined || value === null || value === '')
28
+ return undefined; if (typeof value !== 'number' || !Number.isInteger(value) || value < 1)
29
+ throw new PiboWebHttpError('maxIterations must be a positive integer', 400); return value; }
30
+ function normalizeTokenBudget(value) { if (value === undefined || value === null || value === '')
31
+ return undefined; if (typeof value !== 'number' || !Number.isInteger(value) || value < 1)
32
+ throw new PiboWebHttpError('tokenBudget must be a positive integer', 400); return value; }
33
+ function normalizeStopPolicy(value) { if (value === undefined || value === null)
34
+ return undefined; try {
35
+ return normalizeLoopStopPolicy(value);
36
+ }
37
+ catch (error) {
38
+ throw new PiboWebHttpError(error instanceof Error ? error.message : 'Invalid stopPolicy', 400);
39
+ } }
40
+ function normalizeModelOverride(value) { if (value === undefined || value === null)
41
+ return undefined; if (!value || typeof value !== 'object' || Array.isArray(value))
42
+ throw new PiboWebHttpError('modelOverride must be an object', 400); const raw = value; if (typeof raw.provider !== 'string' || typeof raw.id !== 'string')
43
+ throw new PiboWebHttpError('modelOverride must include provider and id', 400); const provider = raw.provider.trim(); const id = raw.id.trim(); if (!provider || !id)
44
+ throw new PiboWebHttpError('modelOverride must include provider and id', 400); return { provider, id }; }
45
+ function normalizeThinkingLevel(value) { if (value === undefined || value === null || value === '')
46
+ return undefined; if (typeof value !== 'string' || !isPiboThinkingLevel(value))
47
+ throw new PiboWebHttpError('thinkingLevel must be one of off, minimal, low, medium, high, xhigh, max', 400); return value; }
48
+ function normalizeFastMode(value) { if (value === undefined || value === null)
49
+ return undefined; if (typeof value !== 'boolean')
50
+ throw new PiboWebHttpError('fastMode must be a boolean', 400); return value; }
51
+ function normalizeTarget(value, options) { if (!value || typeof value !== 'object' || Array.isArray(value))
52
+ throw new PiboWebHttpError('target is required', 400); const raw = value; if (raw.kind === 'room') {
53
+ const roomId = normalizeString(raw.roomId, 'target.roomId', { required: true });
54
+ let room;
55
+ try {
56
+ room = options.roomService.requireRoom(roomId);
57
+ }
58
+ catch (error) {
59
+ accessDenied(error);
60
+ }
61
+ if (isPiboRoomArchived(room))
62
+ throw new PiboWebHttpError('Archived rooms are read-only', 403);
63
+ return { kind: 'room', roomId };
64
+ } if (raw.kind === 'default-chat') {
65
+ options.roomService.ensureDefaultRoom({ name: 'Shared Chat' });
66
+ return { kind: 'default-chat' };
67
+ } throw new PiboWebHttpError('target.kind must be room or default-chat', 400); }
68
+ function resolveProfile(context, fallback, value) { const requested = normalizeString(value, 'profile') ?? fallback; const profile = context.channelContext.getProfiles?.().find((item) => item.name === requested || item.aliases.includes(requested)); if (!profile)
69
+ throw new PiboWebHttpError(`Unknown profile: ${requested}`, 400); return profile.name; }
70
+ function jobResource(pathname) { const prefix = `${CHAT_WEB_API_PREFIX}/loops/jobs/`; if (!pathname.startsWith(prefix))
71
+ return undefined; const parts = pathname.slice(prefix.length).split('/').filter(Boolean).map((part) => decodeURIComponent(part)); if (!parts[0] || parts.length > 2)
72
+ return undefined; if (parts[1] && !['start', 'stop', 'cancel'].includes(parts[1]))
73
+ return undefined; return { id: parts[0], child: parts[1] }; }
74
+ function createPatch(body, options) { const patch = {}; if (body.mode !== undefined)
75
+ patch.mode = normalizeMode(body.mode, 'goal'); const name = normalizeString(body.name, 'name', { max: 120 }); if (body.name !== undefined && name !== undefined)
76
+ patch.name = name; if (body.description !== undefined)
77
+ patch.description = normalizeString(body.description, 'description', { max: 500 }); const enabled = normalizeEnabled(body.enabled); if (enabled !== undefined)
78
+ patch.enabled = enabled; if (body.target !== undefined)
79
+ patch.target = normalizeTarget(body.target, options); if (body.profile !== undefined)
80
+ patch.profile = resolveProfile(options.context, options.defaultProfile, body.profile); if (body.prompt !== undefined)
81
+ patch.prompt = normalizeString(body.prompt, 'prompt', { required: true, max: 20_000 }); if (body.maxIterations !== undefined)
82
+ patch.maxIterations = normalizeMaxIterations(body.maxIterations) ?? null; if (body.tokenBudget !== undefined)
83
+ patch.tokenBudget = normalizeTokenBudget(body.tokenBudget) ?? null; if (body.stopPolicy !== undefined)
84
+ patch.stopPolicy = normalizeStopPolicy(body.stopPolicy) ?? null; if (body.modelOverride !== undefined)
85
+ patch.modelOverride = normalizeModelOverride(body.modelOverride) ?? null; if (body.thinkingLevel !== undefined)
86
+ patch.thinkingLevel = normalizeThinkingLevel(body.thinkingLevel) ?? null; if (body.fastMode !== undefined)
87
+ patch.fastMode = normalizeFastMode(body.fastMode) ?? null; if (Object.keys(patch).length === 0)
88
+ throw new PiboWebHttpError('No Loop job update fields provided', 400); return patch; }
89
+ function serializeTarget(target) { return target.kind === 'room' ? target : { kind: 'default-chat' }; }
90
+ function serializeJob(job) { const { target, ...rest } = job; return { ...rest, target: serializeTarget(target) }; }
91
+ function serializeRun(run) { return run; }
92
+ export async function handleChatLoopApiRequest(options) {
93
+ const { request, loopStore } = options;
94
+ const url = new URL(request.url);
95
+ const legacyRalphRequest = url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/ralph`);
96
+ if (!legacyRalphRequest && !url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/loops`) && !url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/loop`))
97
+ return undefined;
98
+ const apiPath = url.pathname.replace(/^\/api\/chat\/(?:ralph|loop)(?=\/|$)/, `${CHAT_WEB_API_PREFIX}/loops`);
99
+ if (apiPath === `${CHAT_WEB_API_PREFIX}/loops/status` && request.method === 'GET')
100
+ return responseJson({ status: getPiboLoopService()?.status() ?? { enabled: false, ...loopStore.status() } });
101
+ if (apiPath === `${CHAT_WEB_API_PREFIX}/loops/conditions` && request.method === 'GET')
102
+ return responseJson({ conditions: options.context.channelContext.getLoopStopConditionInfos?.() ?? options.context.channelContext.getCapabilityCatalog?.().loopStopConditions ?? [] });
103
+ if (apiPath === `${CHAT_WEB_API_PREFIX}/loops/templates` && request.method === 'GET')
104
+ return responseJson({ templates: listLoopJobTemplates() });
105
+ if (apiPath === `${CHAT_WEB_API_PREFIX}/loops/jobs` && request.method === 'GET')
106
+ return responseJson({ jobs: loopStore.listJobs({ includeDisabled: url.searchParams.get('includeDisabled') === 'true' }).map(serializeJob) });
107
+ if (apiPath === `${CHAT_WEB_API_PREFIX}/loops/jobs` && request.method === 'POST') {
108
+ requireSameOriginJsonRequest(request);
109
+ const body = await readJsonBody(request);
110
+ const mode = normalizeMode(body.mode, legacyRalphRequest ? 'ralph' : 'goal');
111
+ const tokenBudget = normalizeTokenBudget(body.tokenBudget);
112
+ if (mode === 'ralph' && tokenBudget !== undefined)
113
+ throw new PiboWebHttpError('tokenBudget is only available for goal mode', 400);
114
+ const job = loopStore.createJob({ mode, name: normalizeString(body.name, 'name', { max: 120 }), description: normalizeString(body.description, 'description', { max: 500 }), enabled: normalizeEnabled(body.enabled), target: normalizeTarget(body.target, options), profile: resolveProfile(options.context, options.defaultProfile, body.profile), prompt: normalizeString(body.prompt, 'prompt', { required: true, max: 20_000 }), maxIterations: normalizeMaxIterations(body.maxIterations), tokenBudget, stopPolicy: normalizeStopPolicy(body.stopPolicy), modelOverride: normalizeModelOverride(body.modelOverride), thinkingLevel: normalizeThinkingLevel(body.thinkingLevel), fastMode: normalizeFastMode(body.fastMode) });
115
+ return responseJson({ job: serializeJob(job) }, { status: 201 });
116
+ }
117
+ if (apiPath === `${CHAT_WEB_API_PREFIX}/loops/runs` && request.method === 'GET') {
118
+ const jobId = url.searchParams.get('jobId') || undefined;
119
+ const limit = Number(url.searchParams.get('limit') ?? '100');
120
+ if (jobId && !loopStore.getJob(jobId))
121
+ throw new PiboWebHttpError('Loop job not found', 404);
122
+ return responseJson({ runs: loopStore.listRuns({ jobId, limit: Number.isFinite(limit) ? limit : 100 }).map(serializeRun) });
123
+ }
124
+ const resource = jobResource(apiPath);
125
+ if (!resource)
126
+ return undefined;
127
+ if (resource.child && request.method === 'POST') {
128
+ requireSameOriginJsonRequest(request);
129
+ const service = getPiboLoopService();
130
+ if (!service)
131
+ throw new PiboWebHttpError('Loop service is not running', 503);
132
+ if (resource.child === 'start') {
133
+ const run = await service.startJob(resource.id);
134
+ if (!run)
135
+ throw new PiboWebHttpError('Loop job not found, already running, or stopped by a before-run condition', 404);
136
+ return responseJson({ run: serializeRun(run) }, { status: 202 });
137
+ }
138
+ if (resource.child === 'stop') {
139
+ const job = service.stopJob(resource.id);
140
+ if (!job)
141
+ throw new PiboWebHttpError('Loop job not found', 404);
142
+ return responseJson({ job: serializeJob(job) });
143
+ }
144
+ const job = await service.cancelJob(resource.id);
145
+ if (!job)
146
+ throw new PiboWebHttpError('Loop job not found', 404);
147
+ return responseJson({ job: serializeJob(job) });
148
+ }
149
+ if (resource.child)
150
+ return undefined;
151
+ if (request.method === 'GET') {
152
+ const job = loopStore.getJob(resource.id);
153
+ if (!job)
154
+ throw new PiboWebHttpError('Loop job not found', 404);
155
+ return responseJson({ job: serializeJob(job) });
156
+ }
157
+ if (request.method === 'PATCH') {
158
+ requireSameOriginJsonRequest(request);
159
+ const body = await readJsonBody(request);
160
+ const patch = createPatch(body, options);
161
+ const existing = loopStore.getJob(resource.id);
162
+ if (!existing)
163
+ throw new PiboWebHttpError('Loop job not found', 404);
164
+ if ((patch.mode ?? existing.mode) === 'ralph' && patch.tokenBudget !== undefined && patch.tokenBudget !== null)
165
+ throw new PiboWebHttpError('tokenBudget is only available for goal mode', 400);
166
+ const job = loopStore.updateJob(resource.id, patch);
167
+ if (!job)
168
+ throw new PiboWebHttpError('Loop job not found', 404);
169
+ return responseJson({ job: serializeJob(job) });
170
+ }
171
+ if (request.method === 'DELETE') {
172
+ requireSameOriginJsonRequest(request);
173
+ return responseJson({ removed: loopStore.removeJob(resource.id) });
174
+ }
175
+ return undefined;
176
+ }
@@ -126,6 +126,7 @@ export async function buildTraceView(input) {
126
126
  title: createSessionTitle(input.session, metadata),
127
127
  },
128
128
  events: input.events,
129
+ turnTimings: input.turnTimings,
129
130
  transcriptEntries: allEntries,
130
131
  sessions: input.sessions.map((s) => ({
131
132
  id: s.id,
@@ -175,6 +176,7 @@ export function createTraceViewVersion(input) {
175
176
  const eventTail = input.events.at(-1);
176
177
  return createHash("sha1")
177
178
  .update(JSON.stringify({
179
+ traceProjection: "turn-timing-v2",
178
180
  session: {
179
181
  id: input.session.id,
180
182
  piSessionId: input.session.piSessionId,
@@ -33,9 +33,9 @@ import { ChatTimelineQueryService } from "./data/timeline-query-service.js";
33
33
  import { ChatProjectService } from "./data/project-service.js";
34
34
  import { PiboDataStore } from "../../data/pibo-store.js";
35
35
  import { createDefaultPiboCronStore } from "../../cron/store.js";
36
- import { createDefaultPiboRalphStore } from "../../ralph/store.js";
36
+ import { createDefaultPiboLoopStore } from "../../loops/store.js";
37
37
  import { handleChatCronApiRequest } from "./cron-api.js";
38
- import { handleChatRalphApiRequest } from "./ralph-api.js";
38
+ import { handleChatLoopApiRequest } from "./loop-api.js";
39
39
  import { prepareWebAnnotationMessageAttachments } from "../../web-annotations/attachments.js";
40
40
  import { createDefaultWebAnnotationStore } from "../../web-annotations/store.js";
41
41
  import { CHAT_WEB_MOUNT_PATH, isChatAppPath, responseBuiltChatAsset, responseBuiltChatPublicFile, responseChatAppShell, isVscodeAppPath, responseBuiltVscodeAsset, responseVscodeAppShell } from "./static-assets.js";
@@ -323,6 +323,7 @@ function createFastTraceV2Version(input) {
323
323
  .sort((left, right) => left.id.localeCompare(right.id));
324
324
  return createHash("sha1")
325
325
  .update(JSON.stringify({
326
+ traceProjection: "turn-timing-v2",
326
327
  session: {
327
328
  id: input.session.id,
328
329
  piSessionId: input.session.piSessionId,
@@ -3033,7 +3034,7 @@ export function createChatWebApp(options = {}) {
3033
3034
  agentStore: createAgentStore(options.agentStorePath),
3034
3035
  reliabilityStore: createReliabilityStore(options.reliabilityStorePath),
3035
3036
  cronStore: createDefaultPiboCronStore({ path: options.cronStorePath }),
3036
- ralphStore: createDefaultPiboRalphStore({ path: options.ralphStorePath }),
3037
+ loopStore: createDefaultPiboLoopStore({ path: options.ralphStorePath }),
3037
3038
  dataStore,
3038
3039
  ingestService: new ChatDataIngestService(dataStore),
3039
3040
  traceCache: new Map(),
@@ -3221,14 +3222,14 @@ export function createChatWebApp(options = {}) {
3221
3222
  if (response)
3222
3223
  return response;
3223
3224
  }
3224
- if (url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/ralph`)) {
3225
+ if (url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/loops`) || url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/loop`) || url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/ralph`)) {
3225
3226
  const webSession = await requireSession(request, context);
3226
- const response = await handleChatRalphApiRequest({
3227
+ const response = await handleChatLoopApiRequest({
3227
3228
  request,
3228
3229
  context,
3229
3230
  webSession,
3230
3231
  roomService: state.roomService,
3231
- ralphStore: state.ralphStore,
3232
+ loopStore: state.loopStore,
3232
3233
  defaultProfile,
3233
3234
  });
3234
3235
  if (response)
@@ -4266,6 +4267,7 @@ export function createChatWebApp(options = {}) {
4266
4267
  let metadataMs = 0;
4267
4268
  const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
4268
4269
  const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
4270
+ const turnTimings = state.timelineQuery.listMessageTurnTimings(selectedSession.id);
4269
4271
  const liveSnapshots = timelineCursor.kind === "tail" ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
4270
4272
  const metadataStartedAt = performance.now();
4271
4273
  const transcriptMetadata = timelineCursor.kind === "tail" || timelineCursor.kind === "transcript"
@@ -4328,6 +4330,7 @@ export function createChatWebApp(options = {}) {
4328
4330
  metadata: transcriptMetadata ?? {},
4329
4331
  transcriptEntries: history.entries,
4330
4332
  transcriptOrderOffset: history.startByte,
4333
+ turnTimings,
4331
4334
  includeRawEvents: false,
4332
4335
  latestStreamId,
4333
4336
  });
@@ -4360,6 +4363,7 @@ export function createChatWebApp(options = {}) {
4360
4363
  status: indexedSession?.status,
4361
4364
  metadata: transcriptMetadata ?? {},
4362
4365
  transcriptEntries,
4366
+ turnTimings,
4363
4367
  includeRawEvents: false,
4364
4368
  latestStreamId,
4365
4369
  });
@@ -4474,6 +4478,7 @@ export function createChatWebApp(options = {}) {
4474
4478
  const metadataMs = performance.now() - metadataStartedAt;
4475
4479
  const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
4476
4480
  const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
4481
+ const turnTimings = state.timelineQuery.listMessageTurnTimings(selectedSession.id);
4477
4482
  const liveSnapshots = beforeSequence === undefined ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
4478
4483
  const baseVersion = createTraceViewVersion({
4479
4484
  session: selectedSession,
@@ -4517,6 +4522,7 @@ export function createChatWebApp(options = {}) {
4517
4522
  events,
4518
4523
  status: indexedSession?.status,
4519
4524
  metadata,
4525
+ turnTimings,
4520
4526
  includeRawEvents: false,
4521
4527
  latestStreamId,
4522
4528
  });
@@ -4578,6 +4584,7 @@ export function createChatWebApp(options = {}) {
4578
4584
  sessions: ownedSessions,
4579
4585
  events: state.timelineQuery.listTraceEvents({ piboSessionId, beforeOrAtSequence: eventSequence, limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE }),
4580
4586
  status: indexedSession?.status,
4587
+ turnTimings: state.timelineQuery.listMessageTurnTimings(piboSessionId),
4581
4588
  });
4582
4589
  return responseJson(trace);
4583
4590
  }