@kweaver-ai/kweaver-sdk 0.7.4 → 0.8.1

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 (56) hide show
  1. package/README.md +20 -0
  2. package/README.zh.md +18 -0
  3. package/dist/api/agent-observability.d.ts +51 -0
  4. package/dist/api/agent-observability.js +108 -0
  5. package/dist/api/conversations.d.ts +4 -8
  6. package/dist/api/conversations.js +16 -58
  7. package/dist/api/datasources.d.ts +2 -20
  8. package/dist/api/datasources.js +7 -123
  9. package/dist/api/trace.d.ts +44 -0
  10. package/dist/api/trace.js +81 -0
  11. package/dist/api/vega.d.ts +53 -0
  12. package/dist/api/vega.js +144 -0
  13. package/dist/cli.js +5 -0
  14. package/dist/commands/bkn-ops.js +12 -6
  15. package/dist/commands/bkn-utils.d.ts +9 -0
  16. package/dist/commands/bkn-utils.js +17 -0
  17. package/dist/commands/ds.js +7 -2
  18. package/dist/commands/trace.d.ts +14 -0
  19. package/dist/commands/trace.js +168 -0
  20. package/dist/resources/datasources.js +2 -1
  21. package/dist/trace-core/diagnose/builtin-rules/excessive-tool-calls-per-turn.d.ts +2 -0
  22. package/dist/trace-core/diagnose/builtin-rules/excessive-tool-calls-per-turn.js +15 -0
  23. package/dist/trace-core/diagnose/builtin-rules/excessive-tool-calls-per-turn.yaml +16 -0
  24. package/dist/trace-core/diagnose/builtin-rules/llm-response-truncated-no-continue.d.ts +2 -0
  25. package/dist/trace-core/diagnose/builtin-rules/llm-response-truncated-no-continue.js +44 -0
  26. package/dist/trace-core/diagnose/builtin-rules/llm-response-truncated-no-continue.yaml +15 -0
  27. package/dist/trace-core/diagnose/builtin-rules/register.d.ts +1 -0
  28. package/dist/trace-core/diagnose/builtin-rules/register.js +11 -0
  29. package/dist/trace-core/diagnose/builtin-rules/retrieval-empty-no-fallback.d.ts +2 -0
  30. package/dist/trace-core/diagnose/builtin-rules/retrieval-empty-no-fallback.js +29 -0
  31. package/dist/trace-core/diagnose/builtin-rules/retrieval-empty-no-fallback.yaml +15 -0
  32. package/dist/trace-core/diagnose/builtin-rules/tool-error-swallowed.d.ts +2 -0
  33. package/dist/trace-core/diagnose/builtin-rules/tool-error-swallowed.js +45 -0
  34. package/dist/trace-core/diagnose/builtin-rules/tool-error-swallowed.yaml +15 -0
  35. package/dist/trace-core/diagnose/builtin-rules/tool-loop-no-state-change.d.ts +2 -0
  36. package/dist/trace-core/diagnose/builtin-rules/tool-loop-no-state-change.js +38 -0
  37. package/dist/trace-core/diagnose/builtin-rules/tool-loop-no-state-change.yaml +16 -0
  38. package/dist/trace-core/diagnose/index.d.ts +9 -0
  39. package/dist/trace-core/diagnose/index.js +104 -0
  40. package/dist/trace-core/diagnose/predicate-registry.d.ts +7 -0
  41. package/dist/trace-core/diagnose/predicate-registry.js +30 -0
  42. package/dist/trace-core/diagnose/report-assembler.d.ts +12 -0
  43. package/dist/trace-core/diagnose/report-assembler.js +90 -0
  44. package/dist/trace-core/diagnose/rule-loader.d.ts +11 -0
  45. package/dist/trace-core/diagnose/rule-loader.js +86 -0
  46. package/dist/trace-core/diagnose/schemas.d.ts +109 -0
  47. package/dist/trace-core/diagnose/schemas.js +94 -0
  48. package/dist/trace-core/diagnose/signal-probe.d.ts +5 -0
  49. package/dist/trace-core/diagnose/signal-probe.js +21 -0
  50. package/dist/trace-core/diagnose/synthesizer-template.d.ts +2 -0
  51. package/dist/trace-core/diagnose/synthesizer-template.js +49 -0
  52. package/dist/trace-core/diagnose/trace-shaper.d.ts +3 -0
  53. package/dist/trace-core/diagnose/trace-shaper.js +72 -0
  54. package/dist/trace-core/diagnose/types.d.ts +124 -0
  55. package/dist/trace-core/diagnose/types.js +1 -0
  56. package/package.json +14 -4
