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/dist/agent.js CHANGED
@@ -1,40 +1,70 @@
1
- import { SecurityManager } from "./security.js";
2
- import { SSEWriter } from "./sse.js";
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
- export class MarketplaceAgent {
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 for ${this.name}`;
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: false,
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({ secret: config.secret });
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: toJsonSchema(this.config.inputSchema, "InputSchema"),
47
- output_schema: toJsonSchema(this.config.outputSchema, "OutputSchema"),
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
- serve(portOrOptions) {
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
- // Enable CORS for testing & browser callers
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, X-Marketplace-Protocol, X-Marketplace-Timestamp, X-Marketplace-Nonce, X-Marketplace-Run-Id, X-Marketplace-Signature");
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.writeJsonResponse(res, 200, this.getHealth());
116
+ this.writeJson(res, 200, this.getHealth());
70
117
  return;
71
118
  }
119
+ // Schema manifest
72
120
  if (pathname === "/schema" && method === "GET") {
73
- this.writeJsonResponse(res, 200, this.getManifest());
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
- this.writeJsonResponse(res, 404, {
81
- error: {
82
- code: "NOT_FOUND",
83
- message: `Endpoint ${method} ${pathname} not found`,
84
- retryable: false,
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.writeJsonResponse(res, 500, {
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
- // 1. Security & Signature verification
152
+ // ── Authentication (Dual-Mode: Bearer / HMAC / Dev) ──
101
153
  let authInfo;
102
154
  try {
103
- authInfo = this.security.verifyRequest({
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 && typeof err === "object" && "code" in err && "statusCode" in err) {
112
- const secErr = err;
113
- this.writeJsonResponse(res, secErr.statusCode, {
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 request body
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.writeJsonResponse(res, 400, {
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
- // 2. Pre-Execution Schema Validation
142
- const validation = validateSchemaData(this.config.inputSchema, payload.input);
143
- if (!validation.success) {
144
- this.security.markRunCompleted(runId);
145
- this.writeJsonResponse(res, 400, {
146
- run_id: runId,
147
- status: "failed",
148
- duration_ms: 0,
149
- error: {
150
- code: "INVALID_INPUT",
151
- message: `Input validation failed: ${validation.errors.join("; ")}`,
152
- retryable: false,
153
- details: { validationErrors: validation.errors },
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
- return;
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
- const startTimestamp = Date.now();
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
- // Setup deadline timeout if specified
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
- // Streaming SSE Response Mode
171
- const sseWriter = new SSEWriter(runId, res, {
172
- onClientDisconnect: () => {
173
- abortController.abort(new Error("CLIENT_DISCONNECTED"));
174
- },
175
- });
176
- // Forward abort signal
177
- sseWriter.signal.addEventListener("abort", () => {
178
- abortController.abort(sseWriter.signal.reason);
179
- });
180
- // Send initial run.started event
181
- sseWriter.sendRunStarted();
182
- try {
183
- const result = await this.config.execute({
184
- runId,
185
- input: validation.data,
186
- stream: sseWriter,
187
- signal: abortController.signal,
188
- metadata: payload.metadata,
189
- deadlineMs: payload.deadline_ms,
190
- });
191
- if (timeoutTimer)
192
- clearTimeout(timeoutTimer);
193
- const durationMs = Date.now() - startTimestamp;
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
- sseWriter.sendRunSucceeded(result.output, durationMs, result.telemetry);
311
+ return;
196
312
  }
197
- catch (err) {
198
- if (timeoutTimer)
199
- clearTimeout(timeoutTimer);
200
- const durationMs = Date.now() - startTimestamp;
201
- this.security.markRunCompleted(runId);
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.sendRunFailed(error, durationMs);
335
+ sseWriter.sendTaskFailed(error, durationMs);
336
+ this.reportTelemetry(runId, "failed", telemetryCollector.finalize({ duration_ms: durationMs }), durationMs);
204
337
  }
205
338
  }
206
- else {
207
- // Batch JSON Mode
208
- try {
209
- const noopStream = {
210
- step: () => { },
211
- delta: () => { },
212
- emit: () => { },
213
- isClosed: () => false,
214
- };
215
- const result = await this.config.execute({
216
- runId,
217
- input: validation.data,
218
- stream: noopStream,
219
- signal: abortController.signal,
220
- metadata: payload.metadata,
221
- deadlineMs: payload.deadline_ms,
222
- });
223
- if (timeoutTimer)
224
- clearTimeout(timeoutTimer);
225
- const durationMs = Date.now() - startTimestamp;
226
- this.security.markRunCompleted(runId);
227
- const response = {
228
- run_id: runId,
229
- status: "succeeded",
230
- duration_ms: durationMs,
231
- output: result.output,
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
- const error = normalizeError(err);
242
- const response = {
366
+ this.writeJson(res, 200, {
243
367
  run_id: runId,
244
- status: "failed",
368
+ status: "input-required",
245
369
  duration_ms: durationMs,
246
- error,
247
- };
248
- this.writeJsonResponse(res, 200, response);
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
- writeJsonResponse(res, status, data) {
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
- * Convenience builder function for creating a compliant Marketplace Agent
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 MarketplaceAgent(config);
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
- code: "EXECUTION_TIMEOUT",
282
- message: "Execution exceeded allowed deadline or timeout",
283
- retryable: true,
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) => {