@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
package/README.md CHANGED
@@ -16,6 +16,26 @@ npm install @kweaver-ai/kweaver-sdk
16
16
 
17
17
  Requires **Node.js >= 22**.
18
18
 
19
+ ## API reference (TypeDoc)
20
+
21
+ Generate HTML from source + TSDoc, then open `docs/reference/typescript-api-html/index.html` (gitignored), or serve locally:
22
+
23
+ HTML reference auto-discovers **`src/resources/*`**, **`src/api/*`**, and **`src/auth/*`** via TypeDoc's `entryPointStrategy: "expand"` (`typedoc.json`), so newly added modules appear without editing the config. The English build uses `README.md` as the cover page; the Chinese build uses `README.zh.md`. **"Defined in"** GitHub links read `gitRevision` from `TYPEDOC_GIT_REVISION` → `GITHUB_SHA` → fallback `"main"`; CI should pin links to the build SHA: `TYPEDOC_GIT_REVISION=$GITHUB_SHA npm run docs`.
24
+
25
+ TypeDoc does **not** ship a single-site EN/ZH toggle. Use two outputs: English UI (default) and Chinese UI strings (`docs:zh`, primarily localizes navigation chrome). API descriptions come from TSDoc and stay English unless you maintain duplicate comments elsewhere.
26
+
27
+ ```bash
28
+ cd packages/typescript
29
+ npm install
30
+ npm run docs # English UI → docs/reference/typescript-api-html/
31
+ npm run docs:serve # generate + serve http://127.0.0.1:8766
32
+ npm run docs:zh # Chinese UI + README.zh.md → docs/reference/typescript-api-html-zh/
33
+ npm run docs:serve:zh # generate + serve http://127.0.0.1:8767
34
+ npm run docs:all # both folders
35
+ ```
36
+
37
+ > Files inside `docs/reference/**` are generated. Edit `packages/typescript/README*.md` and TSDoc comments in source instead — anything copied into `media/` is overwritten on the next build.
38
+
19
39
  ## Quick Start
20
40
 
21
41
  ### Authenticate
package/README.zh.md CHANGED
@@ -16,6 +16,24 @@ npm install @kweaver-ai/kweaver-sdk
16
16
 
17
17
  需要 **Node.js >= 22**。
18
18
 
19
+ ## API 参考(TypeDoc)
20
+
21
+ 由源码与 TSDoc 生成 HTML,产物在 `docs/reference/typescript-api-html/index.html`(已 gitignore)。`typedoc.json` 用 **`entryPointStrategy: "expand"`** 自动展开 **`src/resources`**、**`src/api`**、**`src/auth`** 整目录,新增模块无需改配置。英文构建用 `README.md`,中文构建用 `README.zh.md`。**「Defined in」** 链接的 `gitRevision` 取 `TYPEDOC_GIT_REVISION` → `GITHUB_SHA` → 回退 `"main"`;CI 中固定到当前 SHA:`TYPEDOC_GIT_REVISION=$GITHUB_SHA npm run docs`。
22
+
23
+ TypeDoc **不会在同一个站点里提供中英文切换**。做法是生成两套目录:英文界面(默认)与中文界面(**`npm run docs:zh`**),主要翻译导航等界面文案;**API 说明仍以源码里的 TSDoc(英文)为准**。
24
+
25
+ ```bash
26
+ cd packages/typescript
27
+ npm install
28
+ npm run docs # 英文界面 → docs/reference/typescript-api-html/
29
+ npm run docs:serve # 生成并访问 http://127.0.0.1:8766
30
+ npm run docs:zh # 中文界面 + README.zh.md → docs/reference/typescript-api-html-zh/
31
+ npm run docs:serve:zh # 生成并访问 http://127.0.0.1:8767
32
+ npm run docs:all # 两套产物都生成
33
+ ```
34
+
35
+ > `docs/reference/**` 下的所有文件均为生成产物。请编辑 `packages/typescript/README*.md` 与源码 TSDoc 注释;`media/` 下的拷贝在下次构建时会被覆盖。
36
+
19
37
  ## 快速上手
