catelog 1.0.0 → 1.0.2

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/DECISION.md +183 -183
  2. package/README.md +98 -74
  3. package/USAGE.md +958 -0
  4. package/dist/adapters.d.ts +84 -0
  5. package/dist/adapters.d.ts.map +1 -0
  6. package/dist/adapters.js +267 -0
  7. package/dist/adapters.js.map +1 -0
  8. package/dist/agent.d.ts +73 -13
  9. package/dist/agent.d.ts.map +1 -1
  10. package/dist/agent.js +373 -154
  11. package/dist/agent.js.map +1 -1
  12. package/dist/banner.d.ts +6 -3
  13. package/dist/banner.d.ts.map +1 -1
  14. package/dist/banner.js +29 -18
  15. package/dist/banner.js.map +1 -1
  16. package/dist/context.d.ts +18 -0
  17. package/dist/context.d.ts.map +1 -0
  18. package/dist/context.js +82 -0
  19. package/dist/context.js.map +1 -0
  20. package/dist/index.d.ts +9 -6
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +24 -6
  23. package/dist/index.js.map +1 -1
  24. package/dist/schema.d.ts +4 -4
  25. package/dist/schema.d.ts.map +1 -1
  26. package/dist/schema.js +46 -20
  27. package/dist/schema.js.map +1 -1
  28. package/dist/security.d.ts +25 -16
  29. package/dist/security.d.ts.map +1 -1
  30. package/dist/security.js +132 -55
  31. package/dist/security.js.map +1 -1
  32. package/dist/server.d.ts +2 -2
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.js +8 -3
  35. package/dist/server.js.map +1 -1
  36. package/dist/sse.d.ts +31 -7
  37. package/dist/sse.d.ts.map +1 -1
  38. package/dist/sse.js +94 -26
  39. package/dist/sse.js.map +1 -1
  40. package/dist/telemetry.d.ts +41 -0
  41. package/dist/telemetry.d.ts.map +1 -0
  42. package/dist/telemetry.js +93 -0
  43. package/dist/telemetry.js.map +1 -0
  44. package/dist/types.d.ts +115 -27
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +4 -0
  47. package/dist/types.js.map +1 -1
  48. package/package.json +5 -2
