@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/README.md CHANGED
@@ -96,18 +96,22 @@ const result = await client.tools.execute({
96
96
  console.log(result.session); // methodPrice, spent, remaining, maxSpend, ...
97
97
  ```
98
98
 
99
- **Query mode** gives you curated answers — the server handles answer-safe tool discovery, multi-tool orchestration (up to 100 MCP calls per response turn), self-healing retries, completeness checks, model-aware context budgeting, and AI synthesis for one flat fee:
99
+ **Query mode** gives you curated answers — the server runs a discovery-first planner contract (`discover/probe -> plan-from-evidence -> execute -> bounded fallback`) with model-aware context budgeting and AI synthesis for one flat fee:
100
100
  ```typescript
101
101
  const answer = await client.query.run({
102
102
  query: "What are the top whale movements on Base?",
103
103
  modelId: "glm-model", // optional: choose a supported model
104
- queryDepth: "auto", // optional: fast | auto | deep
104
+ queryDepth: "deep", // optional: fast | auto | deep
105
105
  includeDataUrl: true, // optional: persist full execution data to blob
106
+ includeDeveloperTrace: true, // optional: include machine-readable runtime trace
106
107
  });
107
108
  console.log(answer.response); // AI-synthesized answer
108
109
  console.log(answer.toolsUsed); // Which tools were used
109
110
  console.log(answer.cost); // Cost breakdown
110
111
  console.log(answer.dataUrl); // Optional blob URL with full data
112
+ console.log(answer.developerTrace?.summary); // retries/fallbacks/loops summary
113
+ console.log(answer.developerTrace?.diagnostics?.selection); // lane + scout probe diagnostics
114
+ console.log(answer.orchestrationMetrics); // high-level first-pass / rediscovery metrics
111
115
  ```
112
116
 
113
117
  > Mixed listings are first-class: one listing can expose methods to both surfaces. Methods without `_meta.pricing.executeUsd` remain query-only until priced.
@@ -398,13 +402,18 @@ const closed = await client.tools.closeSession("sess_123");
398
402
 
399
403
  #### `client.query.run(options)`
400
404
 
401
- Run an agentic query. The server discovers answer-safe tools, executes the full pipeline (up to 100 MCP calls per response turn), applies model-aware mediator/data budgeting, and returns an AI-synthesized answer.
405
+ Run an agentic query. The server applies discovery-first orchestration (`discover/probe -> plan-from-evidence -> execute -> bounded fallback`) with up to 100 MCP calls per response turn, then returns an AI-synthesized answer.
402
406
 
403
407
  `queryDepth` controls orchestration depth:
404
408
  - `fast`: lower-latency path for simple lookups.
405
409
  - `auto`: server routes to either `fast` or `deep` from query intent + selected tool complexity.
406
410
  - `deep`: completeness-oriented path (default when omitted).
407
411
 
412
+ `includeDeveloperTrace` and `orchestrationMetrics` expose optional rollout diagnostics.
413
+ `developerTrace.summary` reports aggregate retry/fallback counters, while
414
+ `developerTrace.diagnostics.selection` exposes runtime lane and scout probe signals
415
+ used by discovery-first planning.
416
+
408
417
  ```typescript
409
418
  // Simple string
410
419
  const answer = await client.query.run("What are the top whale movements on Base?");
@@ -417,6 +426,7 @@ const answer = await client.query.run({
417
426
  queryDepth: "auto", // optional: fast | auto | deep
418
427
  includeData: true, // optional: include execution data inline
419
428
  includeDataUrl: true, // optional: include blob URL for full data
429
+ includeDeveloperTrace: true, // optional: include Developer Mode trace
420
430
  });
421
431
 
422
432
  console.log(answer.response); // AI-synthesized text
@@ -425,6 +435,9 @@ console.log(answer.cost); // { modelCostUsd, toolCostUsd, totalCostUsd }
425
435
  console.log(answer.durationMs); // Total time
426
436
  console.log(answer.data); // Optional execution data (when includeData=true)
427
437
  console.log(answer.dataUrl); // Optional blob URL (when includeDataUrl=true)
438
+ console.log(answer.developerTrace?.summary); // Optional trace summary (retries/fallbacks/loops)
439
+ console.log(answer.developerTrace?.diagnostics?.selection?.deepMode); // Optional deep lane trace
440
+ console.log(answer.orchestrationMetrics); // Optional high-level first-pass metrics
428
441
  ```
429
442
 
430
443
  When retrieval-first synthesis rollout is enabled server-side, full-data or truncation-sensitive query requests can switch to retrieval-first context assembly using private stage artifacts and canonical execution data slices. `includeData` and `includeDataUrl` continue to reference the same canonical dataset used for synthesis.
@@ -433,6 +446,12 @@ When retrieval-first synthesis rollout is enabled server-side, full-data or trun
433
446
 
434
447
  Same as `run()` but streams events in real-time via SSE.
435
448
 
449
+ Event types:
450
+ - `tool-status`
451
+ - `text-delta`
452
+ - `developer-trace` (when `includeDeveloperTrace=true`)
453
+ - `done`
454
+
436
455
  ```typescript
437
456
  for await (const event of client.query.stream({
438
457
  query: "What are the top whale movements?",
@@ -472,6 +491,8 @@ import type {
472
491
  // Query types (pay-per-response)
473
492
  QueryOptions,
474
493
  QueryResult,
494
+ QueryDeveloperTrace,
495
+ QueryOrchestrationMetrics,
475
496
  QueryCost,
476
497
  QueryStreamEvent,
477
498
  ContextErrorCode,
@@ -549,6 +570,8 @@ interface QueryResult {
549
570
  durationMs: number;
550
571
  data?: unknown; // Optional execution data (includeData=true)
551
572
  dataUrl?: string; // Optional blob URL (includeDataUrl=true)
573
+ developerTrace?: QueryDeveloperTrace; // Optional runtime trace + diagnostics
574
+ orchestrationMetrics?: QueryOrchestrationMetrics; // Optional first-pass outcome metrics
552
575
  }
553
576
  ```
554
577
 
@@ -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;