@@ -238,3 +238,56 @@ export interface ListAllVegaResourcesOptions {
238
238
  /** List all Vega resources (no catalog filter). Uses GET /resources — not /resources/list, which
239
239
  * conflicts with GET /resources/{id} on some gateways (path segment "list" is treated as an id). */
240
240
  export declare function listAllVegaResources(options: ListAllVegaResourcesOptions): Promise<string>;
241
+ export interface ListTablesWithColumnsOptions {
242
+ baseUrl: string;
243
+ accessToken: string;
244
+ /** A vega catalog id, not a legacy data-connection datasource UUID. */
245
+ id: string;
246
+ keyword?: string;
247
+ limit?: number;
248
+ offset?: number;
249
+ businessDomain?: string;
250
+ autoScan?: boolean;
251
+ }
252
+ /**
253
+ * List tables with column details from a vega catalog.
254
+ *
255
+ * Two-stage fetch:
256
+ * 1. GET /api/vega-backend/v1/catalogs/{id}/resources?category=table — list summaries
257
+ * 2. For each resource: GET /api/vega-backend/v1/resources/{rid} — pull source_metadata.columns
258
+ *
259
+ * If the catalog has no resources and `autoScan=true`, triggers a discover and
260
+ * retries the list once. The optional `keyword` filters summaries client-side
261
+ * before the per-resource detail fetches — useful to keep N+1 down to k+1.
262
+ *
263
+ * `id` is a vega catalog id.
264
+ */
265
+ export declare function listTablesWithColumns(options: ListTablesWithColumnsOptions): Promise<string>;
266
+ export interface ScanMetadataOptions {
267
+ baseUrl: string;
268
+ accessToken: string;
269
+ id: string;
270
+ /** Retained for signature compatibility; ignored — vega catalog already knows its connector_type. */
271
+ dsType?: string;
272
+ businessDomain?: string;
273
+ }
274
+ /**
275
+ * Trigger a metadata scan for a vega catalog and wait for completion.
276
+ * `id` is a vega catalog id (e.g. `d7nicrcjto2s73d9g67g`), not a legacy
277
+ * data-connection datasource UUID.
278
+ */
279
+ export declare function scanMetadata(options: ScanMetadataOptions): Promise<string>;
280
+ export interface ScanDatasourceMetadataOptions {
281
+ baseUrl: string;
282
+ accessToken: string;
283
+ id: string;
284
+ businessDomain?: string;
285
+ }
286
+ /**
287
+ * Trigger a metadata scan and wait for completion. `id` is a vega catalog id.
288
+ *
289
+ * @deprecated Use {@link scanMetadata} directly. This wrapper exists only for
290
+ * backward compatibility with callers that used the old data-connection-based
291
+ * `scanDatasourceMetadata` signature.
292
+ */
293
+ export declare function scanDatasourceMetadata(options: ScanDatasourceMetadataOptions): Promise<string>;
package/dist/api/vega.js CHANGED
@@ -488,3 +488,147 @@ export async function listAllVegaResources(options) {
488
488
  businessDomain,
489
489
  });
490
490
  }
