@lanefoundry/gatelane-sdk 0.0.0-stage → 0.1.0

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 (47) hide show
  1. package/dist/candidate.d.ts +26 -0
  2. package/dist/candidate.d.ts.map +1 -0
  3. package/dist/candidate.js +41 -0
  4. package/dist/candidate.js.map +1 -0
  5. package/dist/capture.d.ts +105 -0
  6. package/dist/capture.d.ts.map +1 -0
  7. package/dist/capture.js +78 -0
  8. package/dist/capture.js.map +1 -0
  9. package/dist/dataset.d.ts +56 -0
  10. package/dist/dataset.d.ts.map +1 -0
  11. package/dist/dataset.js +35 -0
  12. package/dist/dataset.js.map +1 -0
  13. package/dist/gate.d.ts +81 -0
  14. package/dist/gate.d.ts.map +1 -0
  15. package/dist/gate.js +82 -0
  16. package/dist/gate.js.map +1 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +16 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/promotion.d.ts +82 -0
  22. package/dist/promotion.d.ts.map +1 -0
  23. package/dist/promotion.js +14 -0
  24. package/dist/promotion.js.map +1 -0
  25. package/dist/storage-fs.d.ts +27 -0
  26. package/dist/storage-fs.d.ts.map +1 -0
  27. package/dist/storage-fs.js +68 -0
  28. package/dist/storage-fs.js.map +1 -0
  29. package/dist/storage-http.d.ts +43 -0
  30. package/dist/storage-http.d.ts.map +1 -0
  31. package/dist/storage-http.js +112 -0
  32. package/dist/storage-http.js.map +1 -0
  33. package/dist/storage.d.ts +76 -0
  34. package/dist/storage.d.ts.map +1 -0
  35. package/dist/storage.js +52 -0
  36. package/dist/storage.js.map +1 -0
  37. package/package.json +115 -4
  38. package/src/candidate.ts +76 -0
  39. package/src/capture.ts +153 -0
  40. package/src/dataset.ts +85 -0
  41. package/src/gate.ts +162 -0
  42. package/src/index.ts +16 -0
  43. package/src/promotion.ts +98 -0
  44. package/src/storage-fs.ts +76 -0
  45. package/src/storage-http.ts +143 -0
  46. package/src/storage.ts +97 -0
  47. package/README.md +0 -3