package/USAGE.md ADDED
@@ -0,0 +1,958 @@
1
+ # Catelog SDK — Usage Guide
2
+
3
+ > **Node.js 18+ · TypeScript 5+ · ESM**
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Installation](#1-installation)
10
+ 2. [Environment Setup](#2-environment-setup)
11
+ 3. [The Minimal Agent](#3-the-minimal-agent)
12
+ 4. [Agent Types](#4-agent-types)
13
+ - [Sync Agent](#41-sync-agent--fast-direct-response)
14
+ - [Async Agent](#42-async-agent--long-running-background-jobs)
15
+ - [Conversational Agent](#43-conversational-agent--multi-turn--hitl)
16
+ 5. [Framework Integrations](#5-framework-integrations)
17
+ - [OpenAI SDK](#51-openai-sdk)
18
+ - [Vercel AI SDK](#52-vercel-ai-sdk)
19
+ - [LangGraph / LangChain](#53-langgraph--langchain)
20
+ - [CrewAI / Custom Generators](#54-crewaicustom-async-generator)
21
+ - [Vanilla TypeScript](#55-vanilla-typescript-no-framework)
22
+ 6. [Streaming Helpers](#6-streaming-helpers)
23
+ 7. [Schema Definition](#7-schema-definition)
24
+ 8. [API Keys](#8-api-keys)
25
+ 9. [Telemetry & Logging](#9-telemetry--logging)
26
+ 10. [What You Get — Endpoints](#10-what-you-get--endpoints)
27
+ 11. [Testing Locally](#11-testing-locally)
28
+ 12. [Registering on the Marketplace](#12-registering-on-the-marketplace)
29
+ 13. [Error Handling](#13-error-handling)
30
+
31
+ ---
32
+
33
+ ## 1. Installation
34
+
35
+ ```bash
36
+ # pnpm (recommended)
37
+ pnpm add catelog zod
38
+
39
+ # npm
40
+ npm install catelog zod
41
+
42
+ # yarn
43
+ yarn add catelog zod
44
+
45
+ # bun
46
+ bun add catelog zod
47
+ ```
48
+
49
+ > **Note:** `zod` is optional. Only needed if you want typed input validation and auto-generated marketplace forms. If you skip it, the SDK accepts any JSON payload.
50
+
51
+ ---
52
+
53
+ ## 2. Environment Setup
54
+
55
+ Create a `.env` file in your project root:
56
+
57
+ ```env
58
+ # Required for production authentication
59
+ CATELOG_SECRET_KEY=cat_sec_live_your_secret_key_here
60
+
61
+ # Optional — used for public/read-only endpoint auth
62
+ CATELOG_PUBLIC_KEY=cat_pub_live_your_public_key_here
63
+
64
+ # Optional — telemetry reporting endpoint
65
+ CATELOG_TELEMETRY_URL=https://telemetry.catelog.io/ingest
66
+
67
+ # Optional — disable telemetry entirely
68
+ CATELOG_TELEMETRY_DISABLED=false
69
+
70
+ # Optional — silence the startup banner
71
+ CATELOG_SILENT=false
72
+
73
+ # Standard Node.js — controls dev/production behavior
74
+ NODE_ENV=development
75
+ ```
76
+
77
+ The SDK auto-reads all of these. You never need to pass them manually in code.
78
+
79
+ > **In development** (`NODE_ENV=development`), you can skip all keys entirely. The SDK runs in permissive dev mode and logs a warning.
80
+
81
+ ---
82
+
83
+ ## 3. The Minimal Agent
84
+
85
+ The absolute minimum to publish a working agent:
86
+
87
+ ```ts
88
+ import { createAgent } from "catelog";
89
+
90
+ const agent = createAgent({
91
+ name: "Hello World",
92
+ async execute({ input }) {
93
+ return { message: `Hello, ${(input as any).name ?? "world"}!` };
94
+ },
95
+ });
96
+
97
+ agent.listen(3000);
98
+ ```
99
+
100
+ That's it. You get:
101
+ - ✅ `GET /health` — ping probe with capabilities
102
+ - ✅ `GET /schema` — auto-generated agent manifest
103
+ - ✅ `POST /execute` — authenticated execution endpoint
104
+ - ✅ SSE streaming with heartbeat keep-alives
105
+ - ✅ Dev mode auth (no keys required in development)
106
+ - ✅ Beautiful terminal startup banner
107
+
108
+ ---
109
+
110
+ ## 4. Agent Types
111
+
112
+ Choose the execution mode that matches your agent's behavior.
113
+
114
+ ---
115
+
116
+ ### 4.1 Sync Agent — Fast, Direct Response
117
+
118
+ **Best for:** Tool calls, Q&A, text generation, summarization, search — anything that completes within ~30 seconds.
119
+
120
+ **How it works:** The client opens a connection, the agent streams events, and the connection closes when the task completes.
121
+
122
+ ```ts
123
+ import { createAgent } from "catelog";
124
+ import { z } from "zod";
125
+
126
+ const agent = createAgent({
127
+ name: "Web Summarizer",
128
+ mode: "sync", // default — can be omitted
129
+
130
+ inputSchema: z.object({
131
+ url: z.string().url().describe("The URL to summarize"),
132
+ }),
133
+
134
+ outputSchema: z.object({
135
+ title: z.string(),
136
+ summary: z.string(),
137
+ readingTimeMinutes: z.number(),
138
+ }),
139
+
140
+ async execute({ input, stream, log, signal }) {
141
+ // Emit progress — does NOT complete the task
142
+ stream.progress(10, "Fetching page content...");
143
+ log.info("Fetching URL", { url: input.url });
144
+
145
+ const html = await fetch(input.url, { signal }).then((r) => r.text());
146
+
147
+ stream.progress(50, "Summarizing with AI...");
148
+ // stream.delta() pipes text live to the buyer UI
149
+ stream.delta("Analyzing page structure...\n");
150
+
151
+ const summary = await summarizeWithAI(html, { signal });
152
+
153
+ stream.progress(90, "Formatting output...");
154
+ stream.delta(summary);
155
+
156
+ // return completes the task automatically
157
+ return {
158
+ output: {
159
+ title: "Example Page",
160
+ summary,
161
+ readingTimeMinutes: Math.ceil(summary.split(" ").length / 200),
162
+ },
163
+ };
164
+ },
165
+ });
166
+
167
+ agent.listen(3000);
168
+ ```
169
+
170
+ **What the marketplace receives:**
171
+
172
+ ```
173
+ task.status → "working" (automatic on start)
174
+ task.progress → 10%, "Fetching page content..."
175
+ task.progress → 50%, "Summarizing with AI..."
176
+ task.delta → "Analyzing page structure..."
177
+ task.delta → "The page covers..."
178
+ task.progress → 90%, "Formatting output..."
179
+ task.completed → { title, summary, readingTimeMinutes } + telemetry
180
+ ```
181
+
182
+ ---
183
+
184
+ ### 4.2 Async Agent — Long-Running Background Jobs
185
+
186
+ **Best for:** Deep research, PDF generation, web scraping, multi-agent workflows — anything that takes more than 30 seconds or risks hitting HTTP gateway timeouts.
187
+
188
+ **How it works:** The SDK immediately returns `202 Accepted` with a job ID. The marketplace polls `GET /tasks/:id` until the job completes. You write normal `async/await` code — the SDK handles the polling infrastructure.
189
+
190
+ ```ts
191
+ import { createAgent } from "catelog";
192
+ import { z } from "zod";
193
+
194
+ const agent = createAgent({
195
+ name: "Deep Research Agent",
196
+ mode: "async",
197
+
198
+ inputSchema: z.object({
199
+ topic: z.string().describe("Research topic or company name"),
200
+ depth: z.enum(["quick", "deep", "comprehensive"]).default("deep"),
201
+ }),
202
+
203
+ outputSchema: z.object({
204
+ report: z.string(),
205
+ sources: z.array(z.string()),
206
+ wordCount: z.number(),
207
+ }),
208
+
209
+ async execute({ input, log }) {
210
+ // The client already has the 202 + job ID
211
+ // Write your long-running logic normally here:
212
+ log.info("Starting deep research", { topic: input.topic, depth: input.depth });
213
+
214
+ const sources = await crawlAndScrape(input.topic); // May take minutes
215
+ const report = await generateReport(sources, input.depth);
216
+
217
+ log.info("Research complete", { sources: sources.length });
218
+
219
+ return {
220
+ output: {
221
+ report,
222
+ sources: sources.map((s) => s.url),
223
+ wordCount: report.split(" ").length,
224
+ },
225
+ telemetry: {
226
+ model_used: "gpt-4o",
227
+ llm_tokens: { total: 12000 },
228
+ },
229
+ };
230
+ },
231
+ });
232
+
233
+ agent.listen(3000);
234
+ ```
235
+
236
+ **The request / response cycle:**
237
+
238
+ ```
239
+ POST /execute
240
+ → 202 Accepted
241
+ { "run_id": "run_abc", "status": "working", "poll_url": "/tasks/run_abc" }
242
+
243
+ GET /tasks/run_abc (marketplace polls every few seconds)
244
+ → 200 { "status": "working", "elapsed_ms": 14200 }
245
+
246
+ GET /tasks/run_abc (when done)
247
+ → 200 {
248
+ "status": "completed",
249
+ "output": { "report": "...", "sources": [...], "wordCount": 4200 },
250
+ "telemetry": { ... }
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ### 4.3 Conversational Agent — Multi-Turn & HITL
257
+
258
+ **Best for:** Document assistants, workflow agents, code review bots, or any agent that needs to ask the user a clarifying question mid-run before proceeding.
259
+
260
+ **How it works:** The agent calls `ctx.needInput({...})` to suspend execution and emit a `task.input_required` SSE event with a resume token. The marketplace shows a form to the buyer. When the buyer submits, the agent is re-invoked with the resume token and the previous session state intact.
261
+
262
+ ```ts
263
+ import { createAgent } from "catelog";
264
+ import { z } from "zod";
265
+
266
+ const agent = createAgent({
267
+ name: "SQL Query Assistant",
268
+ mode: "conversational",
269
+
270
+ inputSchema: z.object({
271
+ question: z.string().describe("What would you like to know?"),
272
+ // resume fields — optional, provided on second invocation
273
+ selectedTable: z.string().optional(),
274
+ }),
275
+
276
+ async execute({ input, ctx, stream, log }) {
277
+ // ── First invocation: user sends { question: "show me orders" } ──
278
+ if (!input.selectedTable) {
279
+ stream.progress(20, "Checking available tables...");
280
+ const tables = await getAvailableTables();
281
+ log.info("Tables discovered", { count: tables.length });
282
+
283
+ // Suspend and ask for input — does NOT complete the task
284
+ return ctx.needInput({
285
+ prompt: `I found ${tables.length} tables. Which one would you like to query?`,
286
+ schema: {
287
+ type: "object",
288
+ properties: {
289
+ selectedTable: {
290
+ type: "string",
291
+ enum: tables,
292
+ description: "Choose the table to query",
293
+ },
294
+ },
295
+ required: ["selectedTable"],
296
+ },
297
+ });
298
+ }
299
+
300
+ // ── Second invocation: user submits { question: "...", selectedTable: "orders" } ──
301
+ stream.progress(30, `Querying ${input.selectedTable}...`);
302
+ log.info("Querying table", { table: input.selectedTable });
303
+
304
+ const results = await runQuery(input.selectedTable, input.question);
305
+
306
+ // Session state is available across turns
307
+ const previousQueries = ctx.session.get<string[]>("queryHistory") ?? [];
308
+ ctx.session.set("queryHistory", [...previousQueries, input.question]);
309
+
310
+ stream.delta(`Found ${results.length} records in ${input.selectedTable}.\n`);
311
+
312
+ return {
313
+ output: {
314
+ table: input.selectedTable,
315
+ rowCount: results.length,
316
+ preview: results.slice(0, 5),
317
+ },
318
+ };
319
+ },
320
+ });
321
+
322
+ agent.listen(3000);
323
+ ```
324
+
325
+ **The SSE event flow:**
326
+
327
+ ```
328
+ First invocation:
329
+ task.status → "working"
330
+ task.progress → 20%, "Checking available tables..."
331
+ task.input_required → { prompt: "...", resume_token: "resume_abc", input_schema: {...} }
332
+
333
+ [Marketplace shows a form to the buyer]
334
+ [Buyer selects "orders" and submits]
335
+
336
+ Second invocation (resume):
337
+ POST /execute { input: { question: "...", selectedTable: "orders" }, resume_token: "resume_abc" }
338
+ task.status → "working"
339
+ task.progress → 30%, "Querying orders..."
340
+ task.delta → "Found 142 records..."
341
+ task.completed → { table: "orders", rowCount: 142, preview: [...] }
342
+ ```
343
+
344
+ ---
345
+
346
+ ## 5. Framework Integrations
347
+
348
+ The SDK provides one-line adapter functions that translate framework-specific streaming into Catelog's task event protocol.
349
+
350
+ ---
351
+
352
+ ### 5.1 OpenAI SDK
353
+
354
+ Works with both streaming and non-streaming completions. Automatically extracts token usage.
355
+
356
+ ```ts
357
+ import OpenAI from "openai";
358
+ import { createAgent, pipeOpenAiStream } from "catelog";
359
+ import { z } from "zod";
360
+
361
+ const openai = new OpenAI(); // Uses OPENAI_API_KEY from env
362
+
363
+ const agent = createAgent({
364
+ name: "OpenAI Analyst",
365
+ inputSchema: z.object({
366
+ prompt: z.string(),
367
+ model: z.enum(["gpt-4o", "gpt-4o-mini"]).default("gpt-4o-mini"),
368
+ }),
369
+
370
+ async execute({ input, stream, signal }) {
371
+ stream.progress(10, "Calling OpenAI...");
372
+
373
+ // Streaming chat completion
374
+ const openaiStream = await openai.chat.completions.create({
375
+ model: input.model,
376
+ messages: [{ role: "user", content: input.prompt }],
377
+ stream: true,
378
+ // Optional: get token counts in final chunk
379
+ stream_options: { include_usage: true },
380
+ });
381
+
382
+ // One line: pipes chunks → task.delta, extracts token usage → telemetry
383
+ return pipeOpenAiStream(openaiStream, stream);
384
+ // Returns: { output: { text: "..." }, telemetry: { llm_tokens: {...}, model_used: "..." } }
385
+ },
386
+ });
387
+
388
+ agent.listen(3000);
389
+ ```
390
+
391
+ **What you get from the adapter:**
392
+ - Each `choices[0].delta.content` chunk → emitted as `task.delta`
393
+ - Tool calls in stream → emitted as `task.activity` events
394
+ - Final `usage` chunk → captured as `telemetry.llm_tokens`
395
+ - Model name → captured as `telemetry.model_used`
396
+
397
+ ---
398
+
399
+ ### 5.2 Vercel AI SDK
400
+
401
+ Works with `streamText`, `generateText`, `streamObject`, and any Vercel AI provider (OpenAI, Anthropic, Google, etc.).
402
+
403
+ ```ts
404
+ import { streamText, generateText } from "ai";
405
+ import { openai } from "@ai-sdk/openai";
406
+ import { anthropic } from "@ai-sdk/anthropic";
407
+ import { createAgent, pipeAiSdkStream } from "catelog";
408
+ import { z } from "zod";
409
+
410
+ const agent = createAgent({
411
+ name: "Vercel AI Coding Assistant",
412
+ inputSchema: z.object({
413
+ prompt: z.string(),
414
+ provider: z.enum(["openai", "anthropic"]).default("openai"),
415
+ }),
416
+
417
+ async execute({ input, stream, signal }) {
418
+ stream.progress(10, "Generating response...");
419
+
420
+ // Works with any Vercel AI provider
421
+ const model = input.provider === "anthropic"
422
+ ? anthropic("claude-3-5-sonnet-20241022")
423
+ : openai("gpt-4o");
424
+
425
+ // Streaming mode
426
+ const result = streamText({
427
+ model,
428
+ prompt: input.prompt,
429
+ abortSignal: signal, // Propagate cancellation
430
+ });
431
+
432
+ // One line: pipes textStream → task.delta, extracts usage → telemetry
433
+ return pipeAiSdkStream(result, stream);
434
+ },
435
+ });
436
+
437
+ agent.listen(3000);
438
+ ```
439
+
440
+ **What you get from the adapter:**
441
+ - Each text chunk → emitted as `task.delta`
442
+ - Final `usage` object → captured as `telemetry.llm_tokens`
443
+ - Tool calls → captured as `telemetry.framework.tool_calls`
444
+
445
+ ---
446
+
447
+ ### 5.3 LangGraph / LangChain
448
+
449
+ Works with compiled LangGraph graphs. Translates node transitions to `task.activity` events and message content to `task.delta`.
450
+
451
+ ```ts
452
+ import { createAgent, pipeLangGraphStream } from "catelog";
453
+ import { StateGraph, Annotation } from "@langchain/langgraph";
454
+ import { ChatOpenAI } from "@langchain/openai";
455
+ import { z } from "zod";
456
+
457
+ // Build your LangGraph normally
458
+ const GraphState = Annotation.Root({ messages: Annotation<string[]>() });
459
+ const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
460
+
461
+ const graph = new StateGraph(GraphState)
462
+ .addNode("research", async (state) => { /* ... */ })
463
+ .addNode("synthesize", async (state) => { /* ... */ })
464
+ .addEdge("__start__", "research")
465
+ .addEdge("research", "synthesize")
466
+ .compile();
467
+
468
+ const agent = createAgent({
469
+ name: "LangGraph Research Pipeline",
470
+ inputSchema: z.object({
471
+ topic: z.string(),
472
+ }),
473
+
474
+ async execute({ input, stream, signal }) {
475
+ stream.progress(5, "Starting graph...");
476
+
477
+ // One line: runs graph, emits node transitions as task.activity
478
+ return pipeLangGraphStream(
479
+ graph,
480
+ { messages: [input.topic] },
481
+ stream,
482
+ { signal }
483
+ );
484
+ },
485
+ });
486
+
487
+ agent.listen(3000);
488
+ ```
489
+
490
+ **What you get from the adapter:**
491
+ - Each graph node entry → emitted as `task.activity` with `{ node: "research", step: 1 }`
492
+ - Message content from nodes → emitted as `task.delta`
493
+ - Visited nodes list → captured in `telemetry.framework.nodes_visited`
494
+
495
+ ---
496
+
497
+ ### 5.4 CrewAI / Custom Async Generator
498
+
499
+ For any framework that doesn't have a built-in adapter, use `pipeAsyncIterableStream`. This works with CrewAI agents, LlamaIndex, custom generators, or any `AsyncIterable<string>`.
500
+
501
+ ```ts
502
+ import { createAgent, pipeAsyncIterableStream } from "catelog";
503
+ import { z } from "zod";
504
+
505
+ // Example: a custom async generator (replace with your framework)
506
+ async function* myCrewAiAgent(input: string): AsyncGenerator<string> {
507
+ yield "Agent 1 is researching...\n";
508
+ await new Promise((r) => setTimeout(r, 1000));
509
+ yield "Agent 2 is writing the report...\n";
510
+ await new Promise((r) => setTimeout(r, 1000));
511
+ yield `Final report on: ${input}\n`;
512
+ }
513
+
514
+ const agent = createAgent({
515
+ name: "CrewAI Research Crew",
516
+ inputSchema: z.object({
517
+ topic: z.string(),
518
+ }),
519
+
520
+ async execute({ input, stream }) {
521
+ stream.progress(10, "Spinning up the crew...");
522
+
523
+ // Pipe any async iterable directly — works with CrewAI, LlamaIndex, etc.
524
+ return pipeAsyncIterableStream(
525
+ myCrewAiAgent(input.topic),
526
+ stream
527
+ );
528
+ },
529
+ });
530
+
531
+ agent.listen(3000);
532
+ ```
533
+
534
+ ---
535
+
536
+ ### 5.5 Vanilla TypeScript (No Framework)
537
+
538
+ No framework? No problem. Use the stream helpers directly.
539
+
540
+ ```ts
541
+ import { createAgent } from "catelog";
542
+ import { z } from "zod";
543
+
544
+ const agent = createAgent({
545
+ name: "Custom Logic Agent",
546
+ inputSchema: z.object({
547
+ data: z.array(z.number()),
548
+ operation: z.enum(["sum", "avg", "max"]),
549
+ }),
550
+
551
+ async execute({ input, stream, log, metric, signal }) {
552
+ log.info("Running computation", { operation: input.operation });
553
+ stream.progress(10, `Computing ${input.operation}...`);
554
+
555
+ let result: number;
556
+ switch (input.operation) {
557
+ case "sum": result = input.data.reduce((a, b) => a + b, 0); break;
558
+ case "avg": result = input.data.reduce((a, b) => a + b, 0) / input.data.length; break;
559
+ case "max": result = Math.max(...input.data); break;
560
+ }
561
+
562
+ // Track custom metrics
563
+ metric.count("numbers_processed", input.data.length);
564
+ metric.gauge("result_value", result);
565
+
566
+ stream.progress(100, "Done!");
567
+ stream.delta(`The ${input.operation} of ${input.data.length} numbers is: ${result}`);
568
+
569
+ return { output: { result, operation: input.operation } };
570
+ },
571
+ });
572
+
573
+ agent.listen(3000);
574
+ ```
575
+
576
+ ---
577
+
578
+ ## 6. Streaming Helpers
579
+
580
+ All of these are available on the `stream` object inside `execute()`:
581
+
582
+ | Method | Signature | What It Does | Completes Task? |
583
+ | :--- | :--- | :--- | :---: |
584
+ | `stream.progress()` | `(percent: number, message?: string)` | Emits a `task.progress` event for the progress bar | No |
585
+ | `stream.delta()` | `(text: string, type?: "text" \| "json_patch")` | Emits a `task.delta` event — live text token | No |
586
+ | `stream.write()` | `(text: string)` | Alias for `stream.delta(text)` | No |
587
+ | `stream.activity()` | `(type: string, data?: object)` | Emits a `task.activity` event (tool calls, etc.) | No |
588
+ | `stream.step()` | `(label: string)` | Legacy alias — maps to `stream.progress(0, label)` | No |
589
+ | `stream.emit()` | `(event: string, data: unknown)` | Raw SSE event emitter | No |
590
+ | `stream.isClosed()` | `() => boolean` | Check if client disconnected | — |
591
+ | `return value` | any | Completes the task with output | **Yes** |
592
+ | `throw error` | Error | Fails the task | **Yes** |
593
+ | `ctx.needInput()` | `(opts)` | Suspends with input-required | **Yes** |
594
+
595
+ ---
596
+
597
+ ## 7. Schema Definition
598
+
599
+ Schemas are **optional** but recommended for marketplace listings. They generate the buyer input form and validate payloads at the gateway.
600
+
601
+ ### With Zod (Recommended)
602
+
603
+ ```ts
604
+ import { z } from "zod";
605
+
606
+ const agent = createAgent({
607
+ name: "Research Agent",
608
+ inputSchema: z.object({
609
+ query: z.string().min(3).describe("The research question"),
610
+ maxSources: z.number().min(1).max(20).default(5).describe("Max sources to cite"),
611
+ language: z.enum(["en", "es", "fr", "de"]).default("en"),
612
+ }),
613
+ outputSchema: z.object({
614
+ report: z.string(),
615
+ sources: z.array(z.string().url()),
616
+ language: z.string(),
617
+ }),
618
+ async execute({ input }) {
619
+ // input is fully typed: { query: string, maxSources: number, language: string }
620
+ return { output: { report: "...", sources: [], language: input.language } };
621
+ },
622
+ });
623
+ ```
624
+
625
+ ### With Raw JSON Schema
626
+
627
+ ```ts
628
+ const agent = createAgent({
629
+ name: "Data Agent",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ query: { type: "string", minLength: 3, description: "The research question" },
634
+ maxSources: { type: "number", minimum: 1, maximum: 20, default: 5 },
635
+ },
636
+ required: ["query"],
637
+ },
638
+ async execute({ input }) { /* ... */ },
639
+ });
640
+ ```
641
+
642
+ ### Without Schema (Accepts Any JSON)
643
+
644
+ ```ts
645
+ const agent = createAgent({
646
+ name: "Flexible Agent",
647
+ // No inputSchema or outputSchema — accepts any JSON
648
+ async execute({ input }) {
649
+ return { output: { received: input } };
650
+ },
651
+ });
652
+ ```
653
+
654
+ ---
655
+
656
+ ## 8. API Keys
657
+
658
+ ### Where Keys Come From
659
+
660
+ Your Catelog dashboard generates two keys per workspace:
661
+
662
+ | Key | Format | Purpose |
663
+ | :--- | :--- | :--- |
664
+ | **Secret key** | `cat_sec_live_...` | Authenticates `/execute` calls |
665
+ | **Publishable key** | `cat_pub_live_...` | Safe to use in frontend code for `/health` and `/schema` |
666
+
667
+ ### Setting Keys
668
+
669
+ Put them in your `.env` — the SDK auto-reads them:
670
+
671
+ ```env
672
+ CATELOG_SECRET_KEY=cat_sec_live_your_key_here
673
+ CATELOG_PUBLIC_KEY=cat_pub_live_your_key_here
674
+ ```
675
+
676
+ Or pass them explicitly in the constructor:
677
+
678
+ ```ts
679
+ const agent = createAgent({
680
+ name: "My Agent",
681
+ secretKey: "cat_sec_live_your_key_here",
682
+ publicKey: "cat_pub_live_your_key_here",
683
+ async execute({ input }) { /* ... */ },
684
+ });
685
+ ```
686
+
687
+ ### How Authentication Works
688
+
689
+ | Caller | Auth Method | Header |
690
+ | :--- | :--- | :--- |
691
+ | **Marketplace Gateway** | HMAC-SHA256 (automatic) | `X-Marketplace-Signature: ...` |
692
+ | **Your curl / Postman** | Bearer secret key | `Authorization: Bearer cat_sec_live_...` |
693
+ | **Your frontend app** | Bearer public key | `Authorization: Bearer cat_pub_live_...` |
694
+ | **Local development** | None (dev mode) | *(no header required)* |
695
+
696
+ In development, you don't need any keys. The SDK runs in permissive mode and logs a warning.
697
+
698
+ ---
699
+
700
+ ## 9. Telemetry & Logging
701
+
702
+ ### Structured Logging
703
+
704
+ A `log` helper is available inside every `execute()`:
705
+
706
+ ```ts
707
+ async execute({ input, log }) {
708
+ log.info("Processing request", { query: input.query });
709
+ log.warn("Rate limit approaching", { remaining: 10 });
710
+ log.error("Failed to fetch", { url: "https://...", status: 429 });
711
+ log.debug("Cache hit", { key: "results:abc" }); // Only in development
712
+ }
713
+ ```
714
+
715
+ Output is color-coded by level with run ID prefix. Debug logs are suppressed in production.
716
+
717
+ ### Custom Metrics
718
+
719
+ A `metric` helper is available for tracking custom counters and gauges:
720
+
721
+ ```ts
722
+ async execute({ input, metric }) {
723
+ metric.count("tokens_processed", 142);
724
+ metric.gauge("queue_depth", 5);
725
+ metric.timing("llm_latency_ms", 1240);
726
+ }
727
+ ```
728
+
729
+ All custom metrics appear in the `telemetry.framework.custom_metrics` field of the final response.
730
+
731
+ ### Returning Telemetry
732
+
733
+ You can provide model usage data in the return value for accurate billing and analytics:
734
+
735
+ ```ts
736
+ return {
737
+ output: { summary: "..." },
738
+ telemetry: {
739
+ model_used: "gpt-4o",
740
+ llm_tokens: {
741
+ prompt: 250,
742
+ completion: 400,
743
+ total: 650,
744
+ },
745
+ cost_usd: 0.0013,
746
+ },
747
+ };
748
+ ```
749
+
750
+ If using a framework adapter (`pipeOpenAiStream`, `pipeAiSdkStream`), token counts are extracted automatically — no need to provide them manually.
751
+
752
+ ---
753
+
754
+ ## 10. What You Get — Endpoints
755
+
756
+ Once `agent.listen(3000)` is called, the SDK exposes these endpoints:
757
+
758
+ ### `GET /health`
759
+ No auth required. Used by the marketplace registration probe.
760
+
761
+ ```bash
762
+ curl http://localhost:3000/health
763
+ ```
764
+ ```json
765
+ {
766
+ "status": "ready",
767
+ "protocol_version": "1.0.0",
768
+ "uptime_seconds": 1284,
769
+ "environment_ready": true,
770
+ "capabilities": {
771
+ "streaming": true,
772
+ "delta_stream": true,
773
+ "max_timeout_seconds": 300,
774
+ "execution_modes": ["sync"]
775
+ }
776
+ }
777
+ ```
778
+
779
+ ### `GET /schema`
780
+ No auth required. Returns the agent manifest for marketplace listing.
781
+
782
+ ```bash
783
+ curl http://localhost:3000/schema
784
+ ```
785
+ ```json
786
+ {
787
+ "id": "my-research-agent",
788
+ "name": "My Research Agent",
789
+ "version": "1.0.0",
790
+ "description": "...",
791
+ "protocol_version": "1.0.0",
792
+ "capabilities": { "streaming": true, "execution_modes": ["sync"] },
793
+ "input_schema": { "type": "object", "properties": { "query": { "type": "string" } } },
794
+ "output_schema": { "type": "object", "properties": { "report": { "type": "string" } } }
795
+ }
796
+ ```
797
+
798
+ ### `POST /execute`
799
+ Runs the agent. Accepts SSE (`Accept: text/event-stream`) or JSON (`Accept: application/json`).
800
+
801
+ ```bash
802
+ # Dev mode — no auth required
803
+ curl -X POST http://localhost:3000/execute \
804
+ -H "Content-Type: application/json" \
805
+ -d '{"input": {"query": "AI trends 2025"}}'
806
+
807
+ # With Bearer auth
808
+ curl -X POST http://localhost:3000/execute \
809
+ -H "Authorization: Bearer cat_sec_live_your_key" \
810
+ -H "Content-Type: application/json" \
811
+ -d '{"input": {"query": "AI trends 2025"}}'
812
+ ```
813
+
814
+ ### `GET /tasks/:id` *(async mode only)*
815
+ Polls the status of a background async job.
816
+
817
+ ```bash
818
+ curl http://localhost:3000/tasks/run_abc123
819
+ ```
820
+ ```json
821
+ { "run_id": "run_abc123", "status": "working", "elapsed_ms": 4200 }
822
+ ```
823
+
824
+ ---
825
+
826
+ ## 11. Testing Locally
827
+
828
+ ### Basic curl test
829
+
830
+ ```bash
831
+ # 1. Health check
832
+ curl http://localhost:3000/health
833
+
834
+ # 2. See the agent schema
835
+ curl http://localhost:3000/schema | jq .
836
+
837
+ # 3. Run the agent (SSE stream)
838
+ curl -N -X POST http://localhost:3000/execute \
839
+ -H "Content-Type: application/json" \
840
+ -H "Accept: text/event-stream" \
841
+ -d '{"input": {"query": "test query"}}'
842
+
843
+ # 4. Run the agent (JSON batch, no streaming)
844
+ curl -X POST http://localhost:3000/execute \
845
+ -H "Content-Type: application/json" \
846
+ -H "Accept: application/json" \
847
+ -d '{"input": {"query": "test query"}}'
848
+ ```
849
+
850
+ ### Running the test suite
851
+
852
+ ```bash
853
+ pnpm test
854
+ # or
855
+ npx tsx test/index.test.ts
856
+ ```
857
+
858
+ ---
859
+
860
+ ## 12. Registering on the Marketplace
861
+
862
+ 1. Deploy your agent to a public URL (Fly.io, Railway, Render, AWS, etc.)
863
+ 2. Go to **Catelog Dashboard → Creator Space → My Agents**
864
+ 3. Click **Register New Agent**
865
+ 4. Paste your public URL (e.g. `https://my-agent.fly.dev`)
866
+ 5. The marketplace automatically:
867
+ - Pings `GET /health` — verifies reachability and protocol version
868
+ - Fetches `GET /schema` — populates agent name, description, and input form
869
+ - Checks `capabilities.execution_modes` — marks sync/async/conversational support
870
+ 6. Review auto-populated fields, set your price, and publish
871
+
872
+ ---
873
+
874
+ ## 13. Error Handling
875
+
876
+ ### Automatic Error Capture
877
+
878
+ You don't need to catch errors manually. Any uncaught exception in `execute()` is automatically converted to a `task.failed` event:
879
+
880
+ ```ts
881
+ async execute({ input }) {
882
+ // If this throws, the SDK emits task.failed automatically
883
+ const result = await riskyOperation(input);
884
+ return { output: result };
885
+ }
886
+ ```
887
+
888
+ ### Custom Error Codes
889
+
890
+ For domain-specific errors, attach a `code` and `retryable` flag:
891
+
892
+ ```ts
893
+ async execute({ input }) {
894
+ const balance = await checkCredits();
895
+ if (balance < 0.01) {
896
+ const err = new Error("Insufficient credits to run this model");
897
+ (err as any).code = "MODEL_RATE_LIMIT";
898
+ (err as any).retryable = true;
899
+ throw err;
900
+ }
901
+ // ...
902
+ }
903
+ ```
904
+
905
+ ### Cancellation via AbortSignal
906
+
907
+ When a buyer cancels or the gateway times out, `signal.aborted` becomes `true`. Pass the signal to your LLM or fetch calls to propagate cancellation and avoid wasted credits:
908
+
909
+ ```ts
910
+ async execute({ input, stream, signal }) {
911
+ // signal propagates cancellation to fetch, OpenAI, LangGraph, etc.
912
+ const response = await fetch("https://api.example.com", { signal });
913
+ const result = await openai.chat.completions.create({ ..., stream: true });
914
+
915
+ // Check manually if needed
916
+ if (signal.aborted) {
917
+ return { output: { partial: true } };
918
+ }
919
+ }
920
+ ```
921
+
922
+ ---
923
+
924
+ ## Quick Reference
925
+
926
+ ```ts
927
+ import { createAgent, pipeOpenAiStream, pipeAiSdkStream, pipeLangGraphStream } from "catelog";
928
+
929
+ const agent = createAgent({
930
+ name: string, // Required
931
+ id?: string, // Auto-slugified from name
932
+ version?: string, // Default: "1.0.0"
933
+ description?: string,
934
+ mode?: "sync" | "async" | "conversational", // Default: "sync"
935
+ secretKey?: string, // Auto-read from CATELOG_SECRET_KEY
936
+ publicKey?: string, // Auto-read from CATELOG_PUBLIC_KEY
937
+ inputSchema?: ZodSchema | JSONSchema, // Optional
938
+ outputSchema?: ZodSchema | JSONSchema, // Optional
939
+ telemetry?: { endpoint?: string; disabled?: boolean },
940
+ capabilities?: { max_timeout_seconds?: number },
941
+
942
+ async execute(ctx) {
943
+ // ctx.runId string
944
+ // ctx.input typed input
945
+ // ctx.stream streaming helpers
946
+ // ctx.signal AbortSignal
947
+ // ctx.log structured logger
948
+ // ctx.metric metric recorder
949
+ // ctx.metadata buyer/session metadata
950
+ // ctx.session get/set for conversational state
951
+ // ctx.needInput() suspend for HITL input
952
+
953
+ return { output: { ... }, telemetry: { ... } }; // or just return { ... }
954
+ },
955
+ });
956
+
957
+ agent.listen(3000); // or agent.serve(3000)
958
+ ```