491
+ /**
492
+ * List tables with column details from a vega catalog.
493
+ *
494
+ * Two-stage fetch:
495
+ * 1. GET /api/vega-backend/v1/catalogs/{id}/resources?category=table — list summaries
496
+ * 2. For each resource: GET /api/vega-backend/v1/resources/{rid} — pull source_metadata.columns
497
+ *
498
+ * If the catalog has no resources and `autoScan=true`, triggers a discover and
499
+ * retries the list once. The optional `keyword` filters summaries client-side
500
+ * before the per-resource detail fetches — useful to keep N+1 down to k+1.
501
+ *
502
+ * `id` is a vega catalog id.
503
+ */
504
+ export async function listTablesWithColumns(options) {
505
+ const { baseUrl, accessToken, id, keyword, limit, offset, businessDomain = "bd_public", autoScan = true, } = options;
506
+ async function listResourceSummaries() {
507
+ const body = await listVegaCatalogResources({
508
+ baseUrl,
509
+ accessToken,
510
+ id,
511
+ category: "table",
512
+ limit,
513
+ offset,
514
+ businessDomain,
515
+ });
516
+ const parsed = JSON.parse(body);
517
+ return Array.isArray(parsed) ? parsed : (parsed.entries ?? parsed.data ?? []);
518
+ }
519
+ let summaries = await listResourceSummaries();
520
+ if (summaries.length === 0 && autoScan) {
521
+ await scanMetadata({ baseUrl, accessToken, id, businessDomain });
522
+ summaries = await listResourceSummaries();
523
+ }
524
+ // Keyword filter applied after autoScan guard: if the catalog has tables but
525
+ // keyword matches none, we must NOT trigger a redundant discover.
526
+ if (keyword) {
527
+ const k = keyword.toLowerCase();
528
+ summaries = summaries.filter((it) => it.name.toLowerCase().includes(k));
529
+ }
530
+ const details = await Promise.all(summaries.map(async (s) => {
531
+ let body;
532
+ try {
533
+ body = await getVegaResource({
534
+ baseUrl,
535
+ accessToken,
536
+ id: s.id,
537
+ businessDomain,
538
+ });
539
+ }
540
+ catch (err) {
541
+ const reason = err instanceof Error ? err.message : String(err);
542
+ throw new Error(`vega resource ${s.id} fetch failed: ${reason}`);
543
+ }
544
+ const parsed = JSON.parse(body);
545
+ if (Array.isArray(parsed.entries)) {
546
+ const arr = parsed.entries;
547
+ if (arr.length === 0) {
548
+ throw new Error(`vega resource ${s.id} returned empty entries`);
549
+ }
550
+ return arr[0];
551
+ }
552
+ if (Array.isArray(parsed.data)) {
553
+ const arr = parsed.data;
554
+ if (arr.length === 0) {
555
+ throw new Error(`vega resource ${s.id} returned empty data`);
556
+ }
557
+ return arr[0];
558
+ }
559
+ return parsed;
560
+ }));
561
+ const tables = [];
562
+ for (const d of details) {
563
+ const columnsRaw = (d.source_metadata?.columns ?? []);
564
+ const tablePkArray = extractPrimaryKeys(d);
565
+ const columns = columnsRaw.map((c) => {
566
+ const name = String(c.name ?? c.field_name ?? "");
567
+ const flagged = isColumnPrimaryKey(c) || tablePkArray.includes(name);
568
+ return {
569
+ name,
570
+ type: String(c.type ?? c.field_type ?? "varchar"),
571
+ comment: typeof c.description === "string"
572
+ ? c.description
573
+ : (typeof c.comment === "string" ? c.comment : undefined),
574
+ ...(flagged ? { isPrimaryKey: true } : {}),
575
+ };
576
+ });
577
+ const synthesizedPks = tablePkArray.length > 0
578
+ ? tablePkArray
579
+ : columns.filter((c) => c.isPrimaryKey).map((c) => c.name);
580
+ tables.push({
581
+ name: d.name,
582
+ columns,
583
+ ...(synthesizedPks.length > 0 ? { primaryKeys: synthesizedPks } : {}),
584
+ });
585
+ }
586
+ return JSON.stringify(tables);
587
+ }
588
+ // Two PK metadata shapes are recognized — both confirmed conventions:
589
+ // - per-column `is_primary_key: true` (data-connection metadata standard)
590
+ // - per-column `column_key === "PRI"` (MySQL INFORMATION_SCHEMA pass-through)
591
+ // - table-level `primary_keys: string[]` (composite-PK carrier)
592
+ // Other plausible spellings (camelCase, singular keys, SQLite `pk` integer) are
593
+ // intentionally NOT recognized here — adding them speculatively risks false
594
+ // matches and creates code paths the test suite can't pin down. Extend only when
595
+ // a real backend response demonstrates the need.
596
+ function isColumnPrimaryKey(col) {
597
+ if (col.is_primary_key === true)
598
+ return true;
599
+ if (typeof col.column_key === "string" && col.column_key.toUpperCase() === "PRI")
600
+ return true;
601
+ return false;
602
+ }
603
+ function extractPrimaryKeys(table) {
604
+ const arr = table.primary_keys;
605
+ if (Array.isArray(arr)) {
606
+ return arr.filter((x) => typeof x === "string");
607
+ }
608
+ return [];
609
+ }
610
+ /**
611
+ * Trigger a metadata scan for a vega catalog and wait for completion.
612
+ * `id` is a vega catalog id (e.g. `d7nicrcjto2s73d9g67g`), not a legacy
613
+ * data-connection datasource UUID.
614
+ */
615
+ export async function scanMetadata(options) {
616
+ const { baseUrl, accessToken, id, businessDomain = "bd_public" } = options;
617
+ return discoverVegaCatalog({
618
+ baseUrl,
619
+ accessToken,
620
+ id,
621
+ wait: true,
622
+ businessDomain,
623
+ });
624
+ }
625
+ /**
626
+ * Trigger a metadata scan and wait for completion. `id` is a vega catalog id.
627
+ *
628
+ * @deprecated Use {@link scanMetadata} directly. This wrapper exists only for
629
+ * backward compatibility with callers that used the old data-connection-based
630
+ * `scanDatasourceMetadata` signature.
631
+ */
632
+ export async function scanDatasourceMetadata(options) {
633
+ return scanMetadata(options);
634
+ }
package/dist/cli.js CHANGED
@@ -183,6 +183,7 @@ Commands:
183
183
  tool Tools inside a toolbox (upload OpenAPI spec, list, enable/disable)