@@ -0,0 +1,143 @@
1
+ /**
2
+ * HTTP storage adapter — sends capture records to a gatelane Worker endpoint.
3
+ *
4
+ * Production: `https://<your-worker>.workers.dev/v1/capture`
5
+ * Local dev: `http://localhost:8787/v1/capture`
6
+ *
7
+ * The Worker writes the record body to R2 (raw blob) and the metadata to D1.
8
+ * This adapter only handles transport; the storage / persistence happens server-side.
9
+ *
10
+ * Retries: 3 attempts with exponential backoff on 5xx / network errors.
11
+ *
12
+ * @see docs/prd.md §6.1 — Cloudflare-first deployment
13
+ */
14
+
15
+ import type { CaptureRecord, StorageAdapter, WriteResult } from './storage.js';
16
+
17
+ export type HttpStorageOptions = {
18
+ /** Base URL of the gatelane Worker, no trailing slash. */
19
+ readonly endpoint: string;
20
+ /** Bearer token for the `/v1/capture` route. */
21
+ readonly token: string;
22
+ /** Max retry attempts on transient failures. Defaults to 3. */
23
+ readonly maxRetries?: number;
24
+ /** Initial backoff in ms. Doubles each retry. Defaults to 200. */
25
+ readonly initialBackoffMs?: number;
26
+ /** Override fetch for testing. Defaults to global fetch. */
27
+ readonly fetchImpl?: typeof fetch;
28
+ };
29
+
30
+ export class HttpStorage implements StorageAdapter {
31
+ readonly name: string;
32
+ private readonly endpoint: string;
33
+ private readonly token: string;
34
+ private readonly maxRetries: number;
35
+ private readonly initialBackoffMs: number;
36
+ private readonly fetchImpl: typeof fetch;
37
+
38
+ constructor(opts: HttpStorageOptions) {
39
+ this.endpoint = opts.endpoint.replace(/\/$/, '');
40
+ this.token = opts.token;
41
+ this.maxRetries = opts.maxRetries ?? 3;
42
+ this.initialBackoffMs = opts.initialBackoffMs ?? 200;
43
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
44
+ this.name = `http(${new URL(this.endpoint).host})`;
45
+ }
46
+
47
+ async write(record: CaptureRecord): Promise<WriteResult> {
48
+ const url = `${this.endpoint}/v1/capture`;
49
+ let lastErr: unknown = null;
50
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
51
+ try {
52
+ const response = await this.fetchImpl(url, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'content-type': 'application/json',
56
+ authorization: `Bearer ${this.token}`,
57
+ 'x-gatelane-span-kind': record.span_kind ?? 'gate.replay',
58
+ },
59
+ body: JSON.stringify(record),
60
+ });
61
+ if (response.ok) {
62
+ const body = (await response.json()) as { id: string; stored_at: string };
63
+ return { id: body.id, stored_at: body.stored_at, backend: this.name };
64
+ }
65
+ // 4xx is fatal — don't retry. 5xx is transient — retry.
66
+ if (response.status >= 400 && response.status < 500) {
67
+ throw new Error(`capture write rejected: ${response.status} ${response.statusText}`);
68
+ }
69
+ lastErr = new Error(`capture write failed: ${response.status} ${response.statusText}`);
70
+ } catch (err) {
71
+ // Re-throw 4xx immediately so the caller sees the rejection; only network / 5xx retries.
72
+ if (err instanceof Error && err.message.startsWith('capture write rejected')) {
73
+ throw err;
74
+ }
75
+ lastErr = err;
76
+ }
77
+ // Backoff before next attempt (5xx / network path).
78
+ if (attempt < this.maxRetries - 1) {
79
+ const delay = this.initialBackoffMs * Math.pow(2, attempt);
80
+ const { promise, resolve } = Promise.withResolvers<void>();
81
+ setTimeout(resolve, delay);
82
+ await promise;
83
+ }
84
+ }
85
+ throw new Error(`capture write failed after ${this.maxRetries} attempts: ${String(lastErr)}`);
86
+ }
87
+
88
+ async read(_id: string): Promise<CaptureRecord | null> {
89
+ // Worker exposes GET /v1/capture/:id in v0.2. Stub: return null.
90
+ // Returning null means "not in local cache; ask the Worker". Tests don't depend on read.
91
+ return null;
92
+ }
93
+
94
+ async list(filter: { source_kind?: string; since?: string; limit?: number } = {}): Promise<ReadonlyArray<CaptureRecord>> {
95
+ const params = new URLSearchParams();
96
+ if (filter.since !== undefined) params.set('since', filter.since);
97
+ if (filter.limit !== undefined) params.set('limit', String(filter.limit));
98
+ const url = `${this.endpoint}/v1/captures${params.size > 0 ? '?' + params.toString() : ''}`;
99
+ const response = await this.fetchImpl(url, {
100
+ headers: { authorization: `Bearer ${this.token}` },
101
+ });
102
+ if (!response.ok) {
103
+ throw new Error(`capture list failed: ${response.status} ${response.statusText}`);
104
+ }
105
+ const body = (await response.json()) as { captures: WireCaptureRecord[] };
106
+ let out = body.captures.map(wireToLocal);
107
+ if (filter.source_kind !== undefined) {
108
+ out = out.filter((r) => r.input.metadata?.['source_kind'] === filter.source_kind);
109
+ }
110
+ return out;
111
+ }
112
+ }
113
+
114
+ /** Wire shape returned by GET /v1/captures (matches packages/shared/src/types.ts CaptureRecord). */
115
+ type WireCaptureRecord = {
116
+ readonly id: string;
117
+ readonly traceId: string;
118
+ readonly prompt: ReadonlyArray<{ role: string; content: string }>;
119
+ readonly response: unknown;
120
+ readonly model: string;
121
+ readonly provider: string;
122
+ readonly costCents: number;
123
+ readonly latencyMs: number;
124
+ readonly metadata: Record<string, unknown>;
125
+ readonly createdAt: string;
126
+ };
127
+
128
+ /** Convert a wire capture row back into the SDK's CapturedCall shape. */
129
+ function wireToLocal(row: WireCaptureRecord): CaptureRecord {
130
+ return {
131
+ id: row.id,
132
+ input: {
133
+ prompt: row.prompt,
134
+ model: row.model,
135
+ metadata: row.metadata,
136
+ },
137
+ output: row.response,
138
+ started_at: row.createdAt,
139
+ completed_at: row.createdAt,
140
+ cost_usd: row.costCents / 100,
141
+ latency_ms: row.latencyMs,
142
+ };
143
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Storage adapter — pluggable back-end for capture records.
3
+ *
4
+ * gatelane's capture SDK is host-agnostic: the same SDK can write to in-memory
5
+ * (tests), filesystem (local dev), or HTTP (production, talking to a Cloudflare
6
+ * Worker that writes to R2/D1).
7
+ *
8
+ * @see docs/prd.md §5.4 — Shared engine
9
+ */
10
+
11
+ export type D1DatabaseLike = {
12
+ prepare(query: string): D1PreparedStatementLike;
13
+ };
14
+
15
+ export type D1PreparedStatementLike = {
16
+ bind(...values: unknown[]): D1PreparedStatementLike;
17
+ first<T = unknown>(column?: string): Promise<T | null>;
18
+ run(): Promise<{ success: boolean }>;
19
+ all<T = unknown>(): Promise<{ results: T[] }>;
20
+ };
21
+
22
+ export type R2BucketLike = {
23
+ put(key: string, value: ReadableStream | ArrayBuffer | string, options?: { httpMetadata?: { contentType?: string } }): Promise<unknown>;
24
+ get(key: string): Promise<{ body: ReadableStream; httpMetadata?: { contentType?: string } } | null>;
25
+ delete(key: string): Promise<void>;
26
+ };
27
+
28
+ import type { CapturedCall } from './capture.js';
29
+ export type CaptureRecord = CapturedCall;
30
+
31
+ /** Result of a successful write. */
32
+ export type WriteResult = {
33
+ readonly id: string;
34
+ readonly stored_at: string;
35
+ readonly backend: string;
36
+ };
37
+
38
+ /** Storage back-end interface. All implementations must be safe to call concurrently. */
39
+ export interface StorageAdapter {
40
+ /** Persist a capture record. Returns the canonical id + storage timestamp. */
41
+ write(record: CaptureRecord): Promise<WriteResult>;
42
+ /** Read back a record by id. Returns null if not found. */
43
+ read(id: string): Promise<CaptureRecord | null>;
44
+ /** List records matching a filter. Used by the freeze-slice tool. */
45
+ list(filter: { source_kind?: string; since?: string; limit?: number }): Promise<ReadonlyArray<CaptureRecord>>;
46
+ /** Identity of the backend (for debugging + audit). */
47
+ readonly name: string;
48
+ }
49
+
50
+ /** In-memory adapter. Default for tests; never persisted across restarts. */
51
+ export class InMemoryStorage implements StorageAdapter {
52
+ readonly name = 'memory';
53
+ private readonly records = new Map<string, CaptureRecord>();
54
+
55
+ async write(record: CaptureRecord): Promise<WriteResult> {
56
+ this.records.set(record.id, record);
57
+ return {
58
+ id: record.id,
59
+ stored_at: new Date().toISOString(),
60
+ backend: this.name,
61
+ };
62
+ }
63
+
64
+ async read(id: string): Promise<CaptureRecord | null> {
65
+ return this.records.get(id) ?? null;
66
+ }
67
+
68
+ async list(filter: { source_kind?: string; since?: string; limit?: number }): Promise<ReadonlyArray<CaptureRecord>> {
69
+ let out = Array.from(this.records.values());
70
+ if (filter.source_kind !== undefined) {
71
+ out = out.filter((r) => r.input.metadata?.['source_kind'] === filter.source_kind);
72
+ }
73
+ if (filter.since !== undefined) {
74
+ const since = Date.parse(filter.since);
75
+ out = out.filter((r) => Date.parse(r.started_at) >= since);
76
+ }
77
+ if (filter.limit !== undefined) {
78
+ out = out.slice(0, filter.limit);
79
+ }
80
+ return out;
81
+ }
82
+ }
83
+
84
+ /** Active storage. Module-level mutable singleton — initialized once. */
85
+ let activeStorage: StorageAdapter = new InMemoryStorage();
86
+
87
+ /** Replace the active storage backend. Returns the previous backend for restoration in tests. */
88
+ export function setStorage(adapter: StorageAdapter): StorageAdapter {
89
+ const previous = activeStorage;
90
+ activeStorage = adapter;
91
+ return previous;
92
+ }
93
+
94
+ /** Get the active storage backend. */
95
+ export function getStorage(): StorageAdapter {
96
+ return activeStorage;
97
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.