@atlanai/sdk 0.2.3 → 0.2.4

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/client.d.ts CHANGED
@@ -42,10 +42,24 @@ export declare class ServiceNamespace {
42
42
  readonly operations: readonly Omit<Operation, "service">[];
43
43
  /** Contract method names for this service, used to resolve a forwarded call. */
44
44
  readonly operationMethods: ReadonlySet<string>;
45
- constructor(name: string, module: GeneratedModule, configuration: GeneratedConfiguration, serviceOperations: readonly Omit<Operation, "service">[]);
45
+ /**
46
+ * Whether the configured credential is an API key rather than a user token.
47
+ * Only the credential's shape was read, never its value.
48
+ */
49
+ readonly apiKeyCredential: boolean;
50
+ constructor(name: string, module: GeneratedModule, configuration: GeneratedConfiguration, serviceOperations: readonly Omit<Operation, "service">[], apiKeyCredential?: boolean);
46
51
  }
47
52
  /** Build a namespace that also answers its operations by method name. */
48
- export declare function createService(name: string, module: GeneratedModule, configuration: GeneratedConfiguration, serviceOperations: readonly Omit<Operation, "service">[]): Service;
53
+ export declare function createService(name: string, module: GeneratedModule, configuration: GeneratedConfiguration, serviceOperations: readonly Omit<Operation, "service">[], apiKeyCredential?: boolean): Service;
54
+ /**
55
+ * Whether a credential is a signed JWT, which is the user-token shape.
56
+ *
57
+ * Only the structure is read, and the value is never logged or compared to a
58
+ * literal: three base64url segments whose first decodes to a JSON object. The
59
+ * gateway draws the same line itself - its 401 vocabulary splits
60
+ * `invalid_api_key`/`unknown_key` from the whole `token_*` family.
61
+ */
62
+ export declare function looksLikeAJwt(token: string): boolean;
49
63
  export interface AtlanClient extends ResourceRoots {
50
64
  }
