forge-openclaw-plugin 0.2.23 → 0.2.25

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 (84) hide show
  1. package/README.md +13 -0
  2. package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
  3. package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
  4. package/dist/assets/index-CFCKDIMH.js +67 -0
  5. package/dist/assets/index-CFCKDIMH.js.map +1 -0
  6. package/dist/assets/index-ZPY6U1TU.css +1 -0
  7. package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
  8. package/dist/assets/motion-DvkU14p-.js.map +1 -0
  9. package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
  10. package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
  11. package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
  12. package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
  13. package/dist/assets/vendor-D9PTEPSB.js +824 -0
  14. package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
  15. package/dist/assets/viz-Cqb6s--o.js +34 -0
  16. package/dist/assets/viz-Cqb6s--o.js.map +1 -0
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/parity.d.ts +1 -1
  19. package/dist/openclaw/parity.js +29 -0
  20. package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
  21. package/dist/openclaw/plugin-entry-shared.js +7 -4
  22. package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
  23. package/dist/openclaw/routes.js +236 -0
  24. package/dist/openclaw/session-bootstrap.d.ts +78 -0
  25. package/dist/openclaw/session-bootstrap.js +240 -0
  26. package/dist/openclaw/tools.js +279 -3
  27. package/dist/server/app.js +855 -19
  28. package/dist/server/connectors/box-registry.js +257 -0
  29. package/dist/server/db.js +2 -0
  30. package/dist/server/discovery-advertiser.js +114 -0
  31. package/dist/server/health.js +39 -11
  32. package/dist/server/index.js +4 -0
  33. package/dist/server/managers/platform/llm-manager.js +40 -4
  34. package/dist/server/managers/platform/openai-responses-provider.js +129 -19
  35. package/dist/server/movement.js +2935 -0
  36. package/dist/server/openapi.js +628 -5
  37. package/dist/server/psyche-types.js +15 -1
  38. package/dist/server/questionnaire-flow.js +552 -0
  39. package/dist/server/questionnaire-seeds.js +853 -0
  40. package/dist/server/questionnaire-types.js +340 -0
  41. package/dist/server/repositories/ai-connectors.js +944 -0
  42. package/dist/server/repositories/ai-processors.js +547 -0
  43. package/dist/server/repositories/diagnostic-logs.js +57 -4
  44. package/dist/server/repositories/entity-ownership.js +9 -1
  45. package/dist/server/repositories/habits.js +77 -9
  46. package/dist/server/repositories/model-settings.js +216 -0
  47. package/dist/server/repositories/notes.js +57 -15
  48. package/dist/server/repositories/preferences.js +124 -0
  49. package/dist/server/repositories/questionnaires.js +1338 -0
  50. package/dist/server/repositories/rewards.js +2 -2
  51. package/dist/server/repositories/settings.js +108 -12
  52. package/dist/server/repositories/surface-layouts.js +76 -0
  53. package/dist/server/repositories/wiki-memory.js +5 -1
  54. package/dist/server/services/entity-crud.js +81 -2
  55. package/dist/server/services/openai-codex-oauth.js +153 -0
  56. package/dist/server/services/psyche-observation-calendar.js +46 -0
  57. package/dist/server/types.js +492 -3
  58. package/dist/server/watch-mobile.js +562 -0
  59. package/dist/server/web.js +9 -2
  60. package/openclaw.plugin.json +1 -1
  61. package/package.json +6 -1
  62. package/server/migrations/024_questionnaires.sql +96 -0
  63. package/server/migrations/025_ai_model_connections.sql +26 -0
  64. package/server/migrations/026_custom_theme_settings.sql +2 -0
  65. package/server/migrations/027_ai_processors.sql +31 -0
  66. package/server/migrations/028_movement_domain.sql +136 -0
  67. package/server/migrations/029_watch_micro_capture.sql +23 -0
  68. package/server/migrations/030_surface_layouts.sql +5 -0
  69. package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  70. package/server/migrations/032_ai_connectors.sql +44 -0
  71. package/server/migrations/033_movement_trip_point_sync.sql +36 -0
  72. package/server/migrations/034_movement_segment_sync.sql +49 -0
  73. package/skills/forge-openclaw/SKILL.md +12 -1
  74. package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
  75. package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
  76. package/dist/assets/index-Ch_xeZ2u.js +0 -63
  77. package/dist/assets/index-Ch_xeZ2u.js.map +0 -1
  78. package/dist/assets/index-DvVM7K6j.css +0 -1
  79. package/dist/assets/motion-D4sZgCHd.js.map +0 -1
  80. package/dist/assets/vendor-De38P6YR.js +0 -729
  81. package/dist/assets/vendor-De38P6YR.js.map +0 -1
  82. package/dist/assets/viz-C6hfyqzu.js +0 -34
  83. package/dist/assets/viz-C6hfyqzu.js.map +0 -1
  84. package/skills/forge-openclaw/cron_jobs.md +0 -395
