@aiconnect/agentjobs-mcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -0
- package/README.md +39 -6
- package/build/index.js +1 -1
- package/build/test-tools.js +1 -1
- package/build/tools/get_job.js +39 -9
- package/build/tools/get_job_activities.js +68 -0
- package/build/tools/list_jobs.js +38 -4
- package/build/utils/formatters.js +218 -23
- package/build/utils/schemas.js +8 -0
- package/docs/agent-jobs-api.md +115 -0
- package/package.json +1 -1
- package/build/config.test.js +0 -22
- package/build/utils/formatters.test.js +0 -629
- package/build/utils/schemas.test.js +0 -95
- package/build/utils/version.test.js +0 -9
|
@@ -47,6 +47,37 @@ const jobConfigSchema = z.object({
|
|
|
47
47
|
failure_cooldown_minutes: z.number().optional(),
|
|
48
48
|
start_prompt: z.string().optional(),
|
|
49
49
|
}).passthrough();
|
|
50
|
+
const activitySourceSchema = z.object({
|
|
51
|
+
type: z.enum(['dispatch', 'process_module', 'direct']),
|
|
52
|
+
reference_id: z.string().optional(),
|
|
53
|
+
execution_id: z.string().optional(),
|
|
54
|
+
job_id: z.string().optional(),
|
|
55
|
+
chat_id: z.string().optional(),
|
|
56
|
+
agent_job_type_id: z.string().optional(),
|
|
57
|
+
channel_code: z.string().optional(),
|
|
58
|
+
}).passthrough();
|
|
59
|
+
// Required-by-contract (per `job-activities-query` spec): every activity entry
|
|
60
|
+
// rendered by the formatter MUST surface id, created_at, status, activity_type_code,
|
|
61
|
+
// source.type, consumed_credits, allocated_credits. Records missing any of these
|
|
62
|
+
// fail the parse and fall back to "[unparseable activity]" so upstream audit-data
|
|
63
|
+
// corruption is surfaced rather than silently rendered as `unknown`/`n/a`.
|
|
64
|
+
const activitySchema = z.object({
|
|
65
|
+
id: z.string().min(1),
|
|
66
|
+
org_id: z.string().optional(),
|
|
67
|
+
activity_type_code: z.string().min(1),
|
|
68
|
+
status: z.enum(['submitted', 'completed', 'canceled']),
|
|
69
|
+
allocated_credits: z.number(),
|
|
70
|
+
consumed_credits: z.number(),
|
|
71
|
+
credits_rule_id: z.number().optional(),
|
|
72
|
+
payloads: z.object({
|
|
73
|
+
input: z.any().optional(),
|
|
74
|
+
output: z.any().optional(),
|
|
75
|
+
}).passthrough().optional(),
|
|
76
|
+
processed_at: isoOrMs.nullable().optional(),
|
|
77
|
+
created_at: z.union([z.string(), z.number()]),
|
|
78
|
+
updated_at: isoOrMs,
|
|
79
|
+
source: activitySourceSchema,
|
|
80
|
+
}).passthrough();
|
|
50
81
|
const jobDetailsSchema = z.object({
|
|
51
82
|
job_id: z.string(),
|
|
52
83
|
job_type_id: z.string(),
|
|
@@ -66,6 +97,13 @@ const jobDetailsSchema = z.object({
|
|
|
66
97
|
channel_data: channelDataSchema.optional().default({}),
|
|
67
98
|
job_config: jobConfigSchema.optional().default({}),
|
|
68
99
|
params: z.record(z.any()).optional().default({}),
|
|
100
|
+
activities_count: z.number().optional(),
|
|
101
|
+
// Activities are intentionally validated as `unknown[]` here (not as
|
|
102
|
+
// `z.array(activitySchema)`): per-entry parsing happens inside
|
|
103
|
+
// `formatActivityEntry`, so a single malformed record degrades to
|
|
104
|
+
// "[unparseable activity]" without making the whole job document fall back
|
|
105
|
+
// to the raw-JSON branch in formatJobDetails.
|
|
106
|
+
Activities: z.array(z.unknown()).optional(),
|
|
69
107
|
}).passthrough();
|
|
70
108
|
const bool = (v) => (v === true ? 'yes' : v === false ? 'no' : 'n/a');
|
|
71
109
|
const safe = (v, fallback = 'n/a') => v === undefined || v === null || v === '' ? fallback : String(v);
|
|
@@ -75,7 +113,134 @@ const truncate = (s, max = 300) => {
|
|
|
75
113
|
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
76
114
|
};
|
|
77
115
|
const fmtList = (arr) => (arr && arr.length ? arr.join(', ') : 'n/a');
|
|
78
|
-
|
|
116
|
+
const ACTIVITY_OUTPUT_MAX = 200;
|
|
117
|
+
function isBinaryValue(value) {
|
|
118
|
+
if (value === null || value === undefined || typeof value !== 'object')
|
|
119
|
+
return false;
|
|
120
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
|
|
121
|
+
return true;
|
|
122
|
+
if (value instanceof ArrayBuffer)
|
|
123
|
+
return true;
|
|
124
|
+
if (typeof ArrayBuffer.isView === 'function' && ArrayBuffer.isView(value))
|
|
125
|
+
return true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function containsBinary(value, seen = new WeakSet()) {
|
|
129
|
+
if (isBinaryValue(value))
|
|
130
|
+
return true;
|
|
131
|
+
if (value === null || typeof value !== 'object')
|
|
132
|
+
return false;
|
|
133
|
+
if (seen.has(value))
|
|
134
|
+
return false;
|
|
135
|
+
seen.add(value);
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
for (const v of value) {
|
|
138
|
+
if (containsBinary(v, seen))
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
for (const v of Object.values(value)) {
|
|
144
|
+
if (containsBinary(v, seen))
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
function safeStringifyOutput(value) {
|
|
150
|
+
if (value === null || value === undefined)
|
|
151
|
+
return '';
|
|
152
|
+
if (typeof value === 'string')
|
|
153
|
+
return value;
|
|
154
|
+
if (containsBinary(value))
|
|
155
|
+
return '[non-serializable]';
|
|
156
|
+
try {
|
|
157
|
+
const text = JSON.stringify(value);
|
|
158
|
+
return text === undefined ? '[non-serializable]' : text;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return '[non-serializable]';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Collapses newlines/CR/tabs into escaped literals and clamps the result so it
|
|
166
|
+
* always fits in a single visual line of the activity output. Used by every
|
|
167
|
+
* code path that injects user-controlled activity content into the rendered
|
|
168
|
+
* line — both the normal `payloads.output` branch and the malformed-entry
|
|
169
|
+
* fallback — so a bad record cannot visually corrupt the surrounding entries.
|
|
170
|
+
*/
|
|
171
|
+
function sanitizeForActivityLine(value, max = ACTIVITY_OUTPUT_MAX) {
|
|
172
|
+
const escaped = value
|
|
173
|
+
.replace(/\r\n/g, '\\n')
|
|
174
|
+
.replace(/\n/g, '\\n')
|
|
175
|
+
.replace(/\r/g, '\\n')
|
|
176
|
+
.replace(/\t/g, '\\t');
|
|
177
|
+
return escaped.length > max ? `${escaped.slice(0, max)}…` : escaped;
|
|
178
|
+
}
|
|
179
|
+
function formatActivitySource(source) {
|
|
180
|
+
if (!source || typeof source !== 'object')
|
|
181
|
+
return 'unknown';
|
|
182
|
+
const type = source.type ?? 'unknown';
|
|
183
|
+
const extras = [];
|
|
184
|
+
if (source.reference_id)
|
|
185
|
+
extras.push(`ref: ${source.reference_id}`);
|
|
186
|
+
if (source.execution_id)
|
|
187
|
+
extras.push(`exec: ${source.execution_id}`);
|
|
188
|
+
if (source.chat_id)
|
|
189
|
+
extras.push(`chat: ${source.chat_id}`);
|
|
190
|
+
if (source.agent_job_type_id)
|
|
191
|
+
extras.push(`type: ${source.agent_job_type_id}`);
|
|
192
|
+
if (source.channel_code)
|
|
193
|
+
extras.push(`channel: ${source.channel_code}`);
|
|
194
|
+
return extras.length ? `${type} (${extras.join(', ')})` : String(type);
|
|
195
|
+
}
|
|
196
|
+
export function formatActivityEntry(activity) {
|
|
197
|
+
let a;
|
|
198
|
+
try {
|
|
199
|
+
a = activitySchema.parse(activity);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
const raw = safeStringifyOutput(activity);
|
|
203
|
+
return `- [unparseable activity] ${sanitizeForActivityLine(raw)}`;
|
|
204
|
+
}
|
|
205
|
+
const createdIso = toIso(a.created_at) ?? 'n/a';
|
|
206
|
+
const status = a.status ?? 'unknown';
|
|
207
|
+
const typeCode = a.activity_type_code ?? 'unknown';
|
|
208
|
+
const sourceLine = formatActivitySource(a.source);
|
|
209
|
+
const consumed = a.consumed_credits ?? 0;
|
|
210
|
+
const allocated = a.allocated_credits ?? 0;
|
|
211
|
+
const lines = [
|
|
212
|
+
`- ${createdIso} [${status}] ${typeCode} via ${sourceLine}`,
|
|
213
|
+
` credits: ${consumed}/${allocated} id: ${a.id}`,
|
|
214
|
+
];
|
|
215
|
+
const output = a.payloads?.output;
|
|
216
|
+
if (output !== undefined && output !== null && output !== '') {
|
|
217
|
+
const stringified = safeStringifyOutput(output);
|
|
218
|
+
if (stringified !== '') {
|
|
219
|
+
lines.push(` output: ${sanitizeForActivityLine(stringified)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
export function formatJobActivitiesList(jobId, activities, meta, offset = 0) {
|
|
225
|
+
if (!meta || typeof meta.count !== 'number' || typeof meta.limit !== 'number' || typeof meta.total !== 'number') {
|
|
226
|
+
throw new Error('formatJobActivitiesList: meta is required with numeric `count`, `limit`, and `total`. ' +
|
|
227
|
+
`Received: ${JSON.stringify(meta)}`);
|
|
228
|
+
}
|
|
229
|
+
const safeActivities = activities || [];
|
|
230
|
+
const { count, total } = meta;
|
|
231
|
+
const hasMore = (offset + count) < total;
|
|
232
|
+
// Advance by the number of rows actually returned, not by `limit`. If the
|
|
233
|
+
// backend ships a short non-terminal page (count < limit), advancing by
|
|
234
|
+
// `limit` would tell the caller to skip unseen rows.
|
|
235
|
+
const nextOffset = hasMore ? offset + count : null;
|
|
236
|
+
const footer = `Returned: ${count} | Total matching: ${total} | Has more: ${hasMore} | Next offset: ${nextOffset === null ? 'null' : nextOffset}`;
|
|
237
|
+
if (safeActivities.length === 0) {
|
|
238
|
+
return `No activities found for job ${jobId}.\n\n${footer}`;
|
|
239
|
+
}
|
|
240
|
+
const entries = safeActivities.map(formatActivityEntry).join('\n');
|
|
241
|
+
return `Activities for job ${jobId} (showing ${count}):\n\n${entries}\n\n${footer}`;
|
|
242
|
+
}
|
|
243
|
+
export function formatJobDetails(job, meta) {
|
|
79
244
|
try {
|
|
80
245
|
const j = jobDetailsSchema.parse(job);
|
|
81
246
|
// Derivados
|
|
@@ -120,6 +285,27 @@ export function formatJobDetails(job) {
|
|
|
120
285
|
}
|
|
121
286
|
})();
|
|
122
287
|
const startPrompt = j.job_config?.start_prompt ? truncate(j.job_config.start_prompt, 500) : undefined;
|
|
288
|
+
// Fail-closed: the only signal that the caller actually requested the
|
|
289
|
+
// overlay is `meta.activities_meta` (a field the backend returns ONLY for
|
|
290
|
+
// ?include=activities). Without that signal we omit the Activities block
|
|
291
|
+
// entirely — even if `j.Activities` happens to be populated in the payload —
|
|
292
|
+
// to keep flag-off output byte-identical to the legacy formatter.
|
|
293
|
+
let activitiesBlock = '';
|
|
294
|
+
const overlayRequested = meta?.activities_meta !== undefined;
|
|
295
|
+
if (overlayRequested) {
|
|
296
|
+
if (Array.isArray(j.Activities) && j.Activities.length > 0) {
|
|
297
|
+
const entries = j.Activities.map(formatActivityEntry).join('\n');
|
|
298
|
+
const total = meta?.activities_meta?.count;
|
|
299
|
+
const limit = meta?.activities_meta?.limit;
|
|
300
|
+
const truncationLine = (typeof total === 'number' && typeof limit === 'number' && total > limit)
|
|
301
|
+
? `\n(showing ${j.Activities.length} of ${total} activities — use get_job_activities for full pagination)`
|
|
302
|
+
: '';
|
|
303
|
+
activitiesBlock = `\n\nActivities:\n${entries}${truncationLine}`;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
activitiesBlock = `\n\nActivities:\n - (no activities recorded for this job)`;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
123
309
|
return (`Job Details
|
|
124
310
|
===========
|
|
125
311
|
|
|
@@ -172,7 +358,7 @@ Result / Tags / Log:
|
|
|
172
358
|
- Result: ${safe(j.result)}
|
|
173
359
|
- Tags: ${fmtList(tagsList)}
|
|
174
360
|
- Execution Log (last 5):
|
|
175
|
-
${lastLogs.length ? lastLogs.join('\n') : ' - n/a'}
|
|
361
|
+
${lastLogs.length ? lastLogs.join('\n') : ' - n/a'}${activitiesBlock}
|
|
176
362
|
`).trim();
|
|
177
363
|
}
|
|
178
364
|
catch (e) {
|
|
@@ -190,15 +376,32 @@ const jobSchema = z.object({
|
|
|
190
376
|
job_status: z.string(),
|
|
191
377
|
result: z.string().nullable(),
|
|
192
378
|
job_type_id: z.string(),
|
|
379
|
+
activities_count: z.number().optional(),
|
|
380
|
+
Activities: z.array(z.any()).optional(),
|
|
193
381
|
}).passthrough(); // .passthrough() permite outros campos não definidos no schema.
|
|
194
382
|
/**
|
|
195
383
|
* Formata um resumo de um job, com os campos principais.
|
|
196
|
-
* @param job - O objeto do job.
|
|
197
|
-
* @returns Uma string formatada com o resumo do job.
|
|
198
384
|
*/
|
|
199
|
-
export function formatJobSummary(job) {
|
|
385
|
+
export function formatJobSummary(job, options = {}) {
|
|
200
386
|
try {
|
|
201
387
|
const parsedJob = jobSchema.parse(job);
|
|
388
|
+
const overlayConsidered = options.includeActivities === true && Array.isArray(parsedJob.Activities);
|
|
389
|
+
const overlayLen = overlayConsidered ? parsedJob.Activities.length : undefined;
|
|
390
|
+
const totalCount = typeof parsedJob.activities_count === 'number' ? parsedJob.activities_count : undefined;
|
|
391
|
+
let activitiesLine = '';
|
|
392
|
+
if (totalCount !== undefined && overlayLen !== undefined && overlayLen !== totalCount) {
|
|
393
|
+
// Surface any divergence between the canonical total and the overlay size,
|
|
394
|
+
// not just the truncation direction (overlay < total). A backend bug that
|
|
395
|
+
// returns more overlay entries than the canonical count is also a real
|
|
396
|
+
// mismatch the caller should see.
|
|
397
|
+
activitiesLine = `\n- Activities: ${totalCount} (overlay: ${overlayLen})`;
|
|
398
|
+
}
|
|
399
|
+
else if (totalCount !== undefined) {
|
|
400
|
+
activitiesLine = `\n- Activities: ${totalCount}`;
|
|
401
|
+
}
|
|
402
|
+
else if (overlayLen !== undefined) {
|
|
403
|
+
activitiesLine = `\n- Activities: ${overlayLen}`;
|
|
404
|
+
}
|
|
202
405
|
return `
|
|
203
406
|
- Job ID: ${parsedJob.job_id}
|
|
204
407
|
- Status: ${parsedJob.job_status}
|
|
@@ -206,7 +409,7 @@ export function formatJobSummary(job) {
|
|
|
206
409
|
- Channel: ${parsedJob.channel_code}
|
|
207
410
|
- Scheduled At: ${parsedJob.scheduled_at}
|
|
208
411
|
- Updated At: ${parsedJob.updated_at}
|
|
209
|
-
- Result: ${parsedJob.result || 'N/A'}
|
|
412
|
+
- Result: ${parsedJob.result || 'N/A'}${activitiesLine}
|
|
210
413
|
`.trim();
|
|
211
414
|
}
|
|
212
415
|
catch {
|
|
@@ -214,21 +417,7 @@ export function formatJobSummary(job) {
|
|
|
214
417
|
return JSON.stringify(job, null, 2);
|
|
215
418
|
}
|
|
216
419
|
}
|
|
217
|
-
|
|
218
|
-
* Formata a resposta para a lista de jobs.
|
|
219
|
-
*
|
|
220
|
-
* `meta` é obrigatório e os três campos (`count`, `limit`, `total`) precisam
|
|
221
|
-
* ser numéricos — confirmado empiricamente que o backend sempre os retorna em
|
|
222
|
-
* `/services/agent-jobs`. Falha explícita aqui é preferível a inventar um
|
|
223
|
-
* footer com valores inferidos, pois isso recriaria a ambiguidade
|
|
224
|
-
* "page count vs total" que esta tool deveria eliminar.
|
|
225
|
-
*
|
|
226
|
-
* @param jobs - Um array de jobs.
|
|
227
|
-
* @param meta - O objeto `meta` retornado pelo backend.
|
|
228
|
-
* @param offset - O `offset` que a tool enviou na requisição (default 0).
|
|
229
|
-
* @returns Uma string formatada com a lista de resumos de jobs.
|
|
230
|
-
*/
|
|
231
|
-
export function formatJobList(jobs, meta, offset = 0) {
|
|
420
|
+
export function formatJobList(jobs, meta, offset = 0, options = {}) {
|
|
232
421
|
if (!meta || typeof meta.count !== 'number' || typeof meta.limit !== 'number' || typeof meta.total !== 'number') {
|
|
233
422
|
throw new Error('formatJobList: meta is required with numeric `count`, `limit`, and `total`. ' +
|
|
234
423
|
`Received: ${JSON.stringify(meta)}`);
|
|
@@ -237,11 +426,17 @@ export function formatJobList(jobs, meta, offset = 0) {
|
|
|
237
426
|
const { count, limit, total } = meta;
|
|
238
427
|
const hasMore = (offset + count) < total;
|
|
239
428
|
const nextOffset = hasMore ? offset + limit : null;
|
|
240
|
-
|
|
429
|
+
let footer = `Returned: ${count} | Total matching: ${total} | Has more: ${hasMore} | Next offset: ${nextOffset === null ? 'null' : nextOffset}`;
|
|
430
|
+
if (options.includeActivities === true && meta.activities_truncated === true) {
|
|
431
|
+
const returned = typeof meta.activities_total_returned === 'number' ? meta.activities_total_returned : '?';
|
|
432
|
+
const available = typeof meta.activities_total_available === 'number' ? meta.activities_total_available : '?';
|
|
433
|
+
footer += `\nActivities truncated: yes (returned ${returned} of ${available} available)`;
|
|
434
|
+
}
|
|
241
435
|
if (safeJobs.length === 0) {
|
|
242
436
|
return `Found 0 jobs.\n\n${footer}`;
|
|
243
437
|
}
|
|
244
|
-
const
|
|
438
|
+
const summaryOpts = { includeActivities: options.includeActivities === true };
|
|
439
|
+
const jobSummaries = safeJobs.map(job => formatJobSummary(job, summaryOpts)).join('\n\n');
|
|
245
440
|
return `Found ${safeJobs.length} jobs.\n\n${jobSummaries}\n\n${footer}`;
|
|
246
441
|
}
|
|
247
442
|
// Schema for job type details
|
package/build/utils/schemas.js
CHANGED
|
@@ -22,3 +22,11 @@ export const flexibleDateTimeSchema = () => z.string().refine((value) => {
|
|
|
22
22
|
}, {
|
|
23
23
|
message: "Invalid date-time string. Please use a valid ISO 8601 format.",
|
|
24
24
|
});
|
|
25
|
+
/**
|
|
26
|
+
* Enum factories for activity record fields. Returned as factories for the
|
|
27
|
+
* same reason as `flexibleDateTimeSchema` — singletons would cause `$ref`
|
|
28
|
+
* collisions in the serialized JSON Schema across sibling tool fields.
|
|
29
|
+
*/
|
|
30
|
+
export const activityStatusSchema = () => z.enum(['submitted', 'completed', 'canceled']);
|
|
31
|
+
export const activitySourceTypeSchema = () => z.enum(['dispatch', 'process_module', 'direct']);
|
|
32
|
+
export const activitiesSortSchema = () => z.enum(['created_at', '-created_at']);
|
package/docs/agent-jobs-api.md
CHANGED
|
@@ -375,3 +375,118 @@ curl -X DELETE https://api.example.com/services/agent-jobs/job123 \
|
|
|
375
375
|
"reason": "No longer needed"
|
|
376
376
|
}'
|
|
377
377
|
```
|
|
378
|
+
|
|
379
|
+
## Activities
|
|
380
|
+
|
|
381
|
+
Cada agent job acumula uma trilha de auditoria de **activities** (registros de operações como `ai_completion`, chamadas de ferramentas e logs internos). A trilha pode ser consultada por dois caminhos: o endpoint dedicado `/services/activities` (paginação real, recomendado para investigação) ou o overlay `?include=activities` em `/services/agent-jobs/:id` e `/services/agent-jobs` (conveniência, com caps server-side).
|
|
382
|
+
|
|
383
|
+
### `ActivityRecord` schema
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
{
|
|
387
|
+
id: string; // UUID v4
|
|
388
|
+
org_id: string;
|
|
389
|
+
activity_type_code: string; // código aberto (ex.: "ai_completion")
|
|
390
|
+
status: 'submitted' | 'completed' | 'canceled';
|
|
391
|
+
allocated_credits: number;
|
|
392
|
+
consumed_credits: number;
|
|
393
|
+
credits_rule_id: number; // 0..3
|
|
394
|
+
payloads?: { input?: any; output?: any };
|
|
395
|
+
processed_at?: string; // ISO 8601, opcional
|
|
396
|
+
created_at: string;
|
|
397
|
+
updated_at: string;
|
|
398
|
+
source: {
|
|
399
|
+
type: 'dispatch' | 'process_module' | 'direct';
|
|
400
|
+
reference_id?: string;
|
|
401
|
+
execution_id?: string;
|
|
402
|
+
job_id?: string;
|
|
403
|
+
chat_id?: string;
|
|
404
|
+
agent_job_type_id?: string;
|
|
405
|
+
channel_code?: string;
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
O contador `activities_count` é mantido server-side no próprio documento do agent job (incrementado a cada activity criada com `source.job_id`). Está disponível em qualquer resposta de job, sem necessidade de `?include=activities`.
|
|
411
|
+
|
|
412
|
+
### Endpoint dedicado: `GET /services/activities`
|
|
413
|
+
|
|
414
|
+
Recomendado para consulta paginada da trilha de um job específico. Sem caps de truncamento; suporta filtros server-side adicionais.
|
|
415
|
+
|
|
416
|
+
**Query params:**
|
|
417
|
+
- `job_id` (string, recomendado) — filtra activities do job indicado.
|
|
418
|
+
- `org_id` (string, opcional) — escopo de organização.
|
|
419
|
+
- `status` (`submitted | completed | canceled`, opcional)
|
|
420
|
+
- `activity_type_code` (string, opcional) — código aberto.
|
|
421
|
+
- `source_type` (`dispatch | process_module | direct`, opcional) — filtra pelo `source.type`.
|
|
422
|
+
- `limit` (int, default 50)
|
|
423
|
+
- `offset` (int, default 0)
|
|
424
|
+
- `sort` (string, default `-created_at`)
|
|
425
|
+
|
|
426
|
+
**Resposta:**
|
|
427
|
+
```json
|
|
428
|
+
{
|
|
429
|
+
"data": [ "ActivityRecord", "..." ],
|
|
430
|
+
"meta": { "count": 50, "limit": 50, "total": 1234, "offset": 0 }
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Exemplo:**
|
|
435
|
+
```bash
|
|
436
|
+
curl -X GET "https://api.example.com/services/activities?job_id=job_abc&status=completed&limit=100" \
|
|
437
|
+
-H "Authorization: Bearer YOUR_TOKEN"
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Overlay: `?include=activities` em `/services/agent-jobs/:id`
|
|
441
|
+
|
|
442
|
+
Anexa as activities mais recentes ao detalhe do job. Útil para combinar contexto do job com trilha em uma única round-trip.
|
|
443
|
+
|
|
444
|
+
**Query params adicionais:**
|
|
445
|
+
- `include=activities` (obrigatório para ativar)
|
|
446
|
+
- `include_limit` (int 1–100, default 50) — máximo de activities anexadas.
|
|
447
|
+
- `include_sort` (`created_at | -created_at`, default `-created_at`)
|
|
448
|
+
|
|
449
|
+
**Resposta:**
|
|
450
|
+
```json
|
|
451
|
+
{
|
|
452
|
+
"data": {
|
|
453
|
+
"job_id": "...",
|
|
454
|
+
"status": "completed",
|
|
455
|
+
"activities_count": 1234,
|
|
456
|
+
"Activities": [ "ActivityRecord", "..." ]
|
|
457
|
+
},
|
|
458
|
+
"meta": {
|
|
459
|
+
"activities_meta": { "count": 1234, "limit": 50 }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
`Activities` vem em **PascalCase** por convenção do serializador da API. Quando `meta.activities_meta.count > meta.activities_meta.limit`, a lista foi truncada — use o endpoint dedicado para acessar o restante.
|
|
465
|
+
|
|
466
|
+
### Overlay: `?include=activities` em `/services/agent-jobs` (lista)
|
|
467
|
+
|
|
468
|
+
Anexa activities a cada job da listagem.
|
|
469
|
+
|
|
470
|
+
**Query params adicionais:**
|
|
471
|
+
- `include=activities` (obrigatório)
|
|
472
|
+
- `activities_limit_per_job` (int 1–100, default 15)
|
|
473
|
+
- `activities_total_limit` (int 1–3000, default 500) — cap global agregado.
|
|
474
|
+
- `activities_sort` (`created_at | -created_at`, default `-created_at`)
|
|
475
|
+
|
|
476
|
+
**Resposta:**
|
|
477
|
+
```json
|
|
478
|
+
{
|
|
479
|
+
"data": [ { "Activities": [] } ],
|
|
480
|
+
"meta": {
|
|
481
|
+
"activities_total_returned": 45,
|
|
482
|
+
"activities_total_available": 120,
|
|
483
|
+
"activities_truncated": true
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Comportamento sensível
|
|
489
|
+
|
|
490
|
+
- **Fail-closed**: se a busca de activities falhar internamente quando `include=activities` está ativo, a request **inteira** falha (status 4xx/5xx). Garante integridade da trilha — nunca retorna o job sem as activities solicitadas. Em caso de erro, refaça a request sem `include=activities` ou use o endpoint dedicado.
|
|
491
|
+
- **Cache bypass**: requests com `?include=activities` em `/services/agent-jobs` ignoram o cache server-side, garantindo dados sempre frescos para uso operacional/auditoria.
|
|
492
|
+
- **Activities count sempre disponível**: `activities_count` no objeto do job é mantido independentemente de `include=activities`, então é consultável diretamente em qualquer resposta de job.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiconnect/agentjobs-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for managing Agent Jobs in the AI Connect platform. Developed by AI Connect - Advanced AI automation and integration solutions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
package/build/config.test.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
describe('config.defaultTimezone', () => {
|
|
3
|
-
beforeEach(() => {
|
|
4
|
-
vi.resetModules();
|
|
5
|
-
});
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
vi.unstubAllEnvs();
|
|
8
|
-
vi.resetModules();
|
|
9
|
-
});
|
|
10
|
-
it('reflects DEFAULT_TIMEZONE when set', async () => {
|
|
11
|
-
vi.stubEnv('DEFAULT_TIMEZONE', 'America/Sao_Paulo');
|
|
12
|
-
const { config } = await import('./config.js');
|
|
13
|
-
expect(config.defaultTimezone).toBe('America/Sao_Paulo');
|
|
14
|
-
expect(config.DEFAULT_TIMEZONE).toBe('America/Sao_Paulo');
|
|
15
|
-
});
|
|
16
|
-
it('falls back to UTC when DEFAULT_TIMEZONE is unset', async () => {
|
|
17
|
-
vi.stubEnv('DEFAULT_TIMEZONE', '');
|
|
18
|
-
const { config } = await import('./config.js');
|
|
19
|
-
expect(config.defaultTimezone).toBe('UTC');
|
|
20
|
-
expect(config.DEFAULT_TIMEZONE).toBe('UTC');
|
|
21
|
-
});
|
|
22
|
-
});
|