catelog 1.0.1 → 1.0.3
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/DECISION.md +183 -183
- package/README.md +98 -74
- package/USAGE.md +958 -0
- package/dist/adapters.d.ts +114 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +347 -0
- package/dist/adapters.js.map +1 -0
- package/dist/agent.d.ts +73 -13
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +373 -154
- package/dist/agent.js.map +1 -1
- package/dist/banner.d.ts +6 -3
- package/dist/banner.d.ts.map +1 -1
- package/dist/banner.js +29 -18
- package/dist/banner.js.map +1 -1
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +82 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/security.d.ts +25 -16
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +132 -55
- package/dist/security.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +8 -3
- package/dist/server.js.map +1 -1
- package/dist/sse.d.ts +31 -7
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +94 -26
- package/dist/sse.js.map +1 -1
- package/dist/telemetry.d.ts +41 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +93 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +115 -27
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
package/dist/agent.js
CHANGED
|
@@ -1,40 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { SecurityManager, SecurityVerificationError } from "./security.js";
|
|
3
|
+
import { SSEWriter, NoopStream } from "./sse.js";
|
|
3
4
|
import { toJsonSchema, validateSchemaData } from "./schema.js";
|
|
5
|
+
import { buildExecutionContext } from "./context.js";
|
|
6
|
+
import { TelemetryReporter } from "./telemetry.js";
|
|
4
7
|
import { PROTOCOL_VERSION, } from "./types.js";
|
|
5
8
|
import { startAgentServer } from "./server.js";
|
|
6
|
-
|
|
9
|
+
const asyncJobs = new Map();
|
|
10
|
+
// Clean up old jobs periodically (keep for 1 hour)
|
|
11
|
+
setInterval(() => {
|
|
12
|
+
const cutoff = Date.now() - 3_600_000;
|
|
13
|
+
for (const [id, job] of asyncJobs) {
|
|
14
|
+
if (job.createdAt < cutoff)
|
|
15
|
+
asyncJobs.delete(id);
|
|
16
|
+
}
|
|
17
|
+
}, 300_000).unref();
|
|
18
|
+
// ─────────────────────────────────────────────────
|
|
19
|
+
// CatelogAgent Class
|
|
20
|
+
// ─────────────────────────────────────────────────
|
|
21
|
+
export class CatelogAgent {
|
|
7
22
|
id;
|
|
8
23
|
name;
|
|
9
24
|
version;
|
|
10
25
|
description;
|
|
26
|
+
mode;
|
|
11
27
|
capabilities;
|
|
12
28
|
security;
|
|
13
29
|
config;
|
|
14
30
|
startTime;
|
|
31
|
+
telemetryReporter;
|
|
15
32
|
constructor(config) {
|
|
16
33
|
this.config = config;
|
|
17
34
|
this.name = config.name;
|
|
18
35
|
this.id = config.id || slugify(config.name);
|
|
19
36
|
this.version = config.version || "1.0.0";
|
|
20
|
-
this.description = config.description || `Agent
|
|
37
|
+
this.description = config.description || `Agent: ${this.name}`;
|
|
38
|
+
this.mode = config.mode || "sync";
|
|
21
39
|
this.capabilities = {
|
|
22
40
|
streaming: true,
|
|
23
|
-
delta_stream:
|
|
41
|
+
delta_stream: true,
|
|
24
42
|
max_timeout_seconds: 300,
|
|
43
|
+
execution_modes: [this.mode],
|
|
25
44
|
...config.capabilities,
|
|
26
45
|
};
|
|
27
|
-
this.security = new SecurityManager({
|
|
46
|
+
this.security = new SecurityManager({
|
|
47
|
+
secret: config.secretKey,
|
|
48
|
+
publicKey: config.publicKey,
|
|
49
|
+
});
|
|
28
50
|
this.startTime = Date.now();
|
|
51
|
+
this.telemetryReporter = new TelemetryReporter(config.telemetry);
|
|
29
52
|
}
|
|
53
|
+
// ─────────────────────────────────────────────
|
|
54
|
+
// GET /health — Ping probe for registration
|
|
55
|
+
// ─────────────────────────────────────────────
|
|
30
56
|
getHealth() {
|
|
31
57
|
return {
|
|
32
58
|
status: "ready",
|
|
33
59
|
protocol_version: PROTOCOL_VERSION,
|
|
34
60
|
uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
|
|
35
61
|
environment_ready: true,
|
|
62
|
+
capabilities: this.capabilities,
|
|
36
63
|
};
|
|
37
64
|
}
|
|
65
|
+
// ─────────────────────────────────────────────
|
|
66
|
+
// GET /schema — Agent Manifest
|
|
67
|
+
// ─────────────────────────────────────────────
|
|
38
68
|
getManifest() {
|
|
39
69
|
return {
|
|
40
70
|
id: this.id,
|
|
@@ -43,121 +73,194 @@ export class MarketplaceAgent {
|
|
|
43
73
|
description: this.description,
|
|
44
74
|
protocol_version: PROTOCOL_VERSION,
|
|
45
75
|
capabilities: this.capabilities,
|
|
46
|
-
input_schema:
|
|
47
|
-
|
|
76
|
+
input_schema: this.config.inputSchema
|
|
77
|
+
? toJsonSchema(this.config.inputSchema, "InputSchema")
|
|
78
|
+
: { type: "object", additionalProperties: true },
|
|
79
|
+
output_schema: this.config.outputSchema
|
|
80
|
+
? toJsonSchema(this.config.outputSchema, "OutputSchema")
|
|
81
|
+
: { type: "object", additionalProperties: true },
|
|
48
82
|
};
|
|
49
83
|
}
|
|
50
|
-
|
|
84
|
+
// ─────────────────────────────────────────────
|
|
85
|
+
// .listen(port) — Start the agent server
|
|
86
|
+
// ─────────────────────────────────────────────
|
|
87
|
+
listen(portOrOptions) {
|
|
51
88
|
const opts = typeof portOrOptions === "number" ? { port: portOrOptions } : portOrOptions;
|
|
52
89
|
return startAgentServer(this, opts);
|
|
53
90
|
}
|
|
91
|
+
/** Alias for backward compat */
|
|
92
|
+
serve(portOrOptions) {
|
|
93
|
+
return this.listen(portOrOptions);
|
|
94
|
+
}
|
|
95
|
+
// ─────────────────────────────────────────────
|
|
96
|
+
// Request Router
|
|
97
|
+
// ─────────────────────────────────────────────
|
|
54
98
|
async handleRequest(req, res) {
|
|
55
99
|
const method = req.method?.toUpperCase() || "GET";
|
|
56
100
|
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
57
101
|
const pathname = url.pathname;
|
|
58
|
-
//
|
|
102
|
+
// CORS
|
|
59
103
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
60
104
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
61
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept,
|
|
105
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, " +
|
|
106
|
+
"X-Marketplace-Protocol, X-Marketplace-Timestamp, X-Marketplace-Nonce, " +
|
|
107
|
+
"X-Marketplace-Run-Id, X-Marketplace-Signature");
|
|
62
108
|
if (method === "OPTIONS") {
|
|
63
109
|
res.writeHead(204);
|
|
64
110
|
res.end();
|
|
65
111
|
return;
|
|
66
112
|
}
|
|
67
113
|
try {
|
|
114
|
+
// Health probe
|
|
68
115
|
if (pathname === "/health" && method === "GET") {
|
|
69
|
-
this.
|
|
116
|
+
this.writeJson(res, 200, this.getHealth());
|
|
70
117
|
return;
|
|
71
118
|
}
|
|
119
|
+
// Schema manifest
|
|
72
120
|
if (pathname === "/schema" && method === "GET") {
|
|
73
|
-
this.
|
|
121
|
+
this.writeJson(res, 200, this.getManifest());
|
|
74
122
|
return;
|
|
75
123
|
}
|
|
124
|
+
// Execute
|
|
76
125
|
if (pathname === "/execute" && method === "POST") {
|
|
77
126
|
await this.handleExecute(req, res);
|
|
78
127
|
return;
|
|
79
128
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
// Async job polling: GET /tasks/:id
|
|
130
|
+
if (pathname.startsWith("/tasks/") && method === "GET") {
|
|
131
|
+
const jobId = pathname.slice(7); // Remove "/tasks/"
|
|
132
|
+
this.handleTaskPoll(res, jobId);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.writeJson(res, 404, {
|
|
136
|
+
error: { code: "NOT_FOUND", message: `${method} ${pathname} not found`, retryable: false },
|
|
86
137
|
});
|
|
87
138
|
}
|
|
88
139
|
catch (err) {
|
|
89
140
|
const error = normalizeError(err);
|
|
90
|
-
this.
|
|
91
|
-
error,
|
|
92
|
-
});
|
|
141
|
+
this.writeJson(res, 500, { error });
|
|
93
142
|
}
|
|
94
143
|
}
|
|
144
|
+
// ─────────────────────────────────────────────
|
|
145
|
+
// POST /execute — Core Execution Handler
|
|
146
|
+
// ─────────────────────────────────────────────
|
|
95
147
|
async handleExecute(req, res) {
|
|
96
148
|
const rawBody = await readBody(req);
|
|
97
149
|
const headers = req.headers;
|
|
98
150
|
const path = req.url || "/execute";
|
|
99
151
|
const method = req.method || "POST";
|
|
100
|
-
//
|
|
152
|
+
// ── Authentication (Dual-Mode: Bearer / HMAC / Dev) ──
|
|
101
153
|
let authInfo;
|
|
102
154
|
try {
|
|
103
|
-
authInfo = this.security.
|
|
104
|
-
method,
|
|
105
|
-
path,
|
|
106
|
-
headers,
|
|
107
|
-
rawBody,
|
|
108
|
-
});
|
|
155
|
+
authInfo = this.security.authenticateRequest({ method, path, headers, rawBody });
|
|
109
156
|
}
|
|
110
157
|
catch (err) {
|
|
111
|
-
if (err
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
error: {
|
|
115
|
-
code: secErr.code,
|
|
116
|
-
message: secErr.message,
|
|
117
|
-
retryable: false,
|
|
118
|
-
},
|
|
158
|
+
if (err instanceof SecurityVerificationError) {
|
|
159
|
+
this.writeJson(res, err.statusCode, {
|
|
160
|
+
error: { code: err.code, message: err.message, retryable: false },
|
|
119
161
|
});
|
|
120
162
|
return;
|
|
121
163
|
}
|
|
122
164
|
throw err;
|
|
123
165
|
}
|
|
124
|
-
// Parse
|
|
166
|
+
// ── Parse body ──
|
|
125
167
|
let payload;
|
|
126
168
|
try {
|
|
127
|
-
payload = rawBody.length > 0 ? JSON.parse(rawBody.toString("utf8")) : {};
|
|
169
|
+
payload = rawBody.length > 0 ? JSON.parse(rawBody.toString("utf8")) : { input: {} };
|
|
128
170
|
}
|
|
129
171
|
catch {
|
|
130
|
-
this.
|
|
131
|
-
error: {
|
|
132
|
-
code: "INVALID_INPUT",
|
|
133
|
-
message: "Malformed JSON request body",
|
|
134
|
-
retryable: false,
|
|
135
|
-
},
|
|
172
|
+
this.writeJson(res, 400, {
|
|
173
|
+
error: { code: "INVALID_INPUT", message: "Malformed JSON body", retryable: false },
|
|
136
174
|
});
|
|
137
175
|
return;
|
|
138
176
|
}
|
|
139
177
|
const runId = payload.run_id || authInfo.runId;
|
|
178
|
+
const inputBytes = rawBody.length;
|
|
179
|
+
// ── Optional input validation (only if schema provided) ──
|
|
180
|
+
let validatedInput;
|
|
181
|
+
if (this.config.inputSchema) {
|
|
182
|
+
const validation = validateSchemaData(this.config.inputSchema, payload.input);
|
|
183
|
+
if (!validation.success) {
|
|
184
|
+
this.writeJson(res, 400, {
|
|
185
|
+
run_id: runId,
|
|
186
|
+
status: "failed",
|
|
187
|
+
duration_ms: 0,
|
|
188
|
+
error: {
|
|
189
|
+
code: "INVALID_INPUT",
|
|
190
|
+
message: `Validation failed: ${validation.errors.join("; ")}`,
|
|
191
|
+
retryable: false,
|
|
192
|
+
details: { validationErrors: validation.errors },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
validatedInput = validation.data;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// No schema — accept any input
|
|
201
|
+
validatedInput = (payload.input ?? {});
|
|
202
|
+
}
|
|
140
203
|
this.security.markRunStarted(runId);
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
204
|
+
// ── Execution Mode Dispatch ──
|
|
205
|
+
if (this.mode === "async") {
|
|
206
|
+
await this.executeAsync(runId, validatedInput, payload, inputBytes, res);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// sync & conversational both stream via SSE or batch JSON
|
|
210
|
+
await this.executeSyncOrConversational(runId, validatedInput, payload, inputBytes, req, res);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ── Async Mode: Return 202 immediately, run in background ──
|
|
214
|
+
async executeAsync(runId, input, payload, inputBytes, res) {
|
|
215
|
+
const job = {
|
|
216
|
+
runId,
|
|
217
|
+
status: "working",
|
|
218
|
+
createdAt: Date.now(),
|
|
219
|
+
};
|
|
220
|
+
asyncJobs.set(runId, job);
|
|
221
|
+
// Respond immediately with 202
|
|
222
|
+
this.writeJson(res, 202, {
|
|
223
|
+
run_id: runId,
|
|
224
|
+
status: "working",
|
|
225
|
+
poll_url: `/tasks/${runId}`,
|
|
226
|
+
});
|
|
227
|
+
// Execute in background (fire-and-forget from HTTP perspective)
|
|
228
|
+
const startTs = Date.now();
|
|
229
|
+
const noopStream = new NoopStream();
|
|
230
|
+
const abortController = new AbortController();
|
|
231
|
+
try {
|
|
232
|
+
const { ctx, telemetryCollector } = buildExecutionContext({
|
|
233
|
+
runId,
|
|
234
|
+
input,
|
|
235
|
+
stream: noopStream,
|
|
236
|
+
signal: abortController.signal,
|
|
237
|
+
metadata: payload.metadata,
|
|
238
|
+
deadlineMs: payload.deadline_ms,
|
|
239
|
+
resumeToken: payload.resume_token,
|
|
240
|
+
inputBytes,
|
|
155
241
|
});
|
|
156
|
-
|
|
242
|
+
const rawResult = await this.config.execute(ctx);
|
|
243
|
+
const durationMs = Date.now() - startTs;
|
|
244
|
+
const result = this.normalizeExecuteResult(rawResult);
|
|
245
|
+
job.status = "completed";
|
|
246
|
+
job.output = result.output;
|
|
247
|
+
job.durationMs = durationMs;
|
|
248
|
+
job.telemetry = telemetryCollector.finalize(result.telemetry);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
job.status = "failed";
|
|
252
|
+
job.error = normalizeError(err);
|
|
253
|
+
job.durationMs = Date.now() - startTs;
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
this.security.markRunCompleted(runId);
|
|
157
257
|
}
|
|
158
|
-
|
|
258
|
+
}
|
|
259
|
+
// ── Sync / Conversational Mode ──
|
|
260
|
+
async executeSyncOrConversational(runId, input, payload, inputBytes, req, res) {
|
|
261
|
+
const startTs = Date.now();
|
|
159
262
|
const acceptsSse = req.headers.accept?.includes("text/event-stream") ?? true;
|
|
160
|
-
//
|
|
263
|
+
// Abort controller for deadline / disconnect
|
|
161
264
|
const abortController = new AbortController();
|
|
162
265
|
let timeoutTimer;
|
|
163
266
|
if (payload.deadline_ms && payload.deadline_ms > Date.now()) {
|
|
@@ -167,89 +270,205 @@ export class MarketplaceAgent {
|
|
|
167
270
|
}, waitTime);
|
|
168
271
|
}
|
|
169
272
|
if (acceptsSse) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
273
|
+
await this.executeWithSSE(runId, input, payload, inputBytes, startTs, res, abortController, timeoutTimer);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
await this.executeWithJSON(runId, input, payload, inputBytes, startTs, res, abortController, timeoutTimer);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// ── SSE Streaming Execution ──
|
|
280
|
+
async executeWithSSE(runId, input, payload, inputBytes, startTs, res, abortController, timeoutTimer) {
|
|
281
|
+
const sseWriter = new SSEWriter(runId, res, {
|
|
282
|
+
onClientDisconnect: () => abortController.abort(new Error("CLIENT_DISCONNECTED")),
|
|
283
|
+
});
|
|
284
|
+
sseWriter.signal.addEventListener("abort", () => {
|
|
285
|
+
abortController.abort(sseWriter.signal.reason);
|
|
286
|
+
});
|
|
287
|
+
// Emit initial working status
|
|
288
|
+
sseWriter.sendTaskStatus("working");
|
|
289
|
+
const { ctx, telemetryCollector } = buildExecutionContext({
|
|
290
|
+
runId,
|
|
291
|
+
input,
|
|
292
|
+
stream: sseWriter,
|
|
293
|
+
signal: abortController.signal,
|
|
294
|
+
metadata: payload.metadata,
|
|
295
|
+
deadlineMs: payload.deadline_ms,
|
|
296
|
+
resumeToken: payload.resume_token,
|
|
297
|
+
inputBytes,
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const rawResult = await this.config.execute(ctx);
|
|
301
|
+
if (timeoutTimer)
|
|
302
|
+
clearTimeout(timeoutTimer);
|
|
303
|
+
const durationMs = Date.now() - startTs;
|
|
304
|
+
// Check if result is a needInput request
|
|
305
|
+
if (this.isNeedInputResult(rawResult)) {
|
|
306
|
+
const needInput = rawResult;
|
|
307
|
+
const resumeToken = `resume_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
308
|
+
sseWriter.sendTaskInputRequired(needInput.prompt, resumeToken, needInput.schema);
|
|
309
|
+
this.reportTelemetry(runId, "input-required", telemetryCollector.finalize(), durationMs);
|
|
194
310
|
this.security.markRunCompleted(runId);
|
|
195
|
-
|
|
311
|
+
return;
|
|
196
312
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
313
|
+
const result = this.normalizeExecuteResult(rawResult);
|
|
314
|
+
const outputBytes = JSON.stringify(result.output).length;
|
|
315
|
+
const telemetry = telemetryCollector.finalize({
|
|
316
|
+
...result.telemetry,
|
|
317
|
+
duration_ms: durationMs,
|
|
318
|
+
output_bytes: outputBytes,
|
|
319
|
+
chunks_streamed: sseWriter.chunksStreamed,
|
|
320
|
+
});
|
|
321
|
+
res.setHeader("X-Catelog-Duration-Ms", String(durationMs));
|
|
322
|
+
sseWriter.sendTaskCompleted(result.output, durationMs, telemetry);
|
|
323
|
+
this.reportTelemetry(runId, "completed", telemetry, durationMs);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
if (timeoutTimer)
|
|
327
|
+
clearTimeout(timeoutTimer);
|
|
328
|
+
const durationMs = Date.now() - startTs;
|
|
329
|
+
if (abortController.signal.aborted) {
|
|
330
|
+
sseWriter.sendTaskCanceled(durationMs);
|
|
331
|
+
this.reportTelemetry(runId, "canceled", telemetryCollector.finalize({ duration_ms: durationMs }), durationMs);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
202
334
|
const error = normalizeError(err);
|
|
203
|
-
sseWriter.
|
|
335
|
+
sseWriter.sendTaskFailed(error, durationMs);
|
|
336
|
+
this.reportTelemetry(runId, "failed", telemetryCollector.finalize({ duration_ms: durationMs }), durationMs);
|
|
204
337
|
}
|
|
205
338
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
telemetry: result.telemetry,
|
|
233
|
-
};
|
|
234
|
-
this.writeJsonResponse(res, 200, response);
|
|
235
|
-
}
|
|
236
|
-
catch (err) {
|
|
237
|
-
if (timeoutTimer)
|
|
238
|
-
clearTimeout(timeoutTimer);
|
|
239
|
-
const durationMs = Date.now() - startTimestamp;
|
|
339
|
+
finally {
|
|
340
|
+
this.security.markRunCompleted(runId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// ── Batch JSON Execution ──
|
|
344
|
+
async executeWithJSON(runId, input, payload, inputBytes, startTs, res, abortController, timeoutTimer) {
|
|
345
|
+
const noopStream = new NoopStream();
|
|
346
|
+
const { ctx, telemetryCollector } = buildExecutionContext({
|
|
347
|
+
runId,
|
|
348
|
+
input,
|
|
349
|
+
stream: noopStream,
|
|
350
|
+
signal: abortController.signal,
|
|
351
|
+
metadata: payload.metadata,
|
|
352
|
+
deadlineMs: payload.deadline_ms,
|
|
353
|
+
resumeToken: payload.resume_token,
|
|
354
|
+
inputBytes,
|
|
355
|
+
});
|
|
356
|
+
try {
|
|
357
|
+
const rawResult = await this.config.execute(ctx);
|
|
358
|
+
if (timeoutTimer)
|
|
359
|
+
clearTimeout(timeoutTimer);
|
|
360
|
+
const durationMs = Date.now() - startTs;
|
|
361
|
+
// Check needInput
|
|
362
|
+
if (this.isNeedInputResult(rawResult)) {
|
|
363
|
+
const needInput = rawResult;
|
|
364
|
+
const resumeToken = `resume_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
240
365
|
this.security.markRunCompleted(runId);
|
|
241
|
-
|
|
242
|
-
const response = {
|
|
366
|
+
this.writeJson(res, 200, {
|
|
243
367
|
run_id: runId,
|
|
244
|
-
status: "
|
|
368
|
+
status: "input-required",
|
|
245
369
|
duration_ms: durationMs,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
370
|
+
prompt: needInput.prompt,
|
|
371
|
+
input_schema: needInput.schema,
|
|
372
|
+
resume_token: resumeToken,
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
249
375
|
}
|
|
376
|
+
const result = this.normalizeExecuteResult(rawResult);
|
|
377
|
+
const outputBytes = JSON.stringify(result.output).length;
|
|
378
|
+
const telemetry = telemetryCollector.finalize({
|
|
379
|
+
...result.telemetry,
|
|
380
|
+
duration_ms: durationMs,
|
|
381
|
+
output_bytes: outputBytes,
|
|
382
|
+
chunks_streamed: noopStream.chunksStreamed,
|
|
383
|
+
});
|
|
384
|
+
this.security.markRunCompleted(runId);
|
|
385
|
+
res.setHeader("X-Catelog-Duration-Ms", String(durationMs));
|
|
386
|
+
const response = {
|
|
387
|
+
run_id: runId,
|
|
388
|
+
status: "completed",
|
|
389
|
+
duration_ms: durationMs,
|
|
390
|
+
output: result.output,
|
|
391
|
+
telemetry,
|
|
392
|
+
};
|
|
393
|
+
this.writeJson(res, 200, response);
|
|
394
|
+
this.reportTelemetry(runId, "completed", telemetry, durationMs);
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
if (timeoutTimer)
|
|
398
|
+
clearTimeout(timeoutTimer);
|
|
399
|
+
const durationMs = Date.now() - startTs;
|
|
400
|
+
this.security.markRunCompleted(runId);
|
|
401
|
+
const error = normalizeError(err);
|
|
402
|
+
const response = {
|
|
403
|
+
run_id: runId,
|
|
404
|
+
status: abortController.signal.aborted ? "canceled" : "failed",
|
|
405
|
+
duration_ms: durationMs,
|
|
406
|
+
error,
|
|
407
|
+
};
|
|
408
|
+
this.writeJson(res, 200, response);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// ── Async Job Polling ──
|
|
412
|
+
handleTaskPoll(res, jobId) {
|
|
413
|
+
const job = asyncJobs.get(jobId);
|
|
414
|
+
if (!job) {
|
|
415
|
+
this.writeJson(res, 404, {
|
|
416
|
+
error: { code: "NOT_FOUND", message: `No task found: ${jobId}`, retryable: false },
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (job.status === "working") {
|
|
421
|
+
this.writeJson(res, 200, {
|
|
422
|
+
run_id: job.runId,
|
|
423
|
+
status: "working",
|
|
424
|
+
elapsed_ms: Date.now() - job.createdAt,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
else if (job.status === "completed") {
|
|
428
|
+
this.writeJson(res, 200, {
|
|
429
|
+
run_id: job.runId,
|
|
430
|
+
status: "completed",
|
|
431
|
+
duration_ms: job.durationMs,
|
|
432
|
+
output: job.output,
|
|
433
|
+
telemetry: job.telemetry,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
this.writeJson(res, 200, {
|
|
438
|
+
run_id: job.runId,
|
|
439
|
+
status: "failed",
|
|
440
|
+
duration_ms: job.durationMs,
|
|
441
|
+
error: job.error,
|
|
442
|
+
});
|
|
250
443
|
}
|
|
251
444
|
}
|
|
252
|
-
|
|
445
|
+
// ── Helpers ──
|
|
446
|
+
/**
|
|
447
|
+
* Normalize the developer's return value into our standard result shape.
|
|
448
|
+
* Supports:
|
|
449
|
+
* - { output: T, telemetry?: ... } → standard result
|
|
450
|
+
* - Any other value → treated as the output directly
|
|
451
|
+
*/
|
|
452
|
+
normalizeExecuteResult(raw) {
|
|
453
|
+
if (raw && typeof raw === "object" && "output" in raw) {
|
|
454
|
+
return raw;
|
|
455
|
+
}
|
|
456
|
+
// The developer just returned a plain object — that IS the output
|
|
457
|
+
return { output: raw };
|
|
458
|
+
}
|
|
459
|
+
isNeedInputResult(value) {
|
|
460
|
+
return value && typeof value === "object" && value[Symbol.for("catelog.needInput")] === true;
|
|
461
|
+
}
|
|
462
|
+
reportTelemetry(runId, status, telemetry, durationMs) {
|
|
463
|
+
this.telemetryReporter.report({
|
|
464
|
+
agentId: this.id,
|
|
465
|
+
runId,
|
|
466
|
+
status,
|
|
467
|
+
telemetry: { ...telemetry, duration_ms: durationMs },
|
|
468
|
+
timestamp: Date.now(),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
writeJson(res, status, data) {
|
|
253
472
|
if (res.writableEnded)
|
|
254
473
|
return;
|
|
255
474
|
const body = JSON.stringify(data);
|
|
@@ -260,12 +479,22 @@ export class MarketplaceAgent {
|
|
|
260
479
|
res.end(body);
|
|
261
480
|
}
|
|
262
481
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
482
|
+
// ─────────────────────────────────────────────────
|
|
483
|
+
// Factory Function
|
|
484
|
+
// ─────────────────────────────────────────────────
|
|
485
|
+
/** Create a new CatelogAgent */
|
|
486
|
+
export function createAgent(config) {
|
|
487
|
+
return new CatelogAgent(config);
|
|
488
|
+
}
|
|
489
|
+
/** Legacy alias */
|
|
266
490
|
export function createMarketplaceAgent(config) {
|
|
267
|
-
return new
|
|
491
|
+
return new CatelogAgent(config);
|
|
268
492
|
}
|
|
493
|
+
// Backward compat alias
|
|
494
|
+
export { CatelogAgent as MarketplaceAgent };
|
|
495
|
+
// ─────────────────────────────────────────────────
|
|
496
|
+
// Utilities
|
|
497
|
+
// ─────────────────────────────────────────────────
|
|
269
498
|
function slugify(text) {
|
|
270
499
|
return text
|
|
271
500
|
.toLowerCase()
|
|
@@ -277,26 +506,16 @@ function slugify(text) {
|
|
|
277
506
|
function normalizeError(err, defaultCode = "INTERNAL_CRASH") {
|
|
278
507
|
if (err instanceof Error) {
|
|
279
508
|
if (err.name === "AbortError" || err.message === "EXECUTION_TIMEOUT") {
|
|
280
|
-
return {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
};
|
|
509
|
+
return { code: "EXECUTION_TIMEOUT", message: "Exceeded deadline or timeout", retryable: true };
|
|
510
|
+
}
|
|
511
|
+
if (err.message === "CLIENT_DISCONNECTED") {
|
|
512
|
+
return { code: "TASK_CANCELED", message: "Client disconnected", retryable: false };
|
|
285
513
|
}
|
|
286
|
-
// Check for standard error codes attached to error object
|
|
287
514
|
const code = err.code || defaultCode;
|
|
288
515
|
const retryable = err.retryable ?? false;
|
|
289
|
-
return {
|
|
290
|
-
code,
|
|
291
|
-
message: err.message,
|
|
292
|
-
retryable,
|
|
293
|
-
};
|
|
516
|
+
return { code, message: err.message, retryable };
|
|
294
517
|
}
|
|
295
|
-
return {
|
|
296
|
-
code: defaultCode,
|
|
297
|
-
message: String(err),
|
|
298
|
-
retryable: false,
|
|
299
|
-
};
|
|
518
|
+
return { code: defaultCode, message: String(err), retryable: false };
|
|
300
519
|
}
|
|
301
520
|
async function readBody(req) {
|
|
302
521
|
return new Promise((resolve, reject) => {
|