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.
- package/README.md +13 -0
- package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
- package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
- package/dist/assets/index-CFCKDIMH.js +67 -0
- package/dist/assets/index-CFCKDIMH.js.map +1 -0
- package/dist/assets/index-ZPY6U1TU.css +1 -0
- package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
- package/dist/assets/motion-DvkU14p-.js.map +1 -0
- package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
- package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
- package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
- package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
- package/dist/assets/vendor-D9PTEPSB.js +824 -0
- package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
- package/dist/assets/viz-Cqb6s--o.js +34 -0
- package/dist/assets/viz-Cqb6s--o.js.map +1 -0
- package/dist/index.html +8 -8
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -0
- package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -4
- package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
- package/dist/openclaw/routes.js +236 -0
- package/dist/openclaw/session-bootstrap.d.ts +78 -0
- package/dist/openclaw/session-bootstrap.js +240 -0
- package/dist/openclaw/tools.js +279 -3
- package/dist/server/app.js +855 -19
- package/dist/server/connectors/box-registry.js +257 -0
- package/dist/server/db.js +2 -0
- package/dist/server/discovery-advertiser.js +114 -0
- package/dist/server/health.js +39 -11
- package/dist/server/index.js +4 -0
- package/dist/server/managers/platform/llm-manager.js +40 -4
- package/dist/server/managers/platform/openai-responses-provider.js +129 -19
- package/dist/server/movement.js +2935 -0
- package/dist/server/openapi.js +628 -5
- package/dist/server/psyche-types.js +15 -1
- package/dist/server/questionnaire-flow.js +552 -0
- package/dist/server/questionnaire-seeds.js +853 -0
- package/dist/server/questionnaire-types.js +340 -0
- package/dist/server/repositories/ai-connectors.js +944 -0
- package/dist/server/repositories/ai-processors.js +547 -0
- package/dist/server/repositories/diagnostic-logs.js +57 -4
- package/dist/server/repositories/entity-ownership.js +9 -1
- package/dist/server/repositories/habits.js +77 -9
- package/dist/server/repositories/model-settings.js +216 -0
- package/dist/server/repositories/notes.js +57 -15
- package/dist/server/repositories/preferences.js +124 -0
- package/dist/server/repositories/questionnaires.js +1338 -0
- package/dist/server/repositories/rewards.js +2 -2
- package/dist/server/repositories/settings.js +108 -12
- package/dist/server/repositories/surface-layouts.js +76 -0
- package/dist/server/repositories/wiki-memory.js +5 -1
- package/dist/server/services/entity-crud.js +81 -2
- package/dist/server/services/openai-codex-oauth.js +153 -0
- package/dist/server/services/psyche-observation-calendar.js +46 -0
- package/dist/server/types.js +492 -3
- package/dist/server/watch-mobile.js +562 -0
- package/dist/server/web.js +9 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -1
- package/server/migrations/024_questionnaires.sql +96 -0
- package/server/migrations/025_ai_model_connections.sql +26 -0
- package/server/migrations/026_custom_theme_settings.sql +2 -0
- package/server/migrations/027_ai_processors.sql +31 -0
- package/server/migrations/028_movement_domain.sql +136 -0
- package/server/migrations/029_watch_micro_capture.sql +23 -0
- package/server/migrations/030_surface_layouts.sql +5 -0
- package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/server/migrations/032_ai_connectors.sql +44 -0
- package/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/server/migrations/034_movement_segment_sync.sql +49 -0
- package/skills/forge-openclaw/SKILL.md +12 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
- package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
- package/dist/assets/index-Ch_xeZ2u.js +0 -63
- package/dist/assets/index-Ch_xeZ2u.js.map +0 -1
- package/dist/assets/index-DvVM7K6j.css +0 -1
- package/dist/assets/motion-D4sZgCHd.js.map +0 -1
- package/dist/assets/vendor-De38P6YR.js +0 -729
- package/dist/assets/vendor-De38P6YR.js.map +0 -1
- package/dist/assets/viz-C6hfyqzu.js +0 -34
- package/dist/assets/viz-C6hfyqzu.js.map +0 -1
- 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
|
|
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:
|
|
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 ??
|
|
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) =>
|
|
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
|
}
|