20
38
 
21
39
  ### 认证
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Single source of truth for `/api/agent-observability/v1/traces/_search` —
3
+ * the OpenSearch-style endpoint that backs both `kweaver agent trace` and
4
+ * `kweaver trace diagnose`.
5
+ *
6
+ * Owns: endpoint URL, auth/headers (via `./headers.ts`), the two-hop strategy
7
+ * (conversation_id → traceIds → spans), and HTTP error handling.
8
+ *
9
+ * Does NOT own normalization: callers receive raw OpenSearch `_source` objects
10
+ * and shape them as needed (TraceSpan for UI rendering, RawSpan for diagnose
11
+ * rules). This keeps the wire contract in one place while letting each consumer
12
+ * pick its own minimal field set.
13
+ */
14
+ export declare const TRACE_SEARCH_PATH = "/api/agent-observability/v1/traces/_search";
15
+ export declare class TraceFetchError extends Error {
16
+ readonly status?: number | undefined;
17
+ readonly url?: string | undefined;
18
+ constructor(message: string, status?: number | undefined, url?: string | undefined);
19
+ }
20
+ export interface FetchRawSpansByConversationOpts {
21
+ baseUrl: string;
22
+ accessToken: string;
23
+ businessDomain: string;
24
+ conversationId: string;
25
+ /** Cap on `terms` aggregation bucket count. Default 100. */
26
+ maxTraceIds?: number;
27
+ /** Cap on spans returned by the second query. Default 2000. */
28
+ maxSpans?: number;
29
+ }
30
+ export interface FetchRawSpansByConversationResult {
31
+ /** Distinct traceIds observed for this conversation, in agg-bucket order. */
32
+ traceIds: string[];
33
+ /** Raw `_source` objects, unmodified. Callers do their own normalization. */
34
+ rawSources: Array<Record<string, unknown>>;
35
+ /** True if the agg saw `sum_other_doc_count > 0` (more traceIds than maxTraceIds). */
36
+ truncated: boolean;
37
+ }
38
+ export declare function postTraceSearch(baseUrl: string, accessToken: string, businessDomain: string, body: unknown): Promise<Record<string, unknown>>;
39
+ /**
40
+ * Two-hop fetch of all `_source` documents belonging to a conversation.
41
+ *
42
+ * Hop 1: aggregate `traceId.keyword` for spans tagged with
43
+ * `attributes.gen_ai.conversation.id.keyword == conversationId`.
44
+ * Hop 2: fetch every span whose `traceId.keyword` is in the agg buckets.
45
+ *
46
+ * Fixture-compat fast path: when the first response carries no `aggregations`
47
+ * but does carry `hits.hits`, that is taken as a flat spans payload and hop 2
48
+ * is skipped. Existing e2e fixtures (single OpenSearch payload per file) thus
49
+ * remain usable with a single mock-fetch response.
50
+ */
51
+ export declare function fetchRawSpansByConversation(opts: FetchRawSpansByConversationOpts): Promise<FetchRawSpansByConversationResult>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Single source of truth for `/api/agent-observability/v1/traces/_search` —
3
+ * the OpenSearch-style endpoint that backs both `kweaver agent trace` and
4
+ * `kweaver trace diagnose`.
5
+ *
6
+ * Owns: endpoint URL, auth/headers (via `./headers.ts`), the two-hop strategy
7
+ * (conversation_id → traceIds → spans), and HTTP error handling.
8
+ *
9
+ * Does NOT own normalization: callers receive raw OpenSearch `_source` objects
10
+ * and shape them as needed (TraceSpan for UI rendering, RawSpan for diagnose
11
+ * rules). This keeps the wire contract in one place while letting each consumer
12
+ * pick its own minimal field set.
13
+ */
14
+ import { buildHeaders } from "./headers.js";
15
+ export const TRACE_SEARCH_PATH = "/api/agent-observability/v1/traces/_search";
16
+ export class TraceFetchError extends Error {
17
+ status;
18
+ url;
19
+ constructor(message, status, url) {
20
+ super(message);
21
+ this.status = status;
22
+ this.url = url;
23
+ this.name = "TraceFetchError";
24
+ }
25
+ }
26
+ export async function postTraceSearch(baseUrl, accessToken, businessDomain, body) {
27
+ const url = `${baseUrl.replace(/\/+$/, "")}${TRACE_SEARCH_PATH}`;
28
+ const res = await fetch(url, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ ...buildHeaders(accessToken, businessDomain),
33
+ },
34
+ body: JSON.stringify(body),
35
+ });
36
+ const text = await res.text();
37
+ if (!res.ok) {
38
+ throw new TraceFetchError(`trace search failed: HTTP ${res.status} ${res.statusText} — ${text.slice(0, 200)}`, res.status, url);
39
+ }
40
+ if (!text)
41
+ return {};
42
+ try {
43
+ return JSON.parse(text);
44
+ }
45
+ catch (err) {
46
+ throw new TraceFetchError(`trace search: invalid JSON response — ${err.message}`);
47
+ }
48
+ }
49
+ /**
50
+ * Two-hop fetch of all `_source` documents belonging to a conversation.
51
+ *
52
+ * Hop 1: aggregate `traceId.keyword` for spans tagged with
53
+ * `attributes.gen_ai.conversation.id.keyword == conversationId`.
54
+ * Hop 2: fetch every span whose `traceId.keyword` is in the agg buckets.
55
+ *
56
+ * Fixture-compat fast path: when the first response carries no `aggregations`
57
+ * but does carry `hits.hits`, that is taken as a flat spans payload and hop 2
58
+ * is skipped. Existing e2e fixtures (single OpenSearch payload per file) thus
59
+ * remain usable with a single mock-fetch response.
60
+ */
61
+ export async function fetchRawSpansByConversation(opts) {
62
+ const { baseUrl, accessToken, businessDomain, conversationId } = opts;
63
+ const maxTraceIds = opts.maxTraceIds ?? 100;
64
+ const maxSpans = opts.maxSpans ?? 2000;
65
+ const aggResult = await postTraceSearch(baseUrl, accessToken, businessDomain, {
66
+ size: 0,
67
+ query: { term: { "attributes.gen_ai.conversation.id.keyword": conversationId } },
68
+ aggs: { tids: { terms: { field: "traceId.keyword", size: maxTraceIds } } },
69
+ });
70
+ const aggregations = aggResult.aggregations;
71
+ if (!aggregations) {
72
+ const directHits = aggResult.hits?.hits;
73
+ if (Array.isArray(directHits)) {
74
+ const rawSources = [];
75
+ const traceIds = new Set();
76
+ for (const h of directHits) {
77
+ if (!h._source)
78
+ continue;
79
+ rawSources.push(h._source);
80
+ const tid = h._source.traceId ?? h._source.trace_id;
81
+ if (typeof tid === "string" && tid.length > 0)
82
+ traceIds.add(tid);
83
+ }
84
+ return { traceIds: [...traceIds], rawSources, truncated: false };
85
+ }
86
+ }
87
+ const tids = aggregations?.tids;
88
+ const buckets = tids?.buckets ?? [];
89
+ const truncated = (tids?.sum_other_doc_count ?? 0) > 0;
90
+ const traceIds = buckets
91
+ .map((b) => b.key)
92
+ .filter((k) => typeof k === "string" && k.length > 0);
93
+ if (traceIds.length === 0) {
94
+ return { traceIds: [], rawSources: [], truncated: false };
95
+ }
96
+ const spansResult = await postTraceSearch(baseUrl, accessToken, businessDomain, {
97
+ size: maxSpans,
98
+ query: { terms: { "traceId.keyword": traceIds } },
99
+ sort: [{ startTime: "asc" }],
100
+ });
101
+ const hits = spansResult.hits?.hits ?? [];
102
+ const rawSources = [];
103
+ for (const h of hits) {
104
+ if (h._source)
105
+ rawSources.push(h._source);
106
+ }
107
+ return { traceIds, rawSources, truncated };
108
+ }
@@ -61,14 +61,10 @@ export interface TracesByConversationResult {
61
61
  */
62
62
  export declare function listConversations(opts: ListConversationsOptions): Promise<string>;
63
63
  /**
64
- * Fetch all spans belonging to a conversation via trace-ai's OpenSearch-style _search.
65
- *
66
- * Two-hop strategy (see kweaver-sdk#115):
67
- * 1. Aggregate traceIds for spans tagged with gen_ai.conversation.id == conversationId.
68
- * 2. Fetch every span sharing those traceIds — this recovers pipeline spans
69
- * (HTTP entry, internal RPCs, prompt-build) that are not tagged with conversation_id.
70
- *
71
- * Returns a structured result; callers can format as tree/perf/evidence views or stringify.
64
+ * Fetch all spans belonging to a conversation, shaped as `TraceSpan[]` for UI
65
+ * rendering (tree/perf/evidence/reasoning views). The wire-level two-hop and
66
+ * auth/header concerns live in `./agent-observability`; this function only
67
+ * normalizes the raw `_source` documents.
72
68
  */
73
69
  export declare function getTracesByConversation(opts: GetTracesOptions): Promise<TracesByConversationResult>;
74
70
  /**
@@ -1,4 +1,5 @@
1
1
  import { buildHeaders } from "./headers.js";
2
+ import { fetchRawSpansByConversation } from "./agent-observability.js";
2
3
  function buildConversationsUrl(baseUrl, agentKey) {
3
4
  const base = baseUrl.replace(/\/+$/, "");
4
5
  return `${base}/api/agent-factory/v1/app/${agentKey}/conversation`;
@@ -29,32 +30,6 @@ export async function listConversations(opts) {
29
30
  }
30
31
  return body || "[]";
31
32
  }
32
- function buildTraceSearchUrl(baseUrl) {
33
- const base = baseUrl.replace(/\/+$/, "");
34
- return `${base}/api/agent-observability/v1/traces/_search`;
35
- }
36
- async function postTraceSearch(baseUrl, accessToken, businessDomain, body) {
37
- const response = await fetch(buildTraceSearchUrl(baseUrl), {
38
- method: "POST",
39
- headers: {
40
- "Content-Type": "application/json",
41
- ...buildHeaders(accessToken, businessDomain),
42
- },
43
- body: JSON.stringify(body),
44
- });
45
- const text = await response.text();
46
- if (!response.ok) {
47
- throw new Error(`getTracesByConversation failed: HTTP ${response.status} ${response.statusText} — ${text.slice(0, 200)}`);
48
- }
49
- if (!text)
50
- return {};
51
- try {
52
- return JSON.parse(text);
53
- }
54
- catch (err) {
55
- throw new Error(`getTracesByConversation: invalid JSON response — ${err.message}`);
56
- }
57
- }
58
33
  function computeDurationNanos(source) {
59
34
  if (typeof source.durationInNanos === "number")
60
35
  return source.durationInNanos;
@@ -113,45 +88,28 @@ function normalizeSpan(source) {
113
88
  };
114
89
  }
115
90
  /**
116
- * Fetch all spans belonging to a conversation via trace-ai's OpenSearch-style _search.
117
- *
118
- * Two-hop strategy (see kweaver-sdk#115):
119
- * 1. Aggregate traceIds for spans tagged with gen_ai.conversation.id == conversationId.
120
- * 2. Fetch every span sharing those traceIds — this recovers pipeline spans
121
- * (HTTP entry, internal RPCs, prompt-build) that are not tagged with conversation_id.
122
- *
123
- * Returns a structured result; callers can format as tree/perf/evidence views or stringify.
91
+ * Fetch all spans belonging to a conversation, shaped as `TraceSpan[]` for UI
92
+ * rendering (tree/perf/evidence/reasoning views). The wire-level two-hop and
93
+ * auth/header concerns live in `./agent-observability`; this function only
94
+ * normalizes the raw `_source` documents.
124
95
  */
125
96
  export async function getTracesByConversation(opts) {
126
- const { baseUrl, accessToken, conversationId, businessDomain = "bd_public", maxTraceIds = 100, maxSpans = 2000, } = opts;
127
- const aggResult = await postTraceSearch(baseUrl, accessToken, businessDomain, {
128
- size: 0,
129
- query: { term: { "attributes.gen_ai.conversation.id.keyword": conversationId } },
130
- aggs: { tids: { terms: { field: "traceId.keyword", size: maxTraceIds } } },
131
- });
132
- const aggregations = aggResult.aggregations;
133
- const tids = aggregations?.tids;
134
- const buckets = tids?.buckets ?? [];
135
- const truncated = (tids?.sum_other_doc_count ?? 0) > 0;
136
- const traceIds = buckets.map((b) => b.key).filter((k) => typeof k === "string" && k.length > 0);
137
- if (traceIds.length === 0) {
138
- return { conversationId, traceIds: [], spans: [], truncated: false };
139
- }
140
- const spansResult = await postTraceSearch(baseUrl, accessToken, businessDomain, {
141
- size: maxSpans,
142
- query: { terms: { "traceId.keyword": traceIds } },
143
- sort: [{ startTime: "asc" }],
97
+ const { baseUrl, accessToken, conversationId, businessDomain = "bd_public", maxTraceIds, maxSpans, } = opts;
98
+ const fetched = await fetchRawSpansByConversation({
99
+ baseUrl,
100
+ accessToken,
101
+ businessDomain,
102
+ conversationId,
103
+ maxTraceIds,
104
+ maxSpans,
144
105
  });
145
- const hits = spansResult.hits?.hits ?? [];
146
106
  const spans = [];
147
- for (const hit of hits) {
148
- if (!hit._source)
149
- continue;
150
- const span = normalizeSpan(hit._source);
107
+ for (const src of fetched.rawSources) {
108
+ const span = normalizeSpan(src);
151
109
  if (span)
152
110
  spans.push(span);
153
111
  }
154
- return { conversationId, traceIds, spans, truncated };
112
+ return { conversationId, traceIds: fetched.traceIds, spans, truncated: fetched.truncated };
155
113
  }
156
114
  /**
157
115
  * List messages for a conversation.
@@ -58,23 +58,5 @@ export interface ListTablesOptions {
58
58
  businessDomain?: string;
59
59
  }
60
60
  export declare function listTables(options: ListTablesOptions): Promise<string>;
61
- export interface ListTablesWithColumnsOptions extends ListTablesOptions {
62
- autoScan?: boolean;
63
- }
64
- /** List tables with column details. Optionally triggers metadata scan if no tables found. */
65
- export declare function listTablesWithColumns(options: ListTablesWithColumnsOptions): Promise<string>;
66
- export interface ScanMetadataOptions {
67
- baseUrl: string;
68
- accessToken: string;
69
- id: string;
70
- dsType?: string;
71
- businessDomain?: string;
72
- }
73
- export declare function scanMetadata(options: ScanMetadataOptions): Promise<string>;
74
- export interface ScanDatasourceMetadataOptions {
75
- baseUrl: string;
76
- accessToken: string;
77
- id: string;
78
- businessDomain?: string;
79
- }
80
- export declare function scanDatasourceMetadata(options: ScanDatasourceMetadataOptions): Promise<string>;
61
+ export { listTablesWithColumns, scanMetadata, scanDatasourceMetadata, } from "./vega.js";
62
+ export type { ListTablesWithColumnsOptions, ScanMetadataOptions, ScanDatasourceMetadataOptions, } from "./vega.js";
@@ -130,126 +130,10 @@ export async function listTables(options) {
130
130
  }
131
131
  return body;
132
132
  }
133
- /** List tables with column details. Optionally triggers metadata scan if no tables found. */
134
- export async function listTablesWithColumns(options) {
135
- const { id, autoScan = true, ...rest } = options;
136
- let body = await listTables({ ...rest, id });
137
- const parsed = JSON.parse(body);
138
- let items = Array.isArray(parsed) ? parsed : (parsed.entries ?? parsed.data ?? []);
139
- if (items.length === 0 && autoScan) {
140
- await scanMetadata({
141
- baseUrl: rest.baseUrl,
142
- accessToken: rest.accessToken,
143
- id,
144
- businessDomain: rest.businessDomain,
145
- });
146
- body = await listTables({ ...rest, id });
147
- const parsed2 = JSON.parse(body);
148
- items = Array.isArray(parsed2) ? parsed2 : (parsed2.entries ?? parsed2.data ?? []);
149
- }
150
- const base = rest.baseUrl.replace(/\/+$/, "");
151
- const tables = [];
152
- for (const t of items) {
153
- const tableId = String(t.id ?? "");
154
- const tableName = String(t.name ?? "");
155
- let columnsRaw = (t.columns ?? t.fields ?? []);
156
- if (columnsRaw.length === 0 && tableId) {
157
- const tableUrl = `${base}/api/data-connection/v1/metadata/table/${encodeURIComponent(tableId)}?limit=-1`;
158
- const colResponse = await fetch(tableUrl, {
159
- method: "GET",
160
- headers: buildHeaders(rest.accessToken, rest.businessDomain ?? "bd_public"),
161
- });
162
- const colData = (await colResponse.json());
163
- columnsRaw = Array.isArray(colData) ? colData : (colData.entries ?? colData.data ?? []);
164
- }
165
- const tablePkArray = extractPrimaryKeys(t);
166
- const columns = columnsRaw.map((c) => {
167
- const name = String(c.name ?? c.field_name ?? "");
168
- const flagged = isColumnPrimaryKey(c) || tablePkArray.includes(name);
169
- return {
170
- name,
171
- type: String(c.type ?? c.field_type ?? "varchar"),
172
- comment: typeof c.comment === "string" ? c.comment : undefined,
173
- ...(flagged ? { isPrimaryKey: true } : {}),
174
- };
175
- });
176
- // Reconcile: if backend gave per-column flags but no table-level array,
177
- // synthesize one so downstream callers have a single PK source of truth.
178
- const synthesizedPks = tablePkArray.length > 0
179
- ? tablePkArray
180
- : columns.filter((c) => c.isPrimaryKey).map((c) => c.name);
181
- tables.push({
182
- name: tableName,
183
- columns,
184
- ...(synthesizedPks.length > 0 ? { primaryKeys: synthesizedPks } : {}),
185
- });
186
- }
187
- return JSON.stringify(tables);
188
- }
189
- // Two PK metadata shapes are recognized — both confirmed conventions:
190
- // - per-column `is_primary_key: true` (data-connection metadata standard)
191
- // - per-column `column_key === "PRI"` (MySQL INFORMATION_SCHEMA pass-through)
192
- // - table-level `primary_keys: string[]` (composite-PK carrier)
193
- // Other plausible spellings (camelCase, singular keys, SQLite `pk` integer) are
194
- // intentionally NOT recognized here — adding them speculatively risks false
195
- // matches and creates code paths the test suite can't pin down. Extend only when
196
- // a real backend response demonstrates the need.
197
- function isColumnPrimaryKey(col) {
198
- if (col.is_primary_key === true)
199
- return true;
200
- if (typeof col.column_key === "string" && col.column_key.toUpperCase() === "PRI")
201
- return true;
202
- return false;
203
- }
204
- function extractPrimaryKeys(table) {
205
- const arr = table.primary_keys;
206
- if (Array.isArray(arr)) {
207
- return arr.filter((x) => typeof x === "string");
208
- }
209
- return [];
210
- }
211
- export async function scanMetadata(options) {
212
- const { baseUrl, accessToken, id, dsType = "mysql", businessDomain = "bd_public", } = options;
213
- const base = baseUrl.replace(/\/+$/, "");
214
- const scanUrl = `${base}/api/data-connection/v1/metadata/scan`;
215
- const statusUrl = (taskId) => `${base}/api/data-connection/v1/metadata/scan/${taskId}`;
216
- const scanBody = JSON.stringify({
217
- scan_name: `sdk_scan_${id.slice(0, 8)}`,
218
- type: 0,
219
- ds_info: { ds_id: id, ds_type: dsType },
220
- use_default_template: true,
221
- use_multi_threads: true,
222
- status: "open",
223
- });
224
- const scanResponse = await fetch(scanUrl, {
225
- method: "POST",
226
- headers: {
227
- ...buildHeaders(accessToken, businessDomain),
228
- "content-type": "application/json",
229
- },
230
- body: scanBody,
231
- });
232
- const scanResult = await scanResponse.json();
233
- const taskId = scanResult.id ?? "";
234
- for (let i = 0; i < 30; i += 1) {
235
- const delay = Math.min(2000 * Math.pow(1.5, i), 15000);
236
- await new Promise((r) => setTimeout(r, delay));
237
- const statusResponse = await fetch(statusUrl(taskId), {
238
- method: "GET",
239
- headers: buildHeaders(accessToken, businessDomain),
240
- });
241
- const statusData = (await statusResponse.json());
242
- if (statusData.status === "success" || statusData.status === "fail") {
243
- break;
244
- }
245
- }
246
- return taskId;
247
- }
248
- // Looks up a datasource's type then triggers a metadata scan, so callers
249
- // don't have to repeat the GET-then-scan dance whenever a flow needs the
250
- // platform catalog refreshed (after import-csv, before discovering tables).
251
- export async function scanDatasourceMetadata(options) {
252
- const dsBody = await getDatasource(options);
253
- const dsType = JSON.parse(dsBody).type ?? "mysql";
254
- return scanMetadata({ ...options, dsType });
255
- }
133
+ // ── Vega catalog re-exports (backward compatibility) ─────────────────────────
134
+ //
135
+ // listTablesWithColumns, scanMetadata, and scanDatasourceMetadata now live in
136
+ // vega.ts (they talk exclusively to vega-backend, not data-connection).
137
+ // Re-exported here so existing callers don't break — new code should import
138
+ // from "../api/vega.js" directly.
139
+ export { listTablesWithColumns, scanMetadata, scanDatasourceMetadata, } from "./vega.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `RawSpan`-flavored view of conversation trace data, for diagnose rule
3
+ * predicates. The HTTP / two-hop / auth concerns live in `./agent-observability`;
4
+ * this module only normalizes the raw `_source` documents into the minimal
5
+ * span shape rules read.
6
+ */
7
+ export { TraceFetchError } from "./agent-observability.js";
8
+ export interface GetSpansByConversationIdOpts {
9
+ baseUrl: string;
10
+ token: string;
11
+ businessDomain: string;
12
+ conversationId: string;
13
+ /** Cap on `terms` aggregation bucket count. Default 100. */
14
+ maxTraceIds?: number;
15
+ /** Cap on spans returned by the second query. Default 2000. */
16
+ maxSpans?: number;
17
+ }
18
+ export interface RawSpan {
19
+ spanId: string;
20
+ parentSpanId: string | null;
21
+ name?: string;
22
+ startTimeUnixNano?: string;
23
+ endTimeUnixNano?: string;
24
+ status?: {
25
+ code?: string;
26
+ };
27
+ attributes?: Record<string, unknown>;
28
+ /** OTel traceId for the trace this span belongs to (when known). */
29
+ traceId?: string;
30
+ }
31
+ export interface GetSpansByConversationIdResult {
32
+ /** Distinct traceIds observed for this conversation. */
33
+ traceIds: string[];
34
+ /** All spans across all observed traceIds, mapped to `RawSpan` shape. */
35
+ spans: RawSpan[];
36
+ /** True if the agg saw `sum_other_doc_count > 0` (more traceIds than maxTraceIds). */
37
+ truncated: boolean;
38
+ }
39
+ /**
40
+ * ISO timestamp → nanos-since-epoch string. Preserves up to 9 fractional digits.
41
+ * Falls back to ms precision when the input lacks a fractional component.
42
+ */
43
+ export declare function isoToNanos(iso: string | undefined): string | undefined;
44
+ export declare function getSpansByConversationId(opts: GetSpansByConversationIdOpts): Promise<GetSpansByConversationIdResult>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `RawSpan`-flavored view of conversation trace data, for diagnose rule
3
+ * predicates. The HTTP / two-hop / auth concerns live in `./agent-observability`;
4
+ * this module only normalizes the raw `_source` documents into the minimal
5
+ * span shape rules read.
6
+ */
7
+ import { fetchRawSpansByConversation } from "./agent-observability.js";
8
+ export { TraceFetchError } from "./agent-observability.js";
9
+ /**
10
+ * ISO timestamp → nanos-since-epoch string. Preserves up to 9 fractional digits.
11
+ * Falls back to ms precision when the input lacks a fractional component.
12
+ */
13
+ export function isoToNanos(iso) {
14
+ if (!iso)
15
+ return undefined;
16
+ // "YYYY-MM-DDTHH:MM:SS.fffffffffZ" or "...+08:00"
17
+ const m = iso.match(/^(.+?)\.(\d{1,9})(Z|[+-]\d{2}:?\d{2})$/);
18
+ if (!m) {
19
+ const ms = Date.parse(iso);
20
+ if (Number.isNaN(ms))
21
+ return undefined;
22
+ return (BigInt(ms) * 1000000n).toString();
23
+ }
24
+ const ms = Date.parse(m[1] + m[3]);
25
+ if (Number.isNaN(ms))
26
+ return undefined;
27
+ const frac = m[2].padEnd(9, "0").slice(0, 9);
28
+ const seconds = BigInt(Math.floor(ms / 1000));
29
+ return (seconds * 1000000000n + BigInt(frac)).toString();
30
+ }
31
+ function normalizeToRawSpan(source) {
32
+ const spanIdRaw = source.spanId ?? source.span_id;
33
+ const spanId = typeof spanIdRaw === "string" ? spanIdRaw : "";
34
+ if (!spanId)
35
+ return null;
36
+ const parentRaw = source.parentSpanId ?? source.parent_span_id ?? source.parentSpanID;
37
+ const parentSpanId = typeof parentRaw === "string" && parentRaw !== "" && parentRaw !== "0" ? parentRaw : null;
38
+ // Prefer pre-normalized nanos (synthetic fixtures); else derive from ISO.
39
+ let startTimeUnixNano;
40
+ let endTimeUnixNano;
41
+ if (typeof source.startTimeUnixNano === "string")
42
+ startTimeUnixNano = source.startTimeUnixNano;
43
+ else if (typeof source.startTime === "string")
44
+ startTimeUnixNano = isoToNanos(source.startTime);
45
+ if (typeof source.endTimeUnixNano === "string")
46
+ endTimeUnixNano = source.endTimeUnixNano;
47
+ else if (typeof source.endTime === "string")
48
+ endTimeUnixNano = isoToNanos(source.endTime);
49
+ const status = source.status;
50
+ const attributes = source.attributes;
51
+ const name = typeof source.name === "string" ? source.name : undefined;
52
+ const traceIdRaw = source.traceId ?? source.trace_id;
53
+ const traceId = typeof traceIdRaw === "string" ? traceIdRaw : undefined;
54
+ return {
55
+ spanId,
56
+ parentSpanId,
57
+ name,
58
+ startTimeUnixNano,
59
+ endTimeUnixNano,
60
+ status,
61
+ attributes,
62
+ traceId,
63
+ };
64
+ }
65
+ export async function getSpansByConversationId(opts) {
66
+ const fetched = await fetchRawSpansByConversation({
67
+ baseUrl: opts.baseUrl,
68
+ accessToken: opts.token,
69
+ businessDomain: opts.businessDomain,
70
+ conversationId: opts.conversationId,
71
+ maxTraceIds: opts.maxTraceIds,
72
+ maxSpans: opts.maxSpans,
73
+ });
74
+ const spans = [];
75
+ for (const src of fetched.rawSources) {
76
+ const span = normalizeToRawSpan(src);
77
+ if (span)
78
+ spans.push(span);
79
+ }
80
+ return { traceIds: fetched.traceIds, spans, truncated: fetched.truncated };
81
+ }