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