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.
Files changed (48) hide show
  1. package/docs/capabilities.md +1 -1
  2. package/docs/configuration.md +2 -2
  3. package/flutter_app/lib/main.dart +1 -0
  4. package/flutter_app/lib/main_account_settings.dart +50 -22
  5. package/flutter_app/lib/main_admin.dart +24 -10
  6. package/flutter_app/lib/main_app_shell.dart +10 -9
  7. package/flutter_app/lib/main_chat.dart +631 -318
  8. package/flutter_app/lib/main_controller.dart +29 -8
  9. package/flutter_app/lib/main_devices.dart +4 -6
  10. package/flutter_app/lib/main_integrations.dart +15 -7
  11. package/flutter_app/lib/main_models.dart +207 -8
  12. package/flutter_app/lib/main_navigation.dart +36 -18
  13. package/flutter_app/lib/main_operations.dart +162 -91
  14. package/flutter_app/lib/main_settings.dart +273 -78
  15. package/flutter_app/lib/main_shared.dart +8 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/package.json +1 -1
  18. package/server/db/database.js +52 -0
  19. package/server/public/.last_build_id +1 -1
  20. package/server/public/assets/AssetManifest.json +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +79074 -78010
  24. package/server/routes/agents.js +2 -1
  25. package/server/routes/browser.js +1 -14
  26. package/server/routes/memory.js +75 -3
  27. package/server/routes/settings.js +1 -5
  28. package/server/routes/widgets.js +4 -4
  29. package/server/services/ai/capabilityHealth.js +1 -10
  30. package/server/services/ai/deliverables/artifact_helpers.js +190 -0
  31. package/server/services/ai/deliverables/contracts.js +113 -0
  32. package/server/services/ai/deliverables/deliverables.test.js +76 -0
  33. package/server/services/ai/deliverables/index.js +20 -0
  34. package/server/services/ai/deliverables/selector.js +94 -0
  35. package/server/services/ai/deliverables/validator.js +63 -0
  36. package/server/services/ai/deliverables/workflows.js +195 -0
  37. package/server/services/ai/engine.js +259 -1
  38. package/server/services/ai/runEvents.js +100 -0
  39. package/server/services/ai/systemPrompt.js +6 -0
  40. package/server/services/ai/tools.js +1 -5
  41. package/server/services/manager.js +5 -56
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/runtime/manager.js +2 -6
  44. package/server/services/runtime/settings.js +6 -12
  45. package/server/services/websocket.js +3 -1
  46. package/server/services/widgets/focus_widget.js +134 -0
  47. package/server/services/widgets/service.js +130 -2
  48. package/server/utils/deployment.js +4 -3
@@ -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
@@ -13,20 +13,7 @@ async function getBrowserController(req) {
13
13
  return runtimeController;
14
14
  }
15
15
  }
16
- const resolver = req.app?.locals?.getBrowserControllerForUser;
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
@@ -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
- const id = await mm.saveMemory(userId, content, category, importance, { agentId });
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
- res.status(500).json({ error: sanitizeError(err) });
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
- const resolver = req.app?.locals?.getBrowserControllerForUser;
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) {
@@ -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.status(400).json({ error: 'Widget is missing its refresh task.' });
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
- try {
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
+ };