51
65
  export declare class AtlanClient {
package/dist/client.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AtlanClient = exports.ServiceNamespace = exports.WORKSPACE_HEADER = void 0;
37
37
  exports.createService = createService;
38
+ exports.looksLikeAJwt = looksLikeAJwt;
38
39
  const agentRaw = __importStar(require("./raw/agent/index"));
39
40
  const apiRaw = __importStar(require("./raw/api/index"));
40
41
  const evalRaw = __importStar(require("./raw/eval/index"));
@@ -57,8 +58,14 @@ class ServiceNamespace {
57
58
  operations;
58
59
  /** Contract method names for this service, used to resolve a forwarded call. */
59
60
  operationMethods;
60
- constructor(name, module, configuration, serviceOperations) {
61
+ /**
62
+ * Whether the configured credential is an API key rather than a user token.
63
+ * Only the credential's shape was read, never its value.
64
+ */
65
+ apiKeyCredential;
66
+ constructor(name, module, configuration, serviceOperations, apiKeyCredential = false) {
61
67
  this.name = name;
68
+ this.apiKeyCredential = apiKeyCredential;
62
69
  this.configuration = configuration;
63
70
  this.operations = serviceOperations;
64
71
  const operationMethods = new Set(serviceOperations.map((operation) => methodName(operation.operation_id)));
@@ -69,15 +76,15 @@ class ServiceNamespace {
69
76
  continue;
70
77
  const name = exportName.replace(/Api$/, "").replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
71
78
  const raw = new value(configuration);
72
- apis[name] = wrapGeneratedAPI(raw, operationMethods);
79
+ apis[name] = wrapGeneratedAPI(raw, operationMethods, apiKeyCredential);
73
80
  }
74
81
  this.apis = Object.freeze(apis);
75
82
  }
76
83
  }
77
84
  exports.ServiceNamespace = ServiceNamespace;
78
85
  /** Build a namespace that also answers its operations by method name. */
79
- function createService(name, module, configuration, serviceOperations) {
80
- const namespace = new ServiceNamespace(name, module, configuration, serviceOperations);
86
+ function createService(name, module, configuration, serviceOperations, apiKeyCredential = false) {
87
+ const namespace = new ServiceNamespace(name, module, configuration, serviceOperations, apiKeyCredential);
81
88
  return forwardOperations(namespace, namespace.operationMethods);
82
89
  }
83
90
  /**
@@ -138,7 +145,69 @@ function methodName(operationId) {
138
145
  const [service, operation] = operationId.split(".", 2);
139
146
  return service + operation.charAt(0).toUpperCase() + operation.slice(1);
140
147
  }
141
- function wrapGeneratedAPI(api, operationMethods) {
148
+ /**
149
+ * Recover a success body the generated client would have thrown away.
150
+ *
151
+ * A handful of operations document their 200 as `application/json` with no
152
+ * schema. The generator reads that as "no response type" and renders the
153
+ * method as `Promise<void>`: it awaits the `…Raw` sibling and returns nothing,
154
+ * never calling `value()` at all. `GET /skill/v1/skills/{skill_id}` is one -
155
+ * the gateway sends the whole skill and the caller receives `undefined`, which
156
+ * makes a skill impossible to read back.
157
+ *
158
+ * Hand-fixing the contract would not hold, since `contracts/` is synced from
159
+ * the gateway. So the generated method is still the one called, with its own
160
+ * parameter defaults intact, over a receiver that keeps hold of the
161
+ * `ApiResponse` it discards. Only when the call resolves to `undefined` is
162
+ * that response read, and its body is still unconsumed precisely because the
163
+ * generated method ignored it. One request either way, and nothing changes for
164
+ * an operation whose body the generator already keeps.
165
+ */
166
+ async function callKeepingBody(target, method, property, args) {
167
+ const rawName = `${property}Raw`;
168
+ if (typeof target[rawName] !== "function") {
169
+ return await Reflect.apply(method, target, args);
170
+ }
171
+ let captured;
172
+ const receiver = new Proxy(target, {
173
+ get(proxied, name, self) {
174
+ const member = Reflect.get(proxied, name, self);
175
+ if (name !== rawName || typeof member !== "function")
176
+ return member;
177
+ return async (...rawArgs) => {
178
+ captured = (await Reflect.apply(member, proxied, rawArgs));
179
+ return captured;
180
+ };
181
+ },
182
+ });
183
+ const result = await Reflect.apply(method, receiver, args);
184
+ if (result !== undefined || captured === undefined)
185
+ return result;
186
+ return await recoverValue(captured);
187
+ }
188
+ async function recoverValue(response) {
189
+ let value;
190
+ try {
191
+ value = await response.value?.();
192
+ }
193
+ catch {
194
+ // The generated method already read the body; there is nothing to recover.
195
+ return undefined;
196
+ }
197
+ if (value !== undefined || !response.raw)
198
+ return value;
199
+ const contentType = response.raw.headers.get("content-type") ?? "";
200
+ if (!contentType.toLowerCase().includes("json"))
201
+ return value;
202
+ try {
203
+ return await response.raw.json();
204
+ }
205
+ catch {
206
+ // An empty or unparseable body is the `void` the generator promised.
207
+ return value;
208
+ }
209
+ }
210
+ function wrapGeneratedAPI(api, operationMethods, apiKeyCredential) {
142
211
  return new Proxy(api, {
143
212
  get(target, property, receiver) {
144
213
  const value = Reflect.get(target, property, receiver);
@@ -147,22 +216,58 @@ function wrapGeneratedAPI(api, operationMethods) {
147
216
  }
148
217
  return async (...args) => {
149
218
  try {
150
- return await Reflect.apply(value, target, args);
219
+ return await callKeepingBody(target, value, property, args);
151
220
  }
152
221
  catch (error) {
153
222
  const response = error?.response;
154
223
  if (response instanceof Response) {
155
- throw await errors_1.AtlanAPIError.fromResponse(response);
224
+ throw annotateCredential(await errors_1.AtlanAPIError.fromResponse(response), apiKeyCredential);
156
225
  }
157
226
  const cause = error?.cause;
158
227
  if (cause instanceof errors_1.AtlanAPIError)
159
- throw cause;
228
+ throw annotateCredential(cause, apiKeyCredential);
160
229
  throw error;
161
230
  }
162
231
  };
163
232
  },
164
233
  });
165
234
  }
235
+ /**
236
+ * Say which kind of credential a 403 was refused for.
237
+ *
238
+ * A bare "403 forbidden" sends people hunting for a missing role when the
239
+ * credential kind is the whole story: the same POST an API key is refused is
240
+ * accepted from a user token.
241
+ */
242
+ function annotateCredential(error, apiKeyCredential) {
243
+ if (error.status !== 403 || !apiKeyCredential)
244
+ return error;
245
+ return error.withHint("the configured credential is an API key, not a user token; the gateway grants " +
246
+ "an API key a narrower set of permissions - creating an agent or a skill, for " +
247
+ "one, is refused for an API key and accepted for a user token from " +
248
+ "`atlanai auth login` (read it with `atlanai auth token`)");
249
+ }
250
+ /**
251
+ * Whether a credential is a signed JWT, which is the user-token shape.
252
+ *
253
+ * Only the structure is read, and the value is never logged or compared to a
254
+ * literal: three base64url segments whose first decodes to a JSON object. The
255
+ * gateway draws the same line itself - its 401 vocabulary splits
256
+ * `invalid_api_key`/`unknown_key` from the whole `token_*` family.
257
+ */
258
+ function looksLikeAJwt(token) {
259
+ const segments = token.split(".");
260
+ if (segments.length !== 3 || segments.some((segment) => segment === ""))
261
+ return false;
262
+ try {
263
+ const header = segments[0].replace(/-/g, "+").replace(/_/g, "/");
264
+ const decoded = JSON.parse(atob(header));
265
+ return typeof decoded === "object" && decoded !== null && !Array.isArray(decoded);
266
+ }
267
+ catch {
268
+ return false;
269
+ }
270
+ }
166
271
  class AtlanClient {
167
272
  /** Per-service generated clients, for an operation the map has not surfaced. */
168
273
  raw;
@@ -181,6 +286,10 @@ class AtlanClient {
181
286
  const fetchApi = withTimeout(options.fetch ?? globalThis.fetch, timeoutMs);
182
287
  const accessToken = safeTokenProvider(options.bearerToken, options.tokenProvider);
183
288
  const workspace = validateWorkspace(options.workspace);
289
+ // Read once, from the shape alone. A tokenProvider is left unclassified
290
+ // rather than called here: resolving it early could refresh a credential
291
+ // nobody asked for yet.
292
+ const apiKeyCredential = options.bearerToken !== undefined && !looksLikeAJwt(options.bearerToken);
184
293
  const modules = {
185
294
  registry: registryRaw,
186
295
  skill: skillRaw,
@@ -202,7 +311,7 @@ class AtlanClient {
202
311
  accessToken,
203
312
  ...(workspace === undefined ? {} : { headers: { [exports.WORKSPACE_HEADER]: workspace } }),
204
313
  });
205
- namespaces[service.name] = createService(service.name, module, configuration, service.operations);
314
+ namespaces[service.name] = createService(service.name, module, configuration, service.operations, apiKeyCredential);
206
315
  }
207
316
  this.raw = Object.freeze(namespaces);
208
317
  // Resources, not services. A service is a base path; a resource is what a
package/dist/errors.d.ts CHANGED
@@ -4,13 +4,22 @@ export declare class AtlanAPIError extends Error {
4
4
  readonly title: string;
5
5
  readonly detail: string;
6
6
  readonly traceId?: string;
7
+ /**
8
+ * Set by the SDK when it, not the gateway, knows why the call failed - a
9
+ * 403 refused for the kind of credential configured, for instance. Never
10
+ * server-supplied, so unlike `detail` it is safe to put in the message.
11
+ */
12
+ readonly hint: string;
7
13
  constructor(options: {
8
14
  status: number;
9
15
  code?: string;
10
16
  title?: string;
11
17
  detail?: string;
12
18
  traceId?: string;
19
+ hint?: string;
13
20
  });
21
+ /** The same failure, carrying a client-side explanation in its message. */
22
+ withHint(hint: string): AtlanAPIError;
14
23
  static fromResponse(response: Response): Promise<AtlanAPIError>;
15
24
  static fromConnectionError(timedOut?: boolean): AtlanAPIError;
16
25
  }
package/dist/errors.js CHANGED
@@ -7,11 +7,21 @@ class AtlanAPIError extends Error {
7
7
  title;
8
8
  detail;
9
9
  traceId;
10
+ /**
11
+ * Set by the SDK when it, not the gateway, knows why the call failed - a
12
+ * 403 refused for the kind of credential configured, for instance. Never
13
+ * server-supplied, so unlike `detail` it is safe to put in the message.
14
+ */
15
+ hint;
10
16
  constructor(options) {
11
17
  const suffix = options.code ? ` (${options.code})` : "";
18
+ const hint = options.hint ? `; ${options.hint}` : "";
19
+ // `detail` stays out: it is server text and may echo what was sent.
20
+ // `traceId` is an opaque correlation id and is what support asks for.
21
+ const trace = options.traceId ? ` [trace_id=${options.traceId}]` : "";
12
22
  const message = options.status === 0
13
23
  ? `${options.title ?? "Could not reach the Atlan Gateway"}${options.detail ? `; ${options.detail}` : ""}`
14
- : `Atlan API request failed with status ${options.status}${suffix}`;
24
+ : `Atlan API request failed with status ${options.status}${suffix}${trace}${hint}`;
15
25
  super(message);
16
26
  this.name = "AtlanAPIError";
17
27
  this.status = options.status;
@@ -19,6 +29,18 @@ class AtlanAPIError extends Error {
19
29
  this.title = options.title ?? "";
20
30
  this.detail = options.detail ?? "";
21
31
  this.traceId = options.traceId;
32
+ this.hint = options.hint ?? "";
33
+ }
34
+ /** The same failure, carrying a client-side explanation in its message. */
35
+ withHint(hint) {
36
+ return new AtlanAPIError({
37
+ status: this.status,
38
+ code: this.code,
39
+ title: this.title,
40
+ detail: this.detail,
41
+ traceId: this.traceId,
42
+ hint,
43
+ });
22
44
  }
23
45
  static async fromResponse(response) {
24
46
  let problem = {};
package/dist/evals.d.ts CHANGED
@@ -28,6 +28,47 @@ export interface ContextManifest {
28
28
  export declare function createContextManifest(input: readonly ContextItem[]): Promise<ContextManifest>;
29
29
  /** Resolve one dataset by artifact ID or exact name, never fuzzy matching. */
30
30
  export declare function resolveDataset(client: AtlanClient, idOrExactName: string, workspaceId?: string): Promise<unknown>;
31
+ /**
32
+ * The Registry handle for the case called `key` inside `datasetId`.
33
+ *
34
+ * Deterministic, so pushing the same suite twice lands on the same row, and
35
+ * dataset-scoped, so the same suite can also be pushed into a new dataset.
36
+ */
37
+ export declare function recordName(datasetId: string, key: string): Promise<string>;
38
+ /** What one case in a pushed suite ended up as. */
39
+ export interface RecordPush {
40
+ readonly key: string;
41
+ readonly name: string;
42
+ readonly id: string;
43
+ readonly action: "created" | "updated" | "unchanged";
44
+ readonly record: unknown;
45
+ }
46
+ /** A pushed suite: the dataset it landed in and what each case did. */
47
+ export interface DatasetPush {
48
+ readonly dataset: unknown;
49
+ readonly id: string;
50
+ readonly created: boolean;
51
+ readonly records: readonly RecordPush[];
52
+ }
53
+ /**
54
+ * Create or update each case, so the same suite can be pushed repeatedly.
55
+ *
56
+ * Each record's `name` is the developer's key for the case: it becomes the
57
+ * row's `displayName` and keys the Registry handle. A case already in the
58
+ * dataset is patched when its content moved and left alone when it did not.
59
+ */
60
+ export declare function pushRecords(client: AtlanClient, datasetId: string, records: Iterable<Record<string, unknown>>, workspaceId?: string): Promise<RecordPush[]>;
61
+ /**
62
+ * Push a suite under `name`, creating the dataset the first time only.
63
+ *
64
+ * Idempotent: run it again after correcting an expected value and the
65
+ * correction lands on the same rows, in the same dataset, without a 409.
66
+ */
67
+ export declare function pushDataset(client: AtlanClient, name: string, records: Iterable<Record<string, unknown>>, options?: {
68
+ workspaceId?: string;
69
+ displayName?: string;
70
+ description?: string;
71
+ }): Promise<DatasetPush>;
31
72
  export interface StartExperimentOptions {
32
73
  contextManifest?: ContextManifest;
33
74
  }
@@ -35,17 +76,59 @@ export declare class EvalRun {
35
76
  readonly experiment: unknown;
36
77
  readonly dataset: unknown;
37
78
  readonly contextManifest?: ContextManifest | undefined;
79
+ /**
80
+ * The session id stamped on every span this run emits.
81
+ *
82
+ * It is the only join key the gateway promotes off an eval span that can
83
+ * reach the run's subject: `recordExperimentSession` registers this value
84
+ * against the experiment's subject.
85
+ */
38
86
  constructor(experiment: unknown, dataset: unknown, contextManifest?: ContextManifest | undefined);
39
87
  get id(): string;
40
88
  get experimentId(): string;
41
- /** Options for `propagateAttributes` from `@atlanai/sdk/tracing`. */
89
+ /** Stable across retries because the experiment is the resumable unit. */
90
+ get sessionId(): string;
91
+ /**
92
+ * Options for `propagateAttributes` from `@atlanai/sdk/tracing`.
93
+ *
94
+ * Carries the experiment join and the subject join. A nested
95
+ * `propagateAttributes({ sessionId })` still wins for the spans inside it.
96
+ */
42
97
  get traceOptions(): {
43
98
  experimentId: string;
99
+ sessionId: string;
44
100
  metadata?: Record<string, unknown>;
45
101
  };
46
102
  }
47
103
  /** Create a running experiment over a dataset ID or exact dataset name. */
48
104
  export declare function startExperiment(client: AtlanClient, dataset: string, body: Readonly<Record<string, unknown>>, options?: StartExperimentOptions): Promise<EvalRun>;
105
+ export interface ExperimentTracesOptions {
106
+ limit?: number;
107
+ }
108
+ /**
109
+ * Every trace an experiment recorded, newest first.
110
+ *
111
+ * **The experiment is the scope eval traces are filed under.** A subject's own
112
+ * Traces tab (`GET /agent/v1/agents/{id}/traces`) resolves on the trace's
113
+ * *creator identity*, so it lists a run only when the run's spans were exported
114
+ * with that agent's own credential. An eval exported with your user key or a
115
+ * service-account key is filed under that identity instead, and the agent's
116
+ * Traces tab reads empty even though every trace exists. That is not the traces
117
+ * going missing; it is a different scope. Read them here, and call
118
+ * `recordExperimentSession` so the run is also reachable from the subject's
119
+ * Sessions tab.
120
+ *
121
+ * To fill the agent's own Traces tab, export the spans as the agent: pass
122
+ * `logger: initLogger({ apiKey: <the agent's credential> })` while the
123
+ * management `client` keeps your own, since the agent's identity is not
124
+ * entitled to create experiments.
125
+ */
126
+ export declare function experimentTraces(client: AtlanClient, experimentId: string, options?: ExperimentTracesOptions): Promise<unknown[]>;
127
+ export interface RecordExperimentSessionOptions {
128
+ sessionStatus?: string;
129
+ title?: string;
130
+ }
131
+ export declare function recordExperimentSession(client: AtlanClient, run: EvalRun, options?: RecordExperimentSessionOptions): Promise<unknown | undefined>;
49
132
  export interface EvalCase<Input, Expected = unknown, Metadata = Record<string, unknown>> {
50
133
  input: Input;
51
134
  expected?: Expected;
@@ -147,6 +230,13 @@ export interface EvalResultWithSummary<Input, Output, Expected, Metadata> {
147
230
  dataset: unknown;
148
231
  summary: Record<string, unknown>;
149
232
  results: EvalCaseResult<Input, Output, Expected, Metadata>[];
233
+ /** The session id stamped on every span of the run. */
234
+ sessionId: string;
235
+ /**
236
+ * The `session` record that binds the run to the experiment's subject.
237
+ * `undefined` when the experiment named no subject to bind it to.
238
+ */
239
+ session?: unknown;
150
240
  }
151
241
  /** Structural trace surface keeps the management bundle decoupled at build time. */
152
242
  export interface EvalTraceSpan {
@@ -169,13 +259,46 @@ export interface EvalTraceLogger {
169
259
  readonly client: EvalTraceClient;
170
260
  flush(): Promise<void>;
171
261
  }
262
+ export declare function Eval<Input, Output, Expected = unknown, Metadata extends Record<string, unknown> = Record<string, unknown>>(name: string, evaluator: Evaluator<Input, Output, Expected, Metadata>, options?: EvalOptions): Promise<EvalResultWithSummary<Input, Output, Expected, Metadata>>;
263
+ /** One gate result. `ok` is undefined when only a human can answer it. */
264
+ export interface VerificationCheck {
265
+ readonly gate: number;
266
+ readonly name: string;
267
+ readonly ok: boolean | undefined;
268
+ readonly detail: string;
269
+ }
270
+ /** The structured report `verifyExperiment` returns. */
271
+ export interface ExperimentVerification {
272
+ readonly experimentId: string;
273
+ readonly experiment: unknown;
274
+ readonly checks: readonly VerificationCheck[];
275
+ readonly results: readonly unknown[];
276
+ readonly traceIds: readonly string[];
277
+ readonly verifiedTraceIds: readonly string[];
278
+ /** Checks that failed. */
279
+ readonly failures: readonly VerificationCheck[];
280
+ /** Checks a human has to decide. They do not make `ok` false. */
281
+ readonly manual: readonly VerificationCheck[];
282
+ readonly ok: boolean;
283
+ /** Turn a failed gate into an exception, for use in CI. */
284
+ raiseForStatus(): void;
285
+ toString(): string;
286
+ }
287
+ export interface VerifyExperimentOptions {
288
+ /** Fail the gate unless exactly this many result rows exist. */
289
+ readonly expectedCaseCount?: number;
290
+ /** How many case traces to read spans for. `null` reads every one. */
291
+ readonly maxTraces?: number | null;
292
+ }
172
293
  /**
173
- * Run a complete Registry evaluation from a compact Braintrust-shaped definition.
294
+ * Run the documented seven-point evidence gate over a finished experiment.
295
+ *
296
+ * Reads the experiment, its result rows, its trace list and the spans of up to
297
+ * `maxTraces` case traces, and reports whether the evidence chain actually
298
+ * holds. It only reads; nothing is written.
174
299
  *
175
- * Every case is a root OTel trace. Task/provider spans and scorer spans nest
176
- * beneath it, verdicts cite immutable Registry scorer versions, result rows
177
- * carry the root trace ID, traces flush before summary, and the experiment is
178
- * explicitly finalized.
300
+ * Gate 7, the content and masking policy, cannot be decided from the data and
301
+ * is reported as a manual check rather than silently passed.
179
302
  */
180
- export declare function Eval<Input, Output, Expected = unknown, Metadata extends Record<string, unknown> = Record<string, unknown>>(name: string, evaluator: Evaluator<Input, Output, Expected, Metadata>, options?: EvalOptions): Promise<EvalResultWithSummary<Input, Output, Expected, Metadata>>;
303
+ export declare function verifyExperiment(client: unknown, experimentId: string, options?: VerifyExperimentOptions): Promise<ExperimentVerification>;
181
304
  export {};