184
184
  vega Vega observability (catalog, resource, query/sql, connector-type, health/stats/inspect)
185
185
  context-loader Context-loader MCP/HTTP (config, tools, resources, search-schema, tool-call, query-*, etc.)
186
+ trace Diagnose a single trace with rule-based analysis
186
187
  help Show this message`);
187
188
  }
188
189
  export async function run(argv) {
@@ -287,6 +288,10 @@ export async function run(argv) {
287
288
  if (command === "context-loader" || command === "context") {
288
289
  return runContextLoaderCommand(rest);
289
290
  }
291
+ if (command === "trace") {
292
+ const { runTraceCommand } = await import("./commands/trace.js");
293
+ return await runTraceCommand(rest);
294
+ }
290
295
  console.error(`Unknown command: ${command}`);
291
296
  printHelp();
292
297
  return 1;
@@ -5,7 +5,7 @@ import { loadNetwork, allObjects, allRelations, allActions, generateChecksum, va
5
5
  import { prepareBknDirectoryForImport, stripBknEncodingCliArgs, } from "../utils/bkn-encoding.js";
6
6
  import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
7
7
  import { createKnowledgeNetwork, createObjectTypes, deleteKnowledgeNetwork, buildKnowledgeNetwork, getBuildStatus, } from "../api/knowledge-networks.js";
8
- import { listTablesWithColumns, scanDatasourceMetadata } from "../api/datasources.js";
8
+ import { listTablesWithColumns, scanDatasourceMetadata } from "../api/vega.js";
9
9
  import { createDataView, findDataView } from "../api/dataviews.js";
10
10
  import { resolveFiles } from "./ds.js";
11
11
  import { buildTableName } from "./import-csv.js";
@@ -13,7 +13,7 @@ import { downloadBkn, uploadBkn, listActionSchedules, getActionSchedule, createA
13
13
  import { formatCallOutput } from "./call.js";
14
14
  import { resolveBusinessDomain } from "../config/store.js";
15
15
  import { runDsImportCsv } from "./ds.js";
16
- import { pollWithBackoff, detectDisplayKey, formatPkDetectionError, parsePkMap, resolvePrimaryKey, confirmYes, } from "./bkn-utils.js";
16
+ import { pollWithBackoff, detectDisplayKey, formatPkDetectionError, parsePkMap, resolvePrimaryKey, confirmYes, assertVegaCatalogId, } from "./bkn-utils.js";
17
17
  // ── BKN object name validation ──────────────────────────────────────────────
18
18
  // Mirrors bkn-backend OBJECT_NAME_MAX_LENGTH (interfaces/common.go:28) and
19
19
  // validateObjectName (driveradapters/validate.go:85). 40 utf-8 codepoints,
@@ -473,9 +473,11 @@ export async function runKnPullCommand(args) {
473
473
  }
474
474
  }
475
475
  // ── Create from datasource ──────────────────────────────────────────────────
476
- const KN_CREATE_FROM_DS_HELP = `kweaver bkn create-from-ds <ds-id> --name X [options]
476
+ const KN_CREATE_FROM_DS_HELP = `kweaver bkn create-from-ds <vega-catalog-id> --name X [options]
477
477
 
478
- Create a knowledge network from a datasource (dataviews + object types + optional build).
478
+ Create a knowledge network from a vega catalog datasource (dataviews + object types + optional build).
479
+ <vega-catalog-id> is a vega catalog id (use \`kweaver vega catalog list\` to find one;
480
+ legacy data-connection datasource UUIDs are no longer accepted).
479
481
 
480
482
  Options:
481
483
  --name <s> Knowledge network name (required)
@@ -548,6 +550,7 @@ export function parseKnCreateFromDsArgs(args) {
548
550
  if (!dsId || !name) {
549
551
  throw new Error("Usage: kweaver bkn create-from-ds <ds-id> --name X [options]");
550
552
  }
553
+ assertVegaCatalogId(dsId);
551
554
  const pkMap = pkMapStr ? parsePkMap(pkMapStr) : {};
552
555
  if (!businessDomain)
553
556
  businessDomain = resolveBusinessDomain();
@@ -803,9 +806,11 @@ export async function runKnCreateFromDsCommand(args, sampleRows) {
803
806
  }
804
807
  }
805
808
  // ── Create from CSV ─────────────────────────────────────────────────────────
806
- const KN_CREATE_FROM_CSV_HELP = `kweaver bkn create-from-csv <ds-id> --files <glob> --name X [options]
809
+ const KN_CREATE_FROM_CSV_HELP = `kweaver bkn create-from-csv <vega-catalog-id> --files <glob> --name X [options]
807
810
 
808
- Import CSV files into datasource, then create a knowledge network.
811
+ Import CSV files into a vega catalog datasource, then create a knowledge network.
812
+ <vega-catalog-id> is a vega catalog id (use \`kweaver vega catalog list\` to find one;
813
+ legacy data-connection datasource UUIDs are no longer accepted).
809
814
 
810
815
  Options:
811
816
  --files <s> CSV file paths (comma-separated or glob, required)
@@ -891,6 +896,7 @@ export function parseKnCreateFromCsvArgs(args) {
891
896
  if (!dsId || !files || !name) {
892
897
  throw new Error("Usage: kweaver bkn create-from-csv <ds-id> --files <glob> --name X [options]");
893
898
  }
899
+ assertVegaCatalogId(dsId);
894
900
  const pkMap = pkMapStr ? parsePkMap(pkMapStr) : {};
895
901
  if (!businessDomain)
896
902
  businessDomain = resolveBusinessDomain();
@@ -86,4 +86,13 @@ export declare function detectDisplayKey(table: {
86
86
  type: string;
87
87
  }>;
88
88
  }, primaryKey: string): string;
89
+ /**
90
+ * Reject legacy data-connection datasource UUIDs.
91
+ *
92
+ * Since the SDK migration to vega-backend (#114), commands that call
93
+ * `listTablesWithColumns` / `scanMetadata` expect a vega catalog id (a short
94
+ * slug like `d7nicrcjto2s73d9g67g`), not the UUID-shaped id stored in
95
+ * data-connection.
96
+ */
97
+ export declare function assertVegaCatalogId(id: string): void;
89
98
  export declare function confirmYes(prompt: string): Promise<boolean>;
@@ -183,6 +183,23 @@ export function detectDisplayKey(table, primaryKey) {
183
183
  }
184
184
  return primaryKey;
185
185
  }
186
+ // ── Vega catalog id guard ────────────────────────────────────────────────────
187
+ const UUID_V4_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
188
+ /**
189
+ * Reject legacy data-connection datasource UUIDs.
190
+ *
191
+ * Since the SDK migration to vega-backend (#114), commands that call
192
+ * `listTablesWithColumns` / `scanMetadata` expect a vega catalog id (a short
193
+ * slug like `d7nicrcjto2s73d9g67g`), not the UUID-shaped id stored in
194
+ * data-connection.
195
+ */
196
+ export function assertVegaCatalogId(id) {
197
+ if (UUID_V4_RE.test(id)) {
198
+ throw new Error(`expected a vega catalog id, got UUID '${id}'. ` +
199
+ `This looks like a legacy data-connection datasource UUID. ` +
200
+ `Run \`kweaver vega catalog list --keyword <name>\` to find the corresponding catalog id.`);
201
+ }
202
+ }
186
203
  // ── Interactive confirmation ─────────────────────────────────────────────────
