neoagent 2.3.1-beta.63 → 2.3.1-beta.65
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/docs/capabilities.md +1 -1
- package/docs/configuration.md +2 -2
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_account_settings.dart +50 -22
- package/flutter_app/lib/main_admin.dart +24 -10
- package/flutter_app/lib/main_app_shell.dart +10 -9
- package/flutter_app/lib/main_chat.dart +631 -318
- package/flutter_app/lib/main_controller.dart +29 -8
- package/flutter_app/lib/main_devices.dart +4 -6
- package/flutter_app/lib/main_integrations.dart +15 -7
- package/flutter_app/lib/main_models.dart +207 -8
- package/flutter_app/lib/main_navigation.dart +36 -18
- package/flutter_app/lib/main_operations.dart +162 -91
- package/flutter_app/lib/main_settings.dart +273 -78
- package/flutter_app/lib/main_shared.dart +8 -6
- package/flutter_app/lib/main_unified.dart +388 -0
- package/package.json +1 -1
- package/server/db/database.js +52 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.json +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +79074 -78010
- package/server/routes/agents.js +2 -1
- package/server/routes/browser.js +1 -14
- package/server/routes/memory.js +75 -3
- package/server/routes/settings.js +1 -5
- package/server/routes/widgets.js +4 -4
- package/server/services/ai/capabilityHealth.js +1 -10
- package/server/services/ai/deliverables/artifact_helpers.js +190 -0
- package/server/services/ai/deliverables/contracts.js +113 -0
- package/server/services/ai/deliverables/deliverables.test.js +76 -0
- package/server/services/ai/deliverables/index.js +20 -0
- package/server/services/ai/deliverables/selector.js +94 -0
- package/server/services/ai/deliverables/validator.js +63 -0
- package/server/services/ai/deliverables/workflows.js +195 -0
- package/server/services/ai/engine.js +259 -1
- package/server/services/ai/runEvents.js +100 -0
- package/server/services/ai/systemPrompt.js +6 -0
- package/server/services/ai/tools.js +1 -5
- package/server/services/manager.js +5 -56
- package/server/services/memory/manager.js +242 -26
- package/server/services/runtime/manager.js +2 -6
- package/server/services/runtime/settings.js +6 -12
- package/server/services/websocket.js +3 -1
- package/server/services/widgets/focus_widget.js +134 -0
- package/server/services/widgets/service.js +130 -2
- package/server/utils/deployment.js +4 -3
package/server/routes/agents.js
CHANGED
|
@@ -4,6 +4,7 @@ const db = require('../db/database');
|
|
|
4
4
|
const { requireAuth } = require('../middleware/auth');
|
|
5
5
|
const { sanitizeError } = require('../utils/security');
|
|
6
6
|
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
7
|
+
const { listRunEvents } = require('../services/ai/runEvents');
|
|
7
8
|
const { isInterimAssistantMetadata } = require('../services/ai/interim');
|
|
8
9
|
const { buildAgentRunContext } = require('./_helpers/agentRunContext');
|
|
9
10
|
|
|
@@ -176,7 +177,7 @@ router.get('/:id/steps', (req, res) => {
|
|
|
176
177
|
|| run.final_response
|
|
177
178
|
|| null;
|
|
178
179
|
|
|
179
|
-
res.json({ run, steps, response });
|
|
180
|
+
res.json({ run, steps, events: listRunEvents(run.id), response });
|
|
180
181
|
});
|
|
181
182
|
|
|
182
183
|
// Abort a run
|
package/server/routes/browser.js
CHANGED
|
@@ -13,20 +13,7 @@ async function getBrowserController(req) {
|
|
|
13
13
|
return runtimeController;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
const userId = req.session?.userId;
|
|
18
|
-
let controller;
|
|
19
|
-
if (typeof resolver === "function") {
|
|
20
|
-
controller = await resolver(userId);
|
|
21
|
-
} else {
|
|
22
|
-
controller = req.app?.locals?.browserController;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!controller) {
|
|
26
|
-
throw new Error(`getBrowserController: missing browser controller for userId=${userId ?? 'unknown'}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return controller;
|
|
16
|
+
throw new Error('Browser controller is unavailable. VM runtime is required.');
|
|
30
17
|
}
|
|
31
18
|
|
|
32
19
|
// Get browser status
|
package/server/routes/memory.js
CHANGED
|
@@ -33,6 +33,59 @@ function findOwnedMemoryIds(db, userId, agentId, ids) {
|
|
|
33
33
|
).all(userId, agentId, ...ids).map((row) => row.id);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function parsePlainObject(input, fieldName) {
|
|
37
|
+
if (input == null) return null;
|
|
38
|
+
if (typeof input === 'string') {
|
|
39
|
+
try {
|
|
40
|
+
input = JSON.parse(input);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${fieldName} must be valid JSON.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
46
|
+
throw new Error(`${fieldName} must be an object.`);
|
|
47
|
+
}
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeOptionalStringField(value, fieldName, maxLength, pattern = null) {
|
|
52
|
+
if (value == null || value === '') return null;
|
|
53
|
+
if (typeof value !== 'string') {
|
|
54
|
+
throw new Error(`${fieldName} must be a string.`);
|
|
55
|
+
}
|
|
56
|
+
const normalized = value.trim().slice(0, maxLength);
|
|
57
|
+
if (!normalized) return null;
|
|
58
|
+
if (pattern && !pattern.test(normalized)) {
|
|
59
|
+
throw new Error(`${fieldName} has an invalid format.`);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeSourceRef(input) {
|
|
65
|
+
const raw = parsePlainObject(input, 'sourceRef');
|
|
66
|
+
if (!raw) return undefined;
|
|
67
|
+
return {
|
|
68
|
+
sourceType: normalizeOptionalStringField(raw.sourceType ?? raw.type, 'sourceRef.sourceType', 48, /^[a-z0-9_:-]+$/i),
|
|
69
|
+
sourceId: normalizeOptionalStringField(raw.sourceId ?? raw.id, 'sourceRef.sourceId', 128),
|
|
70
|
+
sourceLabel: normalizeOptionalStringField(raw.sourceLabel ?? raw.label, 'sourceRef.sourceLabel', 160),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeScope(input) {
|
|
75
|
+
const raw = parsePlainObject(input, 'scope');
|
|
76
|
+
if (!raw) return undefined;
|
|
77
|
+
const scopeType = normalizeOptionalStringField(raw.scopeType ?? raw.type, 'scope.scopeType', 32, /^(agent|conversation|task|channel|shared)$/i);
|
|
78
|
+
return {
|
|
79
|
+
scopeType: scopeType ? scopeType.toLowerCase() : null,
|
|
80
|
+
scopeId: normalizeOptionalStringField(raw.scopeId ?? raw.id, 'scope.scopeId', 128),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeMetadata(input) {
|
|
85
|
+
const raw = parsePlainObject(input, 'metadata');
|
|
86
|
+
return raw == null ? undefined : raw;
|
|
87
|
+
}
|
|
88
|
+
|
|
36
89
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
90
|
// Overview (for initial page load)
|
|
38
91
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -46,6 +99,7 @@ router.get('/', (req, res) => {
|
|
|
46
99
|
res.json({
|
|
47
100
|
agentId,
|
|
48
101
|
assistantBehaviorNotes: mm.getAssistantBehaviorNotes(userId, { agentId }),
|
|
102
|
+
assistantSelfState: mm.getAssistantSelfState(userId, { agentId }),
|
|
49
103
|
dailyLogs: mm.listDailyLogs(7, userId),
|
|
50
104
|
apiKeys: Object.keys(mm.readApiKeys(userId)),
|
|
51
105
|
coreMemory
|
|
@@ -81,13 +135,31 @@ router.post('/memories', async (req, res) => {
|
|
|
81
135
|
const mm = req.app.locals.memoryManager;
|
|
82
136
|
const userId = req.session.userId;
|
|
83
137
|
const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
|
|
84
|
-
const { content, category = 'episodic', importance = 5 } = req.body;
|
|
138
|
+
const { content, category = 'episodic', importance = 5, sourceRef, scope, staleAfterDays, metadata } = req.body;
|
|
85
139
|
if (!content || !content.trim()) return res.status(400).json({ error: 'content is required' });
|
|
86
140
|
try {
|
|
87
|
-
|
|
141
|
+
let normalizedStaleAfterDays;
|
|
142
|
+
if (staleAfterDays != null && staleAfterDays !== '') {
|
|
143
|
+
normalizedStaleAfterDays = Number.parseInt(staleAfterDays, 10);
|
|
144
|
+
if (!Number.isInteger(normalizedStaleAfterDays) || normalizedStaleAfterDays <= 0) {
|
|
145
|
+
return res.status(400).json({ error: 'staleAfterDays must be a positive integer.' });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const id = await mm.saveMemory(userId, content, category, importance, {
|
|
150
|
+
agentId,
|
|
151
|
+
sourceRef: normalizeSourceRef(sourceRef),
|
|
152
|
+
scope: normalizeScope(scope),
|
|
153
|
+
staleAfterDays: normalizedStaleAfterDays,
|
|
154
|
+
metadata: normalizeMetadata(metadata),
|
|
155
|
+
});
|
|
88
156
|
res.json({ success: true, id });
|
|
89
157
|
} catch (err) {
|
|
90
|
-
|
|
158
|
+
const message = sanitizeError(err);
|
|
159
|
+
if (/must be valid JSON|must be an object|must be a string|has an invalid format/i.test(message)) {
|
|
160
|
+
return res.status(400).json({ error: message });
|
|
161
|
+
}
|
|
162
|
+
res.status(500).json({ error: message });
|
|
91
163
|
}
|
|
92
164
|
});
|
|
93
165
|
|
|
@@ -144,11 +144,7 @@ function getBrowserController(req) {
|
|
|
144
144
|
if (runtimeManager && typeof runtimeManager.getBrowserProviderForUser === 'function') {
|
|
145
145
|
return runtimeManager.getBrowserProviderForUser(req.session?.userId);
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
if (typeof resolver === 'function') {
|
|
149
|
-
return resolver(req.session?.userId);
|
|
150
|
-
}
|
|
151
|
-
return req.app?.locals?.browserController;
|
|
147
|
+
throw new Error('Browser controller is unavailable. VM runtime is required.');
|
|
152
148
|
}
|
|
153
149
|
|
|
154
150
|
function applyHeadlessSetting(req, value) {
|
package/server/routes/widgets.js
CHANGED
|
@@ -78,7 +78,7 @@ router.delete('/:id', (req, res) => {
|
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
router.post('/:id/refresh', (req, res) => {
|
|
81
|
+
router.post('/:id/refresh', async (req, res) => {
|
|
82
82
|
try {
|
|
83
83
|
const service = widgetService(req);
|
|
84
84
|
const taskRuntime = req.app?.locals?.taskRuntime;
|
|
@@ -89,10 +89,10 @@ router.post('/:id/refresh', (req, res) => {
|
|
|
89
89
|
if (!widget) {
|
|
90
90
|
return res.status(404).json({ error: 'Widget not found.' });
|
|
91
91
|
}
|
|
92
|
-
if (!widget.scheduledTaskId) {
|
|
93
|
-
return res.
|
|
92
|
+
if (widget.isSystem || !widget.scheduledTaskId) {
|
|
93
|
+
return res.json(await service.refreshWidget(req.session.userId, req.params.id));
|
|
94
94
|
}
|
|
95
|
-
res.json(taskRuntime.runTaskNow(widget.scheduledTaskId, req.session.userId));
|
|
95
|
+
res.json(await taskRuntime.runTaskNow(widget.scheduledTaskId, req.session.userId));
|
|
96
96
|
} catch (err) {
|
|
97
97
|
res.status(400).json({ error: sanitizeError(err) });
|
|
98
98
|
}
|
|
@@ -78,16 +78,7 @@ async function getBrowserHealth(userId, app, engine) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (!controller) {
|
|
81
|
-
|
|
82
|
-
const resolver = app?.locals?.getBrowserControllerForUser;
|
|
83
|
-
controller = await Promise.resolve(
|
|
84
|
-
typeof resolver === 'function'
|
|
85
|
-
? resolver(userId)
|
|
86
|
-
: (app?.locals?.browserController || engine?.browserController || null)
|
|
87
|
-
);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
resolutionError = resolutionError || err;
|
|
90
|
-
}
|
|
81
|
+
resolutionError = resolutionError || new Error('Browser provider is unavailable. VM runtime is required.');
|
|
91
82
|
}
|
|
92
83
|
|
|
93
84
|
if (!controller && resolutionError) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { normalizeArtifactContract } = require('./contracts');
|
|
6
|
+
|
|
7
|
+
const FILE_EXTENSION_TO_KIND = {
|
|
8
|
+
'.ppt': 'slides',
|
|
9
|
+
'.pptx': 'slides',
|
|
10
|
+
'.key': 'slides',
|
|
11
|
+
'.pdf': 'document',
|
|
12
|
+
'.doc': 'document',
|
|
13
|
+
'.docx': 'document',
|
|
14
|
+
'.md': 'document',
|
|
15
|
+
'.txt': 'document',
|
|
16
|
+
'.html': 'document',
|
|
17
|
+
'.htm': 'document',
|
|
18
|
+
'.csv': 'data',
|
|
19
|
+
'.tsv': 'data',
|
|
20
|
+
'.xlsx': 'data',
|
|
21
|
+
'.xls': 'data',
|
|
22
|
+
'.json': 'data',
|
|
23
|
+
'.png': 'image',
|
|
24
|
+
'.jpg': 'image',
|
|
25
|
+
'.jpeg': 'image',
|
|
26
|
+
'.gif': 'image',
|
|
27
|
+
'.webp': 'image',
|
|
28
|
+
'.svg': 'image',
|
|
29
|
+
'.mp4': 'video',
|
|
30
|
+
'.mov': 'video',
|
|
31
|
+
'.m4v': 'video',
|
|
32
|
+
'.webm': 'video',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const FILE_EXTENSION_TO_MIME = {
|
|
36
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
37
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
38
|
+
'.pdf': 'application/pdf',
|
|
39
|
+
'.doc': 'application/msword',
|
|
40
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
41
|
+
'.md': 'text/markdown',
|
|
42
|
+
'.txt': 'text/plain',
|
|
43
|
+
'.html': 'text/html',
|
|
44
|
+
'.htm': 'text/html',
|
|
45
|
+
'.csv': 'text/csv',
|
|
46
|
+
'.tsv': 'text/tab-separated-values',
|
|
47
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
48
|
+
'.xls': 'application/vnd.ms-excel',
|
|
49
|
+
'.json': 'application/json',
|
|
50
|
+
'.png': 'image/png',
|
|
51
|
+
'.jpg': 'image/jpeg',
|
|
52
|
+
'.jpeg': 'image/jpeg',
|
|
53
|
+
'.gif': 'image/gif',
|
|
54
|
+
'.webp': 'image/webp',
|
|
55
|
+
'.svg': 'image/svg+xml',
|
|
56
|
+
'.mp4': 'video/mp4',
|
|
57
|
+
'.mov': 'video/quicktime',
|
|
58
|
+
'.m4v': 'video/x-m4v',
|
|
59
|
+
'.webm': 'video/webm',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const CANDIDATE_KEYS = [
|
|
63
|
+
'path',
|
|
64
|
+
'paths',
|
|
65
|
+
'file',
|
|
66
|
+
'files',
|
|
67
|
+
'filePath',
|
|
68
|
+
'filePaths',
|
|
69
|
+
'fullPath',
|
|
70
|
+
'fullPaths',
|
|
71
|
+
'mediaPath',
|
|
72
|
+
'mediaPaths',
|
|
73
|
+
'screenshotPath',
|
|
74
|
+
'uiDumpPath',
|
|
75
|
+
'url',
|
|
76
|
+
'urls',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
function inferExtension(candidate = '') {
|
|
80
|
+
return path.extname(String(candidate || '').split('?')[0]).toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inferArtifactKind(candidate = '', fallback = 'artifact') {
|
|
84
|
+
const extension = inferExtension(candidate);
|
|
85
|
+
if (FILE_EXTENSION_TO_KIND[extension]) return FILE_EXTENSION_TO_KIND[extension];
|
|
86
|
+
const normalized = String(candidate || '').toLowerCase();
|
|
87
|
+
if (normalized.includes('image')) return 'image';
|
|
88
|
+
if (normalized.includes('video')) return 'video';
|
|
89
|
+
if (normalized.includes('slide') || normalized.includes('ppt')) return 'slides';
|
|
90
|
+
if (normalized.includes('doc') || normalized.includes('pdf')) return 'document';
|
|
91
|
+
if (normalized.includes('data') || normalized.includes('chart') || normalized.includes('csv')) return 'data';
|
|
92
|
+
return fallback;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function inferMimeType(candidate = '') {
|
|
96
|
+
const extension = inferExtension(candidate);
|
|
97
|
+
return FILE_EXTENSION_TO_MIME[extension] || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizePathOrUri(value) {
|
|
101
|
+
const text = String(value || '').trim();
|
|
102
|
+
if (!text) return null;
|
|
103
|
+
if (text.startsWith('/api/artifacts/')) return { uri: text, path: null };
|
|
104
|
+
if (/^https?:\/\//i.test(text)) return { uri: text, path: null };
|
|
105
|
+
if (/^[A-Za-z]:\\/.test(text)) return { path: text, uri: null };
|
|
106
|
+
if (path.isAbsolute(text)) return { path: text, uri: null };
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function buildArtifactFromCandidate(candidate, fallbackKind = 'artifact') {
|
|
111
|
+
const normalized = normalizePathOrUri(candidate);
|
|
112
|
+
if (!normalized) return null;
|
|
113
|
+
const source = normalized.path || normalized.uri || '';
|
|
114
|
+
const artifact = normalizeArtifactContract({
|
|
115
|
+
kind: inferArtifactKind(source, fallbackKind),
|
|
116
|
+
path: normalized.path,
|
|
117
|
+
uri: normalized.uri,
|
|
118
|
+
label: path.basename(String(source).split('?')[0]) || null,
|
|
119
|
+
mimeType: inferMimeType(source),
|
|
120
|
+
});
|
|
121
|
+
if (artifact.path) {
|
|
122
|
+
try {
|
|
123
|
+
artifact.size = (await fs.promises.stat(artifact.path)).size;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn('[deliverables] Failed to stat artifact candidate:', artifact.path, error?.message || error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return artifact.path || artifact.uri ? artifact : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scanStringForCandidates(text) {
|
|
132
|
+
const input = String(text || '');
|
|
133
|
+
const matches = [];
|
|
134
|
+
const regexes = [
|
|
135
|
+
/\/api\/artifacts\/[A-Za-z0-9%_-]+\/content/g,
|
|
136
|
+
/\/[^\s"'`]+?\.(?:pptx?|pdf|docx?|md|txt|html?|csv|tsv|xlsx?|json|png|jpe?g|gif|webp|svg|mp4|mov|m4v|webm)\b/g,
|
|
137
|
+
/[A-Za-z]:\\[^\s"'`]+?\.(?:pptx?|pdf|docx?|md|txt|html?|csv|tsv|xlsx?|json|png|jpe?g|gif|webp|svg|mp4|mov|m4v|webm)\b/g,
|
|
138
|
+
/https?:\/\/[^\s"'`]+?\.(?:pptx?|pdf|docx?|md|txt|html?|csv|tsv|xlsx?|json|png|jpe?g|gif|webp|svg|mp4|mov|m4v|webm)\b/g,
|
|
139
|
+
];
|
|
140
|
+
for (const regex of regexes) {
|
|
141
|
+
const found = input.match(regex);
|
|
142
|
+
if (found) matches.push(...found);
|
|
143
|
+
}
|
|
144
|
+
return matches;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function extractArtifactsFromResult(toolName, result) {
|
|
148
|
+
const artifacts = [];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const fallbackKind = inferArtifactKind(toolName, 'artifact');
|
|
151
|
+
|
|
152
|
+
async function pushCandidate(candidate) {
|
|
153
|
+
const artifact = await buildArtifactFromCandidate(candidate, fallbackKind);
|
|
154
|
+
if (!artifact) return;
|
|
155
|
+
const key = `${artifact.kind}:${artifact.path || artifact.uri}`;
|
|
156
|
+
if (seen.has(key)) return;
|
|
157
|
+
seen.add(key);
|
|
158
|
+
artifacts.push(artifact);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function visit(value, keyHint = '') {
|
|
162
|
+
if (value == null) return;
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
if (CANDIDATE_KEYS.includes(keyHint)) await pushCandidate(value);
|
|
165
|
+
for (const candidate of scanStringForCandidates(value)) {
|
|
166
|
+
await pushCandidate(candidate);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
for (const item of value) await visit(item, keyHint);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (typeof value === 'object') {
|
|
175
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
176
|
+
await visit(nested, key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await visit(result);
|
|
182
|
+
return artifacts;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
extractArtifactsFromResult,
|
|
187
|
+
inferArtifactKind,
|
|
188
|
+
inferMimeType,
|
|
189
|
+
normalizePathOrUri,
|
|
190
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DELIVERABLE_TYPES = [
|
|
4
|
+
'slides',
|
|
5
|
+
'document',
|
|
6
|
+
'research_report',
|
|
7
|
+
'data_analysis',
|
|
8
|
+
'image',
|
|
9
|
+
'video',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const DELIVERABLE_SELECTION_STATUSES = ['selected', 'standard'];
|
|
13
|
+
const DELIVERABLE_VALIDATION_STATUSES = ['passed', 'failed'];
|
|
14
|
+
|
|
15
|
+
function clampText(value, maxLength = 240) {
|
|
16
|
+
return String(value || '').trim().slice(0, maxLength);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeDeliverableType(value) {
|
|
20
|
+
const normalized = clampText(value, 48).toLowerCase();
|
|
21
|
+
return DELIVERABLE_TYPES.includes(normalized) ? normalized : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeStringList(value, limit = 8, maxLength = 120) {
|
|
25
|
+
if (!Array.isArray(value)) return [];
|
|
26
|
+
return value
|
|
27
|
+
.map((item) => clampText(item, maxLength))
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.slice(0, limit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeArtifactContract(raw = {}) {
|
|
33
|
+
const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
34
|
+
return {
|
|
35
|
+
kind: clampText(source.kind, 48) || 'artifact',
|
|
36
|
+
path: clampText(source.path || source.fullPath, 500) || null,
|
|
37
|
+
uri: clampText(source.uri || source.url, 500) || null,
|
|
38
|
+
label: clampText(source.label, 120) || null,
|
|
39
|
+
mimeType: clampText(source.mimeType || source.contentType, 120) || null,
|
|
40
|
+
size: Number.isFinite(Number(source.size ?? source.byteSize))
|
|
41
|
+
? Number(source.size ?? source.byteSize)
|
|
42
|
+
: null,
|
|
43
|
+
preview: source.preview && typeof source.preview === 'object' && !Array.isArray(source.preview)
|
|
44
|
+
? { ...source.preview }
|
|
45
|
+
: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeDeliverableSelection(raw = {}) {
|
|
50
|
+
const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
51
|
+
const type = normalizeDeliverableType(source.type);
|
|
52
|
+
const confidence = Math.max(0, Math.min(1, Number(source.confidence) || 0));
|
|
53
|
+
const status = DELIVERABLE_SELECTION_STATUSES.includes(String(source.status || '').trim().toLowerCase())
|
|
54
|
+
? String(source.status).trim().toLowerCase()
|
|
55
|
+
: (type ? 'selected' : 'standard');
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
status,
|
|
59
|
+
type,
|
|
60
|
+
confidence,
|
|
61
|
+
goal: clampText(source.goal, 240),
|
|
62
|
+
requestedOutputs: normalizeStringList(source.requested_outputs || source.requestedOutputs, 8, 120),
|
|
63
|
+
supportingCapabilities: normalizeStringList(source.supporting_capabilities || source.supportingCapabilities, 8, 64),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeDeliverableValidationResult(raw = {}) {
|
|
68
|
+
const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
69
|
+
const status = DELIVERABLE_VALIDATION_STATUSES.includes(String(source.status || '').trim().toLowerCase())
|
|
70
|
+
? String(source.status).trim().toLowerCase()
|
|
71
|
+
: 'failed';
|
|
72
|
+
const artifacts = Array.isArray(source.artifacts)
|
|
73
|
+
? source.artifacts.map(normalizeArtifactContract).filter((artifact) => artifact.path || artifact.uri)
|
|
74
|
+
: [];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
summary: clampText(source.summary, 320),
|
|
79
|
+
errors: normalizeStringList(source.errors, 8, 220),
|
|
80
|
+
warnings: normalizeStringList(source.warnings, 8, 220),
|
|
81
|
+
artifacts,
|
|
82
|
+
metrics: source.metrics && typeof source.metrics === 'object' && !Array.isArray(source.metrics)
|
|
83
|
+
? { ...source.metrics }
|
|
84
|
+
: {},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeDeliverableResult(raw = {}) {
|
|
89
|
+
const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
90
|
+
return {
|
|
91
|
+
type: normalizeDeliverableType(source.type),
|
|
92
|
+
status: clampText(source.status, 32) || 'unknown',
|
|
93
|
+
summary: clampText(source.summary, 320),
|
|
94
|
+
artifacts: Array.isArray(source.artifacts)
|
|
95
|
+
? source.artifacts.map(normalizeArtifactContract).filter((artifact) => artifact.path || artifact.uri)
|
|
96
|
+
: [],
|
|
97
|
+
validation: normalizeDeliverableValidationResult(source.validation),
|
|
98
|
+
metadata: source.metadata && typeof source.metadata === 'object' && !Array.isArray(source.metadata)
|
|
99
|
+
? { ...source.metadata }
|
|
100
|
+
: {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
DELIVERABLE_TYPES,
|
|
106
|
+
clampText,
|
|
107
|
+
normalizeArtifactContract,
|
|
108
|
+
normalizeDeliverableResult,
|
|
109
|
+
normalizeDeliverableSelection,
|
|
110
|
+
normalizeDeliverableType,
|
|
111
|
+
normalizeDeliverableValidationResult,
|
|
112
|
+
normalizeStringList,
|
|
113
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { extractArtifactsFromResult } = require('./artifact_helpers');
|
|
7
|
+
const { getDeliverableWorkflow } = require('./workflows');
|
|
8
|
+
const { validateDeliverableExecution } = require('./validator');
|
|
9
|
+
|
|
10
|
+
test('extractArtifactsFromResult collects artifact urls and local file paths', async () => {
|
|
11
|
+
const artifacts = await extractArtifactsFromResult('generate_image', {
|
|
12
|
+
paths: ['/tmp/launch-visual.png'],
|
|
13
|
+
previewUrl: '/api/artifacts/123/content',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.equal(artifacts.length, 2);
|
|
17
|
+
assert.equal(artifacts[0].kind, 'image');
|
|
18
|
+
assert.equal(artifacts[0].path, '/tmp/launch-visual.png');
|
|
19
|
+
assert.ok(!artifacts[0].uri);
|
|
20
|
+
assert.equal(artifacts[1].kind, 'image');
|
|
21
|
+
assert.equal(artifacts[1].uri, '/api/artifacts/123/content');
|
|
22
|
+
assert.ok(!artifacts[1].path);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('slides workflow passes when a matching presentation artifact exists', async () => {
|
|
26
|
+
const workflow = getDeliverableWorkflow('slides');
|
|
27
|
+
const request = workflow.normalizeRequest({
|
|
28
|
+
goal: 'Create the board deck',
|
|
29
|
+
requestedOutputs: ['pptx deck'],
|
|
30
|
+
});
|
|
31
|
+
const plan = workflow.buildExecutionPlan(request);
|
|
32
|
+
|
|
33
|
+
const result = await validateDeliverableExecution({
|
|
34
|
+
workflow,
|
|
35
|
+
request,
|
|
36
|
+
plan,
|
|
37
|
+
finalReply: 'The presentation is ready at /tmp/board-deck.pptx.',
|
|
38
|
+
artifacts: [
|
|
39
|
+
{
|
|
40
|
+
kind: 'slides',
|
|
41
|
+
path: '/tmp/board-deck.pptx',
|
|
42
|
+
label: 'board-deck.pptx',
|
|
43
|
+
mimeType:
|
|
44
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
toolExecutions: [],
|
|
48
|
+
runId: 'run-1',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(result.validation.status, 'passed');
|
|
52
|
+
assert.equal(result.result.type, 'slides');
|
|
53
|
+
assert.equal(result.result.artifacts.length, 1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('video workflow fails when no video artifact was produced', async () => {
|
|
57
|
+
const workflow = getDeliverableWorkflow('video');
|
|
58
|
+
const request = workflow.normalizeRequest({
|
|
59
|
+
goal: 'Create a launch trailer',
|
|
60
|
+
requestedOutputs: ['launch video'],
|
|
61
|
+
});
|
|
62
|
+
const plan = workflow.buildExecutionPlan(request);
|
|
63
|
+
|
|
64
|
+
const result = await validateDeliverableExecution({
|
|
65
|
+
workflow,
|
|
66
|
+
request,
|
|
67
|
+
plan,
|
|
68
|
+
finalReply: 'I finished the video.',
|
|
69
|
+
artifacts: [],
|
|
70
|
+
toolExecutions: [],
|
|
71
|
+
runId: 'run-2',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.equal(result.validation.status, 'failed');
|
|
75
|
+
assert.match(result.validation.summary, /validation failed/i);
|
|
76
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { selectDeliverableWorkflow } = require('./selector');
|
|
4
|
+
const {
|
|
5
|
+
buildDeliverableWorkflowGuidance,
|
|
6
|
+
getDeliverableWorkflow,
|
|
7
|
+
listDeliverableWorkflows,
|
|
8
|
+
} = require('./workflows');
|
|
9
|
+
const { extractArtifactsFromResult } = require('./artifact_helpers');
|
|
10
|
+
const { DeliverableValidationError, validateDeliverableExecution } = require('./validator');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
buildDeliverableWorkflowGuidance,
|
|
14
|
+
DeliverableValidationError,
|
|
15
|
+
extractArtifactsFromResult,
|
|
16
|
+
getDeliverableWorkflow,
|
|
17
|
+
listDeliverableWorkflows,
|
|
18
|
+
selectDeliverableWorkflow,
|
|
19
|
+
validateDeliverableExecution,
|
|
20
|
+
};
|