@ctxprotocol/sdk 0.8.3 → 0.8.5

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/index.cjs CHANGED
@@ -230,11 +230,119 @@ var Query = class {
230
230
  constructor(client) {
231
231
  this.client = client;
232
232
  }
233
+ buildSyntheticTraceFromRunResult(params) {
234
+ const timeline = params.toolsUsed.map((tool, index) => ({
235
+ stepType: "tool-call",
236
+ event: "tool-call",
237
+ status: "success",
238
+ timestampMs: index,
239
+ tool: {
240
+ id: tool.id,
241
+ name: tool.name
242
+ },
243
+ metadata: {
244
+ skillCalls: tool.skillCalls,
245
+ synthetic: true
246
+ }
247
+ }));
248
+ const toolCalls = params.toolsUsed.reduce(
249
+ (sum, tool) => sum + Math.max(tool.skillCalls, 0),
250
+ 0
251
+ );
252
+ return {
253
+ summary: {
254
+ toolCalls,
255
+ retryCount: 0,
256
+ selfHealCount: 0,
257
+ fallbackCount: 0,
258
+ failureCount: 0,
259
+ recoveryCount: 0,
260
+ completionChecks: 0,
261
+ loopCount: 0
262
+ },
263
+ timeline,
264
+ source: "sdk-fallback",
265
+ synthetic: true,
266
+ reason: "backend_trace_missing",
267
+ durationMs: params.durationMs
268
+ };
269
+ }
270
+ buildSyntheticTraceFromStreamStatus(params) {
271
+ const timeline = params.statusTimeline.map((entry, index) => ({
272
+ stepType: "tool-status",
273
+ event: "tool-status",
274
+ status: entry.status,
275
+ timestampMs: index,
276
+ tool: entry.tool.name || entry.tool.id ? {
277
+ id: entry.tool.id || void 0,
278
+ name: entry.tool.name || void 0
279
+ } : void 0,
280
+ metadata: { synthetic: true }
281
+ }));
282
+ const toolCallsFromUsage = params.toolsUsed.reduce(
283
+ (sum, tool) => sum + Math.max(tool.skillCalls, 0),
284
+ 0
285
+ );
286
+ const toolCallsFromStatus = params.statusTimeline.filter(
287
+ (entry) => entry.status === "tool-complete"
288
+ ).length;
289
+ const toolCalls = toolCallsFromUsage > 0 ? toolCallsFromUsage : toolCallsFromStatus;
290
+ const retryCount = params.statusTimeline.filter(
291
+ (entry) => /(retry|fix|reflect|recover)/i.test(entry.status)
292
+ ).length;
293
+ const completionChecks = params.statusTimeline.filter(
294
+ (entry) => /complet/i.test(entry.status)
295
+ ).length;
296
+ return {
297
+ summary: {
298
+ toolCalls,
299
+ retryCount,
300
+ selfHealCount: retryCount,
301
+ fallbackCount: 0,
302
+ failureCount: 0,
303
+ recoveryCount: 0,
304
+ completionChecks,
305
+ loopCount: retryCount
306
+ },
307
+ timeline,
308
+ source: "sdk-fallback",
309
+ synthetic: true,
310
+ reason: "backend_trace_missing",
311
+ durationMs: params.durationMs
312
+ };
313
+ }
314
+ mergeDeveloperTrace(first, second) {
315
+ if (!first) return second;
316
+ if (!second) return first;
317
+ const firstTimeline = Array.isArray(first.timeline) ? first.timeline : [];
318
+ const secondTimeline = Array.isArray(second.timeline) ? second.timeline : [];
319
+ const mergedTimeline = [...firstTimeline, ...secondTimeline];
320
+ return {
321
+ ...first,
322
+ ...second,
323
+ summary: {
324
+ ...typeof first.summary === "object" && first.summary ? first.summary : {},
325
+ ...typeof second.summary === "object" && second.summary ? second.summary : {}
326
+ },
327
+ ...mergedTimeline.length > 0 ? { timeline: mergedTimeline } : {}
328
+ };
329
+ }
330
+ parseStreamEvent(rawData) {
331
+ const parsed = JSON.parse(rawData);
332
+ if (!parsed || typeof parsed !== "object") {
333
+ return void 0;
334
+ }
335
+ const event = parsed;
336
+ if (typeof event.type !== "string") {
337
+ return void 0;
338
+ }
339
+ return event;
340
+ }
233
341
  /**
234
342
  * Run an agentic query and wait for the full response.
235
343
  *
236
344
  * The server discovers relevant tools (or uses the ones you specify),
237
- * executes the full agentic pipeline (up to 100 MCP calls per tool),
345
+ * executes the discovery-first pipeline (up to 100 MCP calls per tool),
238
346
  * and returns an AI-synthesized answer. Payment is settled after
239
347
  * successful execution via deferred settlement.
240
348
  *
@@ -263,42 +371,25 @@ var Query = class {
263
371
  */
264
372
  async run(options) {
265
373
  const opts = typeof options === "string" ? { query: options } : options;
266
- const headers = opts.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0;
267
- const response = await this.client._fetch(
268
- "/api/v1/query",
269
- {
270
- method: "POST",
271
- headers,
272
- body: JSON.stringify({
273
- query: opts.query,
274
- tools: opts.tools,
275
- modelId: opts.modelId,
276
- includeData: opts.includeData,
277
- includeDataUrl: opts.includeDataUrl,
278
- queryDepth: opts.queryDepth,
279
- stream: false
280
- })
374
+ let terminalError;
375
+ for await (const event of this.stream(opts)) {
376
+ if (event.type === "error") {
377
+ terminalError = {
378
+ error: event.error,
379
+ ...event.code ? { code: event.code } : {},
380
+ ...event.scope ? { scope: event.scope } : {},
381
+ ...event.reasonCode ? { reasonCode: event.reasonCode } : {}
382
+ };
383
+ continue;
384
+ }
385
+ if (event.type === "done") {
386
+ return event.result;
281
387
  }
282
- );
283
- if ("error" in response) {
284
- throw new ContextError(
285
- response.error,
286
- response.code,
287
- void 0,
288
- response.helpUrl
289
- );
290
388
  }
291
- if (response.success) {
292
- return {
293
- response: response.response,
294
- toolsUsed: response.toolsUsed,
295
- cost: response.cost,
296
- durationMs: response.durationMs,
297
- data: response.data,
298
- dataUrl: response.dataUrl
299
- };
389
+ if (terminalError) {
390
+ throw new ContextError(terminalError.error, terminalError.code);
300
391
  }
301
- throw new ContextError("Unexpected response format from query API");
392
+ throw new ContextError("Streaming query ended before done event");
302
393
  }
303
394
  /**
304
395
  * Run an agentic query with streaming. Returns an async iterable that
@@ -307,6 +398,8 @@ var Query = class {
307
398
  * Event types:
308
399
  * - `tool-status` — A tool started executing or changed status
309
400
  * - `text-delta` — A chunk of the AI response text
401
+ * - `developer-trace` — Runtime trace metadata (when includeDeveloperTrace=true)
402
+ * - `error` — A structured query/runtime error emitted before stream completion
310
403
  * - `done` — The full response is complete (includes final `QueryResult`)
311
404
  *
312
405
  * @param options - Query options or a plain string question
@@ -322,9 +415,15 @@ var Query = class {
322
415
  * case "text-delta":
323
416
  * process.stdout.write(event.delta);
324
417
  * break;
418
+ * case "developer-trace":
419
+ * console.log("Trace summary:", event.trace.summary);
420
+ * break;
325
421
  * case "done":
326
422
  * console.log("\nCost:", event.result.cost.totalCostUsd);
327
423
  * break;
424
+ * case "error":
425
+ * console.error("Stream error:", event.error);
426
+ * break;
328
427
  * }
329
428
  * }
330
429
  * ```
@@ -341,7 +440,9 @@ var Query = class {
341
440
  modelId: opts.modelId,
342
441
  includeData: opts.includeData,
343
442
  includeDataUrl: opts.includeDataUrl,
443
+ includeDeveloperTrace: opts.includeDeveloperTrace,
344
444
  queryDepth: opts.queryDepth,
445
+ debugScoutDeepMode: opts.debugScoutDeepMode,
345
446
  stream: true
346
447
  })
347
448
  });
@@ -352,6 +453,48 @@ var Query = class {
352
453
  const reader = body.getReader();
353
454
  const decoder = new TextDecoder();
354
455
  let buffer = "";
456
+ let aggregatedTrace;
457
+ const statusTimeline = [];
458
+ const parseAndHydrateEvent = (rawData) => {
459
+ const event = this.parseStreamEvent(rawData);
460
+ if (!event) {
461
+ return void 0;
462
+ }
463
+ if (event.type === "developer-trace") {
464
+ aggregatedTrace = this.mergeDeveloperTrace(aggregatedTrace, event.trace);
465
+ return event;
466
+ }
467
+ if (event.type === "tool-status") {
468
+ statusTimeline.push({
469
+ status: event.status,
470
+ tool: {
471
+ id: event.tool.id,
472
+ name: event.tool.name
473
+ }
474
+ });
475
+ return event;
476
+ }
477
+ if (event.type === "done") {
478
+ let mergedTrace = this.mergeDeveloperTrace(
479
+ aggregatedTrace,
480
+ event.result.developerTrace
481
+ );
482
+ if (!mergedTrace && opts.includeDeveloperTrace) {
483
+ mergedTrace = statusTimeline.length > 0 ? this.buildSyntheticTraceFromStreamStatus({
484
+ statusTimeline,
485
+ toolsUsed: event.result.toolsUsed,
486
+ durationMs: event.result.durationMs
487
+ }) : this.buildSyntheticTraceFromRunResult({
488
+ toolsUsed: event.result.toolsUsed,
489
+ durationMs: event.result.durationMs
490
+ });
491
+ }
492
+ if (mergedTrace) {
493
+ event.result.developerTrace = mergedTrace;
494
+ }
495
+ }
496
+ return event;
497
+ };
355
498
  try {
356
499
  while (true) {
357
500
  const { done, value } = await reader.read();
@@ -365,7 +508,10 @@ var Query = class {
365
508
  const data = trimmed.slice(6);
366
509
  if (data === "[DONE]") return;
367
510
  try {
368
- yield JSON.parse(data);
511
+ const event = parseAndHydrateEvent(data);
512
+ if (event) {
513
+ yield event;
514
+ }
369
515
  } catch {
370
516
  }
371
517
  }
@@ -375,7 +521,10 @@ var Query = class {
375
521
  const data = buffer.trim().slice(6);
376
522
  if (data !== "[DONE]") {
377
523
  try {
378
- yield JSON.parse(data);
524
+ const event = parseAndHydrateEvent(data);
525
+ if (event) {
526
+ yield event;
527
+ }
379
528
  } catch {
380
529
  }
381
530
  }
@@ -454,30 +603,34 @@ var ContextClient = class {
454
603
  *
455
604
  * @internal
456
605
  */
457
- async _fetch(endpoint, options = {}) {
606
+ async _fetch(endpoint, options = {}, fetchOptions) {
458
607
  if (this._closed) {
459
608
  throw new ContextError("Client has been closed");
460
609
  }
461
610
  const url = `${this.baseUrl}${endpoint}`;
462
611
  const maxRetries = 3;
463
612
  const timeoutMs = this.requestTimeoutMs;
613
+ const method = (options.method ?? "GET").toUpperCase();
614
+ const requestHeaders = new Headers(options.headers);
615
+ const canRetryRequest = fetchOptions?.retry === false ? false : method === "GET" || method === "HEAD" || method === "OPTIONS" || requestHeaders.has("Idempotency-Key");
464
616
  let lastError;
465
617
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
466
618
  const controller = new AbortController();
467
619
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
620
+ const mergedHeaders = new Headers(requestHeaders);
621
+ if (!mergedHeaders.has("Content-Type")) {
622
+ mergedHeaders.set("Content-Type", "application/json");
623
+ }
624
+ mergedHeaders.set("Authorization", `Bearer ${this.apiKey}`);
468
625
  try {
469
626
  const response = await fetch(url, {
470
627
  ...options,
471
628
  signal: controller.signal,
472
- headers: {
473
- "Content-Type": "application/json",
474
- Authorization: `Bearer ${this.apiKey}`,
475
- ...options.headers
476
- }
629
+ headers: mergedHeaders
477
630
  });
478
631
  clearTimeout(timeout);
479
632
  if (!response.ok) {
480
- if (response.status >= 500 && attempt < maxRetries) {
633
+ if (response.status >= 500 && canRetryRequest && attempt < maxRetries) {
481
634
  const delay = Math.min(1e3 * 2 ** attempt, 1e4);
482
635
  await new Promise((resolve) => setTimeout(resolve, delay));
483
636
  continue;
@@ -496,7 +649,16 @@ var ContextClient = class {
496
649
  }
497
650
  throw new ContextError(errorMessage, errorCode, response.status, helpUrl);
498
651
  }
499
- return response.json();
652
+ try {
653
+ return await response.json();
654
+ } catch (error) {
655
+ const parseError = error instanceof Error ? error : new Error(String(error));
656
+ throw new ContextError(
657
+ `Failed to parse JSON response: ${parseError.message}`,
658
+ void 0,
659
+ response.status
660
+ );
661
+ }
500
662
  } catch (error) {
501
663
  clearTimeout(timeout);
502
664
  if (error instanceof ContextError) {
@@ -504,7 +666,7 @@ var ContextClient = class {
504
666
  }
505
667
  lastError = error instanceof Error ? error : new Error(String(error));
506
668
  const isRetryable = lastError.name === "AbortError" || lastError.message.includes("fetch failed") || lastError.message.includes("ECONNRESET") || lastError.message.includes("ETIMEDOUT");
507
- if (isRetryable && attempt < maxRetries) {
669
+ if (isRetryable && canRetryRequest && attempt < maxRetries) {
508
670
  const delay = Math.min(1e3 * 2 ** attempt, 1e4);
509
671
  await new Promise((resolve) => setTimeout(resolve, delay));
510
672
  continue;