187
204
  export function confirmYes(prompt) {
188
205
  return new Promise((resolve) => {
@@ -3,9 +3,11 @@ import { statSync } from "node:fs";
3
3
  import { glob } from "node:fs/promises";
4
4
  import { resolve as resolvePath } from "node:path";
5
5
  import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
6
- import { testDatasource, createDatasource, listDatasources, getDatasource, deleteDatasource, listTablesWithColumns, scanMetadata, } from "../api/datasources.js";
6
+ import { testDatasource, createDatasource, listDatasources, getDatasource, deleteDatasource, } from "../api/datasources.js";
7
+ import { listTablesWithColumns, scanMetadata } from "../api/vega.js";
7
8
  import { formatCallOutput } from "./call.js";
8
9
  import { resolveBusinessDomain } from "../config/store.js";
10
+ import { assertVegaCatalogId } from "./bkn-utils.js";
9
11
  import { parseCsvFile, buildTableName, splitBatches, buildFieldMappings, buildDagBody, } from "./import-csv.js";
10
12
  import { executeDataflow } from "../api/dataflow.js";
11
13
  function confirmYes(prompt) {
@@ -196,9 +198,12 @@ async function runDsTablesCommand(args) {
196
198
  id = arg;
197
199
  }
198
200
  if (!id) {
199
- console.error("Usage: kweaver ds tables <id> [--keyword X]");
201
+ console.error("Usage: kweaver ds tables <vega-catalog-id> [--keyword X]\n" +
202
+ " <vega-catalog-id> is a vega catalog id (use `kweaver vega catalog list` to find one;\n" +
203
+ " legacy data-connection datasource UUIDs are no longer accepted).");
200
204
  return 1;
201
205
  }
206
+ assertVegaCatalogId(id);
202
207
  const token = await ensureValidToken();
203
208
  const body = await listTablesWithColumns({
204
209
  baseUrl: token.baseUrl,
@@ -0,0 +1,14 @@
1
+ export interface ParsedTraceArgs {
2
+ subcommand: "diagnose" | "rules-validate" | "help";
3
+ conversationId?: string;
4
+ rulePath?: string;
5
+ out: string | null;
6
+ rulesDir: string | null;
7
+ noBuiltin: boolean;
8
+ noLlm: boolean;
9
+ baseUrl: string | null;
10
+ token: string | null;
11
+ businessDomain: string | null;
12
+ }
13
+ export declare function parseTraceArgs(argv: string[]): ParsedTraceArgs;
14
+ export declare function runTraceCommand(rest: string[]): Promise<number>;
@@ -0,0 +1,168 @@
1
+ import yargs from "yargs";
2
+ import { diagnose, TraceNotFoundError } from "../trace-core/diagnose/index.js";
3
+ import { RuleLoadError } from "../trace-core/diagnose/rule-loader.js";
4
+ import { RuleProbeError } from "../trace-core/diagnose/signal-probe.js";
5
+ import { RuleSchema } from "../trace-core/diagnose/schemas.js";
6
+ import { ensureValidToken } from "../auth/oauth.js";
7
+ import yaml from "js-yaml";
8
+ import fs from "node:fs/promises";
9
+ export function parseTraceArgs(argv) {
10
+ if (argv.length === 0) {
11
+ return defaults("help");
12
+ }
13
+ const head = argv[0];
14
+ if (head !== "diagnose") {
15
+ return defaults("help");
16
+ }
17
+ if (argv[1] === "rules" && argv[2] === "validate") {
18
+ return { ...defaults("rules-validate"), rulePath: argv[3] };
19
+ }
20
+ // diagnose <traceId> [flags...]
21
+ const parsed = yargs(argv.slice(1))
22
+ .option("out", { type: "string", default: undefined })
23
+ .option("rules", { type: "string", default: undefined })
24
+ .option("builtin", { type: "boolean", default: true }) // --no-builtin sets this to false
25
+ .option("llm", { type: "boolean", default: false }) // PR-A: forced false (--no-llm)
26
+ .option("token", { type: "string" })
27
+ .option("base-url", { type: "string" })
28
+ .option("business-domain", { alias: "bd", type: "string" })
29
+ .help(false)
30
+ .parseSync();
31
+ return {
32
+ subcommand: "diagnose",
33
+ conversationId: String(parsed._[0] ?? ""),
34
+ out: parsed.out ?? null,
35
+ rulesDir: parsed.rules ?? null,
36
+ noBuiltin: !parsed.builtin,
37
+ noLlm: !parsed.llm,
38
+ baseUrl: parsed.baseUrl ?? null,
39
+ token: parsed.token ?? null,
40
+ businessDomain: parsed.businessDomain ?? null,
41
+ };
42
+ }
43
+ function defaults(sub) {
44
+ return {
45
+ subcommand: sub,
46
+ out: null,
47
+ rulesDir: null,
48
+ noBuiltin: false,
49
+ noLlm: true,
50
+ baseUrl: null,
51
+ token: null,
52
+ businessDomain: null,
53
+ };
54
+ }
55
+ function printHelp() {
56
+ process.stdout.write(`kweaver trace — trace diagnosis commands
57
+
58
+ Subcommands:
59
+ trace diagnose <conversation_id> Diagnose the trace produced by a conversation; emit YAML report
60
+ (the id is the conversation_id returned by 'agent chat' /
61
+ 'agent sessions'; spans are fetched from agent-observability)
62
+ --out <file> Write report to file (default: stdout)
63
+ --rules <dir> Override <cwd>/diagnosis-rules/
64
+ --no-builtin Disable the 5 builtin baseline rules
65
+ --no-llm PR-A: always on; PR-B will allow disabling
66
+
67
+ trace diagnose rules validate <rule.yaml> Validate a rule yaml file (exit 0 ok, 6 fail)
68
+
69
+ Auth flags (any subcommand): --token, --base-url, --business-domain (-bd).
70
+ `);
71
+ }
72
+ export async function runTraceCommand(rest) {
73
+ const args = parseTraceArgs(rest);
74
+ if (args.subcommand === "help") {
75
+ printHelp();
76
+ return 0;
77
+ }
78
+ if (args.subcommand === "rules-validate") {
79
+ return await runRulesValidate(args.rulePath ?? "");
80
+ }
81
+ // diagnose
82
+ if (!args.conversationId) {
83
+ process.stderr.write("error: missing <conversation_id>\n");
84
+ return 2;
85
+ }
86
+ let baseUrl = args.baseUrl ?? process.env.KWEAVER_BASE_URL ?? "";
87
+ let token = args.token ?? process.env.KWEAVER_TOKEN ?? "";
88
+ const bd = args.businessDomain ?? process.env.KWEAVER_BUSINESS_DOMAIN ?? "bd_public";
89
+ // Fall back to the active platform from `~/.kweaver/` (same as agent trace),
90
+ // so users don't need to pass --base-url / --token explicitly. Tokens are
91
+ // auto-refreshed for OAuth platforms; "__NO_AUTH__" is returned for no-auth.
92
+ if (!baseUrl || !token) {
93
+ try {
94
+ const t = await ensureValidToken();
95
+ if (!baseUrl)
96
+ baseUrl = t.baseUrl;
97
+ if (!token)
98
+ token = t.accessToken;
99
+ }
100
+ catch (e) {
101
+ process.stderr.write(`error: missing --base-url / --token, and no active platform in ~/.kweaver/ — ${e.message}\n`);
102
+ return 5;
103
+ }
104
+ }
105
+ if (!baseUrl || !token) {
106
+ process.stderr.write("error: missing --base-url / --token (or KWEAVER_BASE_URL / KWEAVER_TOKEN env)\n");
107
+ return 5;
108
+ }
109
+ try {
110
+ await diagnose(args.conversationId, {
111
+ out: args.out,
112
+ rulesDir: args.rulesDir,
113
+ noBuiltin: args.noBuiltin,
114
+ noLlm: true,
115
+ agentProvider: null,
116
+ timeoutMs: 60000,
117
+ baseUrl,
118
+ token,
119
+ businessDomain: bd,
120
+ });
121
+ return 0;
122
+ }
123
+ catch (e) {
124
+ if (e instanceof TraceNotFoundError) {
125
+ process.stderr.write(`error: ${e.message}; check time window / tenant\n`);
126
+ return 4;
127
+ }
128
+ if (e instanceof RuleLoadError) {
129
+ process.stderr.write(`error: ${e.message}\n`);
130
+ return 6;
131
+ }
132
+ if (e instanceof RuleProbeError) {
133
+ process.stderr.write(`error: ${e.message}\n`);
134
+ return 6;
135
+ }
136
+ process.stderr.write(`error: ${e.message}\n`);
137
+ return 1;
138
+ }
139
+ }
140
+ async function runRulesValidate(rulePath) {
141
+ if (!rulePath) {
142
+ process.stderr.write("error: missing <rule.yaml> path\n");
143
+ return 2;
144
+ }
145
+ let raw;
146
+ try {
147
+ raw = await fs.readFile(rulePath, "utf8");
148
+ }
149
+ catch (e) {
150
+ process.stderr.write(`error: cannot read ${rulePath}: ${e.message}\n`);
151
+ return 6;
152
+ }
153
+ let parsed;
154
+ try {
155
+ parsed = yaml.load(raw);
156
+ }
157
+ catch (e) {
158
+ process.stderr.write(`error: yaml parse error: ${e.message}\n`);
159
+ return 6;
160
+ }
161
+ const result = RuleSchema.safeParse(parsed);
162
+ if (!result.success) {
163
+ process.stderr.write(`error: schema validation failed:\n${result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n`);
164
+ return 6;
165
+ }
166
+ process.stdout.write(`ok: ${rulePath} validates against diagnosis-rule/v1\n`);
167
+ return 0;
168
+ }
@@ -1,4 +1,5 @@
1
- import { testDatasource, createDatasource, listDatasources, getDatasource, deleteDatasource, listTables, listTablesWithColumns, scanMetadata, } from "../api/datasources.js";
1
+ import { testDatasource, createDatasource, listDatasources, getDatasource, deleteDatasource, listTables, } from "../api/datasources.js";
2
+ import { listTablesWithColumns, scanMetadata } from "../api/vega.js";
2
3
  export class DataSourcesResource {
3
4
  ctx;
4
5
  constructor(ctx) {
@@ -0,0 +1,2 @@
1
+ import type { Predicate } from "../types.js";
2
+ export declare const predicate: Predicate;
@@ -0,0 +1,15 @@
1
+ // PR-A approximation: counts tool calls across the entire trace, not per user turn.
2
+ // Real per-turn scoping requires turn segmentation by gen_ai.conversation.id round trips,
3
+ // which is deferred to PR-B (where the synthesizer can also use turn boundaries for narratives).
4
+ // For single-turn traces (the common case in PR-A) this approximation matches the rule semantics.
5
+ export const predicate = (trace, params) => {
6
+ const max = params.max_tool_calls_per_turn ?? 10;
7
+ const tools = trace.byKind.get("tool") ?? [];
8
+ if (tools.length <= max)
9
+ return [];
10
+ return [{
11
+ evidenceSpans: tools.map((t) => t.spanId),
12
+ excerpt: `tool calls per turn exceeded threshold: ${tools.length} > ${max}`,
13
+ bindings: { count: tools.length, max_calls: max },
14
+ }];
15
+ };
@@ -0,0 +1,16 @@
1
+ schema_version: diagnosis-rule/v1
2
+ id: excessive_tool_calls_per_turn
3
+ severity: medium
4
+ symptom: excessive_tool_calls_per_user_turn
5
+ taxonomy:
6
+ signals_axis: execution
7
+ ms_class: tool_misuse
8
+ suggested_fix:
9
+ target: decision_agent.prompt
10
+ change_template: "constrain plan to at most {{max_calls}} tool calls per user turn; observed {{count}}"
11
+ verify_with:
12
+ assertion_templates:
13
+ - "tool_call_count_per_turn <= {{max_calls}}"
14
+ predicate: builtin:excessive_tool_calls_per_turn
15
+ params:
16
+ max_tool_calls_per_turn: 10
@@ -0,0 +1,2 @@
1
+ import type { Predicate } from "../types.js";
2
+ export declare const predicate: Predicate;
@@ -0,0 +1,44 @@
1
+ function finishReason(s) {
2
+ // OTel GenAI 1.x emits an array (`finish_reasons`); older spans / fixtures
3
+ // use the singular string form. Accept both; first non-empty entry wins.
4
+ const arr = s.attributes["gen_ai.response.finish_reasons"];
5
+ if (Array.isArray(arr)) {
6
+ for (const r of arr) {
7
+ if (typeof r === "string" && r.length > 0)
8
+ return r;
9
+ }
10
+ }
11
+ const a = s.attributes["gen_ai.response.finish_reason"] ?? s.attributes["llm.finish_reason"];
12
+ return typeof a === "string" ? a : "";
13
+ }
14
+ function conversationId(s) {
15
+ const v = s.attributes["gen_ai.conversation.id"];
16
+ return typeof v === "string" ? v : "";
17
+ }
18
+ export const predicate = (trace) => {
19
+ const llms = (trace.byKind.get("llm") ?? [])
20
+ .slice()
21
+ .sort((a, b) => Number(BigInt(a.startTimeUnixNano) - BigInt(b.startTimeUnixNano)));
22
+ const hits = [];
23
+ for (let i = 0; i < llms.length; i++) {
24
+ const s = llms[i];
25
+ if (finishReason(s) !== "length")
26
+ continue;
27
+ const convId = conversationId(s);
28
+ let hasContinuation = false;
29
+ for (let j = i + 1; j < llms.length; j++) {
30
+ if (conversationId(llms[j]) === convId) {
31
+ hasContinuation = true;
32
+ break;
33
+ }
34
+ }
35
+ if (!hasContinuation) {
36
+ hits.push({
37
+ evidenceSpans: [s.spanId],
38
+ excerpt: `LLM response truncated (finish_reason=length) with no continuation span in conversation '${convId}'`,
39
+ bindings: { conversation_id: convId },
40
+ });
41
+ }
42
+ }
43
+ return hits;
44
+ };
@@ -0,0 +1,15 @@
1
+ schema_version: diagnosis-rule/v1
2
+ id: llm_response_truncated_no_continue
3
+ severity: medium
4
+ symptom: llm_output_truncated_with_no_continuation
5
+ taxonomy:
6
+ signals_axis: execution
7
+ ms_class: context_loss
8
+ suggested_fix:
9
+ target: decision_agent.prompt
10
+ change_template: "after finish_reason=length, send a continuation request or split the task earlier"
11
+ verify_with:
12
+ assertion_templates:
13
+ - "if(llm.finish_reason == 'length'): next_step in [continuation, split_task]"
14
+ predicate: builtin:llm_response_truncated_no_continue
15
+ params: {}
@@ -0,0 +1,11 @@
1
+ import { registerPredicate } from "../predicate-registry.js";
2
+ import { predicate as toolLoopNoStateChange } from "./tool-loop-no-state-change.js";
3
+ import { predicate as toolErrorSwallowed } from "./tool-error-swallowed.js";
4
+ import { predicate as retrievalEmptyNoFallback } from "./retrieval-empty-no-fallback.js";
5
+ import { predicate as llmResponseTruncatedNoContinue } from "./llm-response-truncated-no-continue.js";
6
+ import { predicate as excessiveToolCallsPerTurn } from "./excessive-tool-calls-per-turn.js";
7
+ registerPredicate("tool_loop_no_state_change", toolLoopNoStateChange);
8
+ registerPredicate("tool_error_swallowed", toolErrorSwallowed);
9
+ registerPredicate("retrieval_empty_no_fallback", retrievalEmptyNoFallback);
10
+ registerPredicate("llm_response_truncated_no_continue", llmResponseTruncatedNoContinue);
11
+ registerPredicate("excessive_tool_calls_per_turn", excessiveToolCallsPerTurn);
@@ -0,0 +1,2 @@
1
+ import type { Predicate } from "../types.js";
2
+ export declare const predicate: Predicate;