@@ -0,0 +1,547 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { execFile as execFileCallback } from "node:child_process";
5
+ import path from "node:path";
6
+ import { getDatabase } from "../db.js";
7
+ import { aiProcessorLinkSchema, aiProcessorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiProcessorSchema, surfaceProcessorGraphPayloadSchema, updateAiProcessorSchema } from "../types.js";
8
+ import { FORGE_DEFAULT_AGENT_ID, getAiModelConnectionById, listAiModelConnections, readModelConnectionCredential } from "./model-settings.js";
9
+ import { getSettings } from "./settings.js";
10
+ const MAX_RUN_HISTORY = 12;
11
+ const MAX_TOOL_STEPS = 6;
12
+ const execFile = promisify(execFileCallback);
13
+ function parseJson(value, fallback) {
14
+ try {
15
+ return value ? JSON.parse(value) : fallback;
16
+ }
17
+ catch {
18
+ return fallback;
19
+ }
20
+ }
21
+ function slugifySegment(value) {
22
+ const normalized = value
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, "-")
26
+ .replace(/^-+|-+$/g, "");
27
+ return normalized || "processor";
28
+ }
29
+ function buildProcessorSlug(title, id) {
30
+ return `${slugifySegment(title)}-${id.slice(-6)}`;
31
+ }
32
+ function processorWidgetId(processorId) {
33
+ return `aiproc:${processorId}`;
34
+ }
35
+ function processorIdFromNodeId(nodeId) {
36
+ return nodeId.startsWith("aiproc:") ? nodeId.slice("aiproc:".length) : null;
37
+ }
38
+ function resolveAllowedPath(inputPath) {
39
+ const candidate = path.resolve(process.cwd(), inputPath);
40
+ const workspaceRoot = process.cwd();
41
+ if (candidate !== workspaceRoot &&
42
+ !candidate.startsWith(`${workspaceRoot}${path.sep}`)) {
43
+ throw new Error("Machine access is restricted to the Forge workspace root.");
44
+ }
45
+ return candidate;
46
+ }
47
+ function tryParseStructuredAgentResponse(value) {
48
+ try {
49
+ return JSON.parse(value);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function executeMachineTool(processor, tool, args) {
56
+ if (tool === "machine_read_file") {
57
+ if (!processor.machineAccess.read) {
58
+ throw new Error("Read access is disabled for this processor.");
59
+ }
60
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
61
+ if (!targetPath) {
62
+ throw new Error("machine_read_file requires a string path.");
63
+ }
64
+ const content = await readFile(targetPath, "utf8");
65
+ return {
66
+ path: targetPath,
67
+ content
68
+ };
69
+ }
70
+ if (tool === "machine_write_file") {
71
+ if (!processor.machineAccess.write) {
72
+ throw new Error("Write access is disabled for this processor.");
73
+ }
74
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
75
+ if (!targetPath) {
76
+ throw new Error("machine_write_file requires a string path.");
77
+ }
78
+ if (typeof args.content !== "string") {
79
+ throw new Error("machine_write_file requires string content.");
80
+ }
81
+ await writeFile(targetPath, args.content, "utf8");
82
+ return {
83
+ path: targetPath,
84
+ bytesWritten: Buffer.byteLength(args.content, "utf8")
85
+ };
86
+ }
87
+ if (!processor.machineAccess.exec) {
88
+ throw new Error("Exec access is disabled for this processor.");
89
+ }
90
+ if (typeof args.command !== "string" || args.command.trim().length === 0) {
91
+ throw new Error("machine_exec requires a command string.");
92
+ }
93
+ const cwd = typeof args.cwd === "string" && args.cwd.trim().length > 0
94
+ ? resolveAllowedPath(args.cwd)
95
+ : process.cwd();
96
+ const result = await execFile("zsh", ["-lc", args.command], {
97
+ cwd,
98
+ timeout: 15_000,
99
+ maxBuffer: 256_000
100
+ });
101
+ return {
102
+ cwd,
103
+ stdout: result.stdout.trim(),
104
+ stderr: result.stderr.trim()
105
+ };
106
+ }
107
+ async function runProcessorAgent(processor, agent, fullPrompt, services) {
108
+ if (!agent.profile) {
109
+ return "No model connection is configured for this agent yet.";
110
+ }
111
+ const toolNames = [
112
+ processor.machineAccess.read ? "machine_read_file(path)" : null,
113
+ processor.machineAccess.write
114
+ ? "machine_write_file(path, content)"
115
+ : null,
116
+ processor.machineAccess.exec ? "machine_exec(command, cwd?)" : null
117
+ ].filter(Boolean);
118
+ if (toolNames.length === 0) {
119
+ const result = await services.llm.runTextPrompt(agent.profile, {
120
+ explicitApiKey: agent.explicitApiKey,
121
+ systemPrompt: "You are an AI processor inside Forge. Follow the prompt flow exactly, use the linked context carefully, and return only the final output for your assigned agent.",
122
+ prompt: fullPrompt
123
+ });
124
+ return result.outputText.trim();
125
+ }
126
+ const transcript = [];
127
+ for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
128
+ const result = await services.llm.runTextPrompt(agent.profile, {
129
+ explicitApiKey: agent.explicitApiKey,
130
+ systemPrompt: [
131
+ "You are an AI processor inside Forge.",
132
+ "You may use machine tools when they are enabled.",
133
+ `Available tools: ${toolNames.join(", ")}.`,
134
+ "Return strict JSON only.",
135
+ 'For a final answer, return {"action":"final","text":"..."}',
136
+ 'To call a tool, return {"action":"tool","tool":"machine_exec","args":{...}}'
137
+ ].join(" "),
138
+ prompt: [
139
+ fullPrompt,
140
+ transcript.length > 0
141
+ ? `Tool transcript:\n${transcript.join("\n\n")}`
142
+ : ""
143
+ ]
144
+ .filter(Boolean)
145
+ .join("\n\n")
146
+ });
147
+ const structured = tryParseStructuredAgentResponse(result.outputText.trim());
148
+ if (!structured || structured.action === "final") {
149
+ return structured?.text?.trim() || result.outputText.trim();
150
+ }
151
+ const toolResult = await executeMachineTool(processor, structured.tool, structured.args);
152
+ transcript.push(`Tool call ${structured.tool}: ${JSON.stringify(structured.args)}`, `Tool result: ${JSON.stringify(toolResult)}`);
153
+ }
154
+ return "Processor stopped after reaching the maximum tool step count.";
155
+ }
156
+ function mapProcessor(row) {
157
+ return aiProcessorSchema.parse({
158
+ id: row.id,
159
+ slug: row.slug,
160
+ surfaceId: row.surface_id,
161
+ title: row.title,
162
+ promptFlow: row.prompt_flow,
163
+ contextInput: row.context_input,
164
+ toolConfig: parseJson(row.tool_config_json, []),
165
+ agentIds: parseJson(row.agent_ids_json, []),
166
+ agentConfigs: parseJson(row.agent_config_json, []),
167
+ triggerMode: row.trigger_mode,
168
+ cronExpression: row.cron_expression,
169
+ machineAccess: parseJson(row.machine_access_json, {
170
+ read: false,
171
+ write: false,
172
+ exec: false
173
+ }),
174
+ endpointEnabled: row.endpoint_enabled === 1,
175
+ lastRunAt: row.last_run_at,
176
+ lastRunStatus: row.last_run_status,
177
+ lastRunOutput: parseJson(row.last_run_output_json, null),
178
+ runHistory: parseJson(row.run_history_json, []),
179
+ createdAt: row.created_at,
180
+ updatedAt: row.updated_at
181
+ });
182
+ }
183
+ function mapLink(row) {
184
+ return aiProcessorLinkSchema.parse({
185
+ id: row.id,
186
+ surfaceId: row.surface_id,
187
+ sourceWidgetId: row.source_widget_id,
188
+ targetProcessorId: row.target_processor_id,
189
+ accessMode: row.access_mode,
190
+ capabilityMode: row.capability_mode,
191
+ metadata: parseJson(row.metadata_json, {}),
192
+ createdAt: row.created_at,
193
+ updatedAt: row.updated_at
194
+ });
195
+ }
196
+ export function listAiProcessors(surfaceId) {
197
+ const rows = surfaceId
198
+ ? (getDatabase()
199
+ .prepare(`SELECT * FROM ai_processors WHERE surface_id = ? ORDER BY created_at ASC`)
200
+ .all(surfaceId) ?? [])
201
+ : (getDatabase()
202
+ .prepare(`SELECT * FROM ai_processors ORDER BY created_at ASC`)
203
+ .all() ?? []);
204
+ return rows.map(mapProcessor);
205
+ }
206
+ export function getAiProcessorById(processorId) {
207
+ const row = getDatabase()
208
+ .prepare(`SELECT * FROM ai_processors WHERE id = ?`)
209
+ .get(processorId);
210
+ return row ? mapProcessor(row) : null;
211
+ }
212
+ export function getAiProcessorBySlug(slug) {
213
+ const row = getDatabase()
214
+ .prepare(`SELECT * FROM ai_processors WHERE slug = ?`)
215
+ .get(slug);
216
+ return row ? mapProcessor(row) : null;
217
+ }
218
+ export function listAiProcessorLinks(surfaceId) {
219
+ const rows = surfaceId
220
+ ? (getDatabase()
221
+ .prepare(`SELECT * FROM ai_processor_links WHERE surface_id = ? ORDER BY created_at ASC`)
222
+ .all(surfaceId) ?? [])
223
+ : (getDatabase()
224
+ .prepare(`SELECT * FROM ai_processor_links ORDER BY created_at ASC`)
225
+ .all() ?? []);
226
+ return rows.map(mapLink);
227
+ }
228
+ export function getSurfaceProcessorGraph(surfaceId) {
229
+ return surfaceProcessorGraphPayloadSchema.parse({
230
+ surfaceId,
231
+ processors: listAiProcessors(surfaceId),
232
+ links: listAiProcessorLinks(surfaceId)
233
+ });
234
+ }
235
+ export function createAiProcessor(input) {
236
+ const parsed = createAiProcessorSchema.parse(input);
237
+ const now = new Date().toISOString();
238
+ const id = `aip_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
239
+ const slug = buildProcessorSlug(parsed.title, id);
240
+ getDatabase()
241
+ .prepare(`INSERT INTO ai_processors (
242
+ id, slug, surface_id, title, prompt_flow, context_input, tool_config_json, agent_ids_json, agent_config_json, trigger_mode, cron_expression, machine_access_json, endpoint_enabled, last_run_at, last_run_status, last_run_output_json, run_history_json, created_at, updated_at
243
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
244
+ .run(id, slug, parsed.surfaceId, parsed.title, parsed.promptFlow, parsed.contextInput, JSON.stringify(parsed.toolConfig), JSON.stringify(parsed.agentIds), JSON.stringify(parsed.agentConfigs), parsed.triggerMode, parsed.cronExpression, JSON.stringify(parsed.machineAccess), parsed.endpointEnabled ? 1 : 0, null, "idle", null, "[]", now, now);
245
+ return getAiProcessorById(id);
246
+ }
247
+ export function updateAiProcessor(processorId, patch) {
248
+ const current = getAiProcessorById(processorId);
249
+ if (!current) {
250
+ return null;
251
+ }
252
+ const parsed = updateAiProcessorSchema.parse(patch);
253
+ const next = {
254
+ ...current,
255
+ ...parsed,
256
+ slug: parsed.title && parsed.title !== current.title
257
+ ? buildProcessorSlug(parsed.title, current.id)
258
+ : current.slug,
259
+ machineAccess: {
260
+ ...current.machineAccess,
261
+ ...(parsed.machineAccess ?? {})
262
+ }
263
+ };
264
+ const now = new Date().toISOString();
265
+ getDatabase()
266
+ .prepare(`UPDATE ai_processors
267
+ SET slug = ?, title = ?, prompt_flow = ?, context_input = ?, tool_config_json = ?, agent_ids_json = ?, agent_config_json = ?, trigger_mode = ?, cron_expression = ?, machine_access_json = ?, endpoint_enabled = ?, updated_at = ?
268
+ WHERE id = ?`)
269
+ .run(next.slug, next.title, next.promptFlow, next.contextInput, JSON.stringify(next.toolConfig), JSON.stringify(next.agentIds), JSON.stringify(next.agentConfigs), next.triggerMode, next.cronExpression, JSON.stringify(next.machineAccess), next.endpointEnabled ? 1 : 0, now, processorId);
270
+ return getAiProcessorById(processorId);
271
+ }
272
+ export function deleteAiProcessor(processorId) {
273
+ const current = getAiProcessorById(processorId);
274
+ if (!current) {
275
+ return null;
276
+ }
277
+ getDatabase().prepare(`DELETE FROM ai_processors WHERE id = ?`).run(processorId);
278
+ return current;
279
+ }
280
+ function assertProcessorGraphEdgeIsValid(input) {
281
+ const sourceProcessorId = processorIdFromNodeId(input.sourceWidgetId);
282
+ if (!sourceProcessorId) {
283
+ return;
284
+ }
285
+ if (sourceProcessorId === input.targetProcessorId) {
286
+ throw new Error("AI processor links cannot point a processor to itself.");
287
+ }
288
+ const links = listAiProcessorLinks(input.surfaceId);
289
+ const adjacency = new Map();
290
+ for (const link of links) {
291
+ const upstreamProcessorId = processorIdFromNodeId(link.sourceWidgetId);
292
+ if (!upstreamProcessorId) {
293
+ continue;
294
+ }
295
+ const current = adjacency.get(upstreamProcessorId) ?? new Set();
296
+ current.add(link.targetProcessorId);
297
+ adjacency.set(upstreamProcessorId, current);
298
+ }
299
+ const nextTargets = adjacency.get(sourceProcessorId) ?? new Set();
300
+ nextTargets.add(input.targetProcessorId);
301
+ adjacency.set(sourceProcessorId, nextTargets);
302
+ const seen = new Set();
303
+ const stack = [input.targetProcessorId];
304
+ while (stack.length > 0) {
305
+ const current = stack.pop();
306
+ if (current === sourceProcessorId) {
307
+ throw new Error("This link would create a processor cycle.");
308
+ }
309
+ if (seen.has(current)) {
310
+ continue;
311
+ }
312
+ seen.add(current);
313
+ for (const next of adjacency.get(current) ?? []) {
314
+ stack.push(next);
315
+ }
316
+ }
317
+ }
318
+ export function createAiProcessorLink(input) {
319
+ const parsed = createAiProcessorLinkSchema.parse(input);
320
+ assertProcessorGraphEdgeIsValid(parsed);
321
+ const existing = listAiProcessorLinks(parsed.surfaceId).find((link) => link.sourceWidgetId === parsed.sourceWidgetId &&
322
+ link.targetProcessorId === parsed.targetProcessorId);
323
+ const now = new Date().toISOString();
324
+ const id = existing?.id ?? `ail_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
325
+ getDatabase()
326
+ .prepare(`INSERT INTO ai_processor_links (
327
+ id, surface_id, source_widget_id, target_processor_id, access_mode, capability_mode, metadata_json, created_at, updated_at
328
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
329
+ ON CONFLICT(id) DO UPDATE SET
330
+ access_mode = excluded.access_mode,
331
+ capability_mode = excluded.capability_mode,
332
+ metadata_json = excluded.metadata_json,
333
+ updated_at = excluded.updated_at`)
334
+ .run(id, parsed.surfaceId, parsed.sourceWidgetId, parsed.targetProcessorId, parsed.accessMode, parsed.capabilityMode, JSON.stringify(parsed.metadata), existing?.createdAt ?? now, now);
335
+ return listAiProcessorLinks(parsed.surfaceId).find((entry) => entry.id === id);
336
+ }
337
+ export function deleteAiProcessorLink(linkId) {
338
+ const existing = listAiProcessorLinks().find((entry) => entry.id === linkId);
339
+ if (!existing) {
340
+ return null;
341
+ }
342
+ getDatabase().prepare(`DELETE FROM ai_processor_links WHERE id = ?`).run(linkId);
343
+ return existing;
344
+ }
345
+ function resolveProcessorAgentProfiles(processor, secrets) {
346
+ const allConnections = listAiModelConnections();
347
+ const requestedAgentIds = processor.agentIds.length > 0 ? processor.agentIds : [FORGE_DEFAULT_AGENT_ID];
348
+ const configByAgentId = new Map(processor.agentConfigs.map((config) => [config.agentId, config]));
349
+ const settings = getSettings();
350
+ return requestedAgentIds.map((agentId) => {
351
+ const override = configByAgentId.get(agentId) ?? null;
352
+ let connection = (override?.connectionId
353
+ ? getAiModelConnectionById(override.connectionId)
354
+ : null) ??
355
+ allConnections.find((entry) => entry.agentId === agentId) ??
356
+ null;
357
+ if (agentId === FORGE_DEFAULT_AGENT_ID) {
358
+ const selected = settings.modelSettings.forgeAgent.basicChat.connectionId;
359
+ connection =
360
+ (override?.connectionId ? connection : null) ??
361
+ (selected ? getAiModelConnectionById(selected) : null);
362
+ }
363
+ if (!connection) {
364
+ return {
365
+ agentId,
366
+ agentLabel: agentId === FORGE_DEFAULT_AGENT_ID ? "Forge Agent" : agentId,
367
+ profile: null,
368
+ explicitApiKey: null
369
+ };
370
+ }
371
+ const credential = readModelConnectionCredential(connection.id, secrets);
372
+ const explicitApiKey = credential?.kind === "api_key"
373
+ ? credential.apiKey
374
+ : credential?.kind === "oauth"
375
+ ? credential.access
376
+ : null;
377
+ return {
378
+ agentId,
379
+ agentLabel: agentId === FORGE_DEFAULT_AGENT_ID
380
+ ? "Forge Agent"
381
+ : connection.agentLabel,
382
+ profile: {
383
+ provider: connection.provider,
384
+ baseUrl: connection.baseUrl,
385
+ model: override?.model?.trim() || connection.model,
386
+ systemPrompt: "",
387
+ secretId: null,
388
+ metadata: {}
389
+ },
390
+ explicitApiKey
391
+ };
392
+ });
393
+ }
394
+ function writeProcessorRunState(processor, input) {
395
+ const nextHistory = [
396
+ input.runEntry,
397
+ ...processor.runHistory.filter((entry) => entry.id !== input.runEntry.id)
398
+ ].slice(0, MAX_RUN_HISTORY);
399
+ getDatabase()
400
+ .prepare(`UPDATE ai_processors
401
+ SET last_run_at = ?, last_run_status = ?, last_run_output_json = ?, run_history_json = ?, updated_at = ?
402
+ WHERE id = ?`)
403
+ .run(input.lastRunAt, input.lastRunStatus, input.lastRunOutput ? JSON.stringify(input.lastRunOutput) : null, JSON.stringify(nextHistory), new Date().toISOString(), processor.id);
404
+ }
405
+ async function executeAiProcessor(processorId, input, services, state) {
406
+ if (state.cache.has(processorId)) {
407
+ return state.cache.get(processorId);
408
+ }
409
+ if (state.active.has(processorId)) {
410
+ throw new Error("Processor graph contains a cycle.");
411
+ }
412
+ const processor = getAiProcessorById(processorId);
413
+ if (!processor) {
414
+ throw new Error("AI processor not found.");
415
+ }
416
+ state.active.add(processorId);
417
+ const parsed = runAiProcessorSchema.parse(input);
418
+ const runEntryId = `air_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
419
+ const runStartedAt = new Date().toISOString();
420
+ writeProcessorRunState(processor, {
421
+ lastRunAt: runStartedAt,
422
+ lastRunStatus: "running",
423
+ lastRunOutput: processor.lastRunOutput,
424
+ runEntry: {
425
+ id: runEntryId,
426
+ trigger: state.trigger,
427
+ startedAt: runStartedAt,
428
+ completedAt: null,
429
+ status: "running",
430
+ input: parsed.input,
431
+ output: null,
432
+ error: null
433
+ }
434
+ });
435
+ try {
436
+ const links = listAiProcessorLinks(processor.surfaceId).filter((link) => link.targetProcessorId === processor.id);
437
+ const upstreamOutputs = [];
438
+ const linkedContext = [];
439
+ for (const link of links) {
440
+ const sourceProcessorId = processorIdFromNodeId(link.sourceWidgetId);
441
+ if (sourceProcessorId) {
442
+ const upstream = await executeAiProcessor(sourceProcessorId, {
443
+ input: parsed.input,
444
+ context: parsed.context,
445
+ widgetSnapshots: parsed.widgetSnapshots
446
+ }, services, state);
447
+ upstreamOutputs.push({
448
+ processorId: sourceProcessorId,
449
+ title: upstream.processor.title,
450
+ output: upstream.output
451
+ });
452
+ linkedContext.push(`Upstream processor ${upstream.processor.title} provided ${link.capabilityMode} access (${link.accessMode}).`);
453
+ continue;
454
+ }
455
+ const snapshot = parsed.widgetSnapshots[link.sourceWidgetId];
456
+ linkedContext.push([
457
+ `Linked widget ${link.sourceWidgetId} offers ${link.capabilityMode} access (${link.accessMode}).`,
458
+ snapshot !== undefined
459
+ ? `Snapshot: ${JSON.stringify(snapshot)}`
460
+ : `Metadata: ${JSON.stringify(link.metadata)}`
461
+ ].join(" "));
462
+ }
463
+ const fullPrompt = [
464
+ processor.promptFlow.trim(),
465
+ processor.contextInput.trim()
466
+ ? `Processor context:\n${processor.contextInput.trim()}`
467
+ : "",
468
+ linkedContext.length > 0
469
+ ? `Linked capabilities:\n${linkedContext.join("\n")}`
470
+ : "",
471
+ upstreamOutputs.length > 0
472
+ ? `Upstream processor outputs:\n${upstreamOutputs
473
+ .map((entry) => `${entry.title} (${entry.processorId})\n${entry.output.concatenated}`)
474
+ .join("\n\n")}`
475
+ : "",
476
+ parsed.input.trim() ? `Runtime input:\n${parsed.input.trim()}` : "",
477
+ Object.keys(parsed.context).length > 0
478
+ ? `Structured context:\n${JSON.stringify(parsed.context, null, 2)}`
479
+ : ""
480
+ ]
481
+ .filter(Boolean)
482
+ .join("\n\n");
483
+ const agents = resolveProcessorAgentProfiles(processor, services.secrets);
484
+ const outputsByAgent = {};
485
+ await Promise.all(agents.map(async (agent) => {
486
+ outputsByAgent[agent.agentLabel] = await runProcessorAgent(processor, agent, fullPrompt, {
487
+ llm: services.llm
488
+ });
489
+ }));
490
+ const output = {
491
+ concatenated: Object.entries(outputsByAgent)
492
+ .map(([agentLabel, text]) => `${agentLabel}\n${text}`.trim())
493
+ .join("\n\n"),
494
+ byAgent: outputsByAgent
495
+ };
496
+ const completedAt = new Date().toISOString();
497
+ writeProcessorRunState(processor, {
498
+ lastRunAt: completedAt,
499
+ lastRunStatus: "completed",
500
+ lastRunOutput: output,
501
+ runEntry: {
502
+ id: runEntryId,
503
+ trigger: state.trigger,
504
+ startedAt: runStartedAt,
505
+ completedAt,
506
+ status: "completed",
507
+ input: parsed.input,
508
+ output,
509
+ error: null
510
+ }
511
+ });
512
+ const result = {
513
+ processor: getAiProcessorById(processor.id),
514
+ output
515
+ };
516
+ state.cache.set(processorId, result);
517
+ state.active.delete(processorId);
518
+ return result;
519
+ }
520
+ catch (error) {
521
+ const failedAt = new Date().toISOString();
522
+ writeProcessorRunState(processor, {
523
+ lastRunAt: failedAt,
524
+ lastRunStatus: "failed",
525
+ lastRunOutput: processor.lastRunOutput,
526
+ runEntry: {
527
+ id: runEntryId,
528
+ trigger: state.trigger,
529
+ startedAt: runStartedAt,
530
+ completedAt: failedAt,
531
+ status: "failed",
532
+ input: parsed.input,
533
+ output: null,
534
+ error: error instanceof Error ? error.message : "Processor run failed."
535
+ }
536
+ });
537
+ state.active.delete(processorId);
538
+ throw error;
539
+ }
540
+ }
541
+ export async function runAiProcessor(processorId, input, services, options = {}) {
542
+ return await executeAiProcessor(processorId, input, services, {
543
+ cache: new Map(),
544
+ active: new Set(),
545
+ trigger: options.trigger ?? "manual"
546
+ });
547
+ }
@@ -9,6 +9,10 @@ const MAX_STRING_LENGTH = 4_000;
9
9
  const MAX_ARRAY_ITEMS = 24;
10
10
  const MAX_OBJECT_KEYS = 40;
11
11
  const MAX_DEPTH = 4;
12
+ const LIST_MAX_STRING_LENGTH = 600;
13
+ const LIST_MAX_ARRAY_ITEMS = 8;
14
+ const LIST_MAX_OBJECT_KEYS = 16;
15
+ const LIST_MAX_DEPTH = 2;
12
16
  let nextRetentionSweepAt = 0;
13
17
  function nowIso() {
14
18
  return new Date().toISOString();
@@ -77,7 +81,54 @@ function sanitizeDetails(details) {
77
81
  sanitizeDiagnosticValue(value)
78
82
  ]));
79
83
  }
80
- function mapRow(row) {
84
+ function compactDiagnosticValue(value, depth = 0) {
85
+ if (value === null ||
86
+ typeof value === "boolean" ||
87
+ typeof value === "number") {
88
+ return value;
89
+ }
90
+ if (typeof value === "string") {
91
+ return value.length > LIST_MAX_STRING_LENGTH
92
+ ? `${value.slice(0, LIST_MAX_STRING_LENGTH)}…`
93
+ : value;
94
+ }
95
+ if (typeof value === "bigint") {
96
+ return value.toString();
97
+ }
98
+ if (typeof value === "function" || typeof value === "symbol") {
99
+ return String(value);
100
+ }
101
+ if (value instanceof Date) {
102
+ return value.toISOString();
103
+ }
104
+ if (depth >= LIST_MAX_DEPTH) {
105
+ if (Array.isArray(value)) {
106
+ return `[Array(${value.length})]`;
107
+ }
108
+ return "[Object]";
109
+ }
110
+ if (Array.isArray(value)) {
111
+ return value
112
+ .slice(0, LIST_MAX_ARRAY_ITEMS)
113
+ .map((entry) => compactDiagnosticValue(entry, depth + 1));
114
+ }
115
+ if (value && typeof value === "object") {
116
+ const entries = Object.entries(value).slice(0, LIST_MAX_OBJECT_KEYS);
117
+ return Object.fromEntries(entries.map(([key, entry]) => [
118
+ key,
119
+ compactDiagnosticValue(entry, depth + 1)
120
+ ]));
121
+ }
122
+ return String(value);
123
+ }
124
+ function compactDiagnosticDetails(details) {
125
+ return Object.fromEntries(Object.entries(details).map(([key, value]) => [
126
+ key,
127
+ compactDiagnosticValue(value)
128
+ ]));
129
+ }
130
+ function mapRow(row, options = {}) {
131
+ const parsedDetails = JSON.parse(row.details_json);
81
132
  return diagnosticLogEntrySchema.parse({
82
133
  id: row.id,
83
134
  level: row.level,
@@ -91,7 +142,9 @@ function mapRow(row) {
91
142
  entityType: row.entity_type,
92
143
  entityId: row.entity_id,
93
144
  jobId: row.job_id,
94
- details: JSON.parse(row.details_json),
145
+ details: options.compactDetails
146
+ ? compactDiagnosticDetails(parsedDetails)
147
+ : parsedDetails,
95
148
  createdAt: row.created_at
96
149
  });
97
150
  }
@@ -197,7 +250,7 @@ export function listDiagnosticLogs(filters = {}) {
197
250
  params.push(filters.beforeCreatedAt, filters.beforeCreatedAt, filters.beforeId);
198
251
  }
199
252
  const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
200
- const limit = filters.limit ?? 200;
253
+ const limit = filters.limit ?? 100;
201
254
  const rows = getDatabase()
202
255
  .prepare(`SELECT id, level, source, scope, event_key, message, route, function_name,
203
256
  request_id, entity_type, entity_id, job_id, details_json, created_at
@@ -206,7 +259,7 @@ export function listDiagnosticLogs(filters = {}) {
206
259
  ORDER BY created_at DESC, id DESC
207
260
  LIMIT ?`)
208
261
  .all(...params, limit);
209
- const logs = rows.map(mapRow);
262
+ const logs = rows.map((row) => mapRow(row, { compactDetails: true }));
210
263
  const tail = rows.at(-1) ?? null;
211
264
  return {
212
265
  logs,
@@ -88,5 +88,13 @@ export function filterOwnedEntities(entityType, entities, userIds) {
88
88
  return decorated;
89
89
  }
90
90
  const allowed = new Set(userIds);
91
- return decorated.filter((entity) => entity.userId !== null && allowed.has(entity.userId));
91
+ return decorated.filter((entity) => {
92
+ if (entity.userId !== null && allowed.has(entity.userId)) {
93
+ return true;
94
+ }
95
+ const embeddedUserId = "userId" in entity && typeof entity.userId === "string"
96
+ ? entity.userId
97
+ : null;
98
+ return embeddedUserId !== null && allowed.has(embeddedUserId);
99
+ });
92
100
  }