deepline 0.1.91 → 0.1.94

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.
@@ -36,6 +36,15 @@
36
36
  import { resolveConfig } from './config.js';
37
37
  import { DeeplineError } from './errors.js';
38
38
  import { HttpClient } from './http.js';
39
+ import {
40
+ STREAM_HEALTHY_CONNECTION_MS,
41
+ isTransientPlayStreamError,
42
+ streamReconnectDelayMs,
43
+ } from './stream-reconnect.js';
44
+ import {
45
+ observeRunEvents,
46
+ RunObserveTransportUnavailableError,
47
+ } from './runs/observe-transport.js';
39
48
  import type {
40
49
  DeeplineClientOptions,
41
50
  ResolvedConfig,
@@ -129,11 +138,29 @@ export type RunsListOptions = {
129
138
  /** Streaming options for `client.runs.tail(...)`. */
130
139
  export type RunsTailOptions = {
131
140
  signal?: AbortSignal;
141
+ /**
142
+ * Called before each stream reconnect. Server stream windows are finite, so
143
+ * long runs reconnect with backoff until a terminal status is observed.
144
+ */
145
+ onReconnect?: (info: {
146
+ attempt: number;
147
+ delayMs: number;
148
+ reason: string;
149
+ }) => void;
150
+ /**
151
+ * Display-only transport notices: subscription-transport reconnects,
152
+ * staleness warnings, and the one-time fallback notice when the server
153
+ * cannot serve the Convex subscription transport (ADR-0008).
154
+ */
155
+ onNotice?: (message: string) => void;
132
156
  };
133
157
 
134
158
  /** Log fetch options for `client.runs.logs(...)`. */
135
159
  export type RunsLogsOptions = {
160
+ /** Return the LAST `limit` stored log lines (default 200). */
136
161
  limit?: number;
162
+ /** Fetch every stored log line, paginating to the full totalCount. */
163
+ all?: boolean;
137
164
  };
138
165
 
139
166
  /** Persisted log response for one play run. */
@@ -146,6 +173,28 @@ export type RunsLogsResult = {
146
173
  truncated: boolean;
147
174
  hasMore: boolean;
148
175
  entries: string[];
176
+ /**
177
+ * True when the run crossed the Run Log Stream retention cap: `totalCount`
178
+ * keeps counting, but stored line bodies end at a loud truncation marker.
179
+ */
180
+ logsTruncated?: boolean;
181
+ };
182
+
183
+ /** Server page cap for GET /api/v2/runs/:runId/logs (ADR-0009). */
184
+ const RUN_LOGS_PAGE_LIMIT = 1_000;
185
+
186
+ /** Wire shape of one GET /api/v2/runs/:runId/logs page. */
187
+ type RunLogsPageResponse = {
188
+ runId: string;
189
+ totalLogCount: number;
190
+ logsTruncated: boolean;
191
+ lastStoredSeq: number;
192
+ afterSeq: number;
193
+ entries: Array<{ seq: number; line: string }>;
194
+ firstSeq: number | null;
195
+ lastSeq: number | null;
196
+ hasMore: boolean;
197
+ nextAfterSeq: number | null;
149
198
  };
150
199
 
151
200
  /** One persisted runtime-sheet row returned by `client.runs.exportDatasetRows(...)`. */
@@ -328,6 +377,13 @@ type PlayLiveStatusState = {
328
377
  runId: string;
329
378
  status: PlayStatus['status'];
330
379
  logs: string[];
380
+ /**
381
+ * Absolute (1-based) sequence number of the last log line appended to
382
+ * `logs`. play.run.log payloads carry `firstSeq` (ADR-0009), so overlapping
383
+ * re-deliveries are skipped positionally — repeated identical lines are
384
+ * preserved and snapshots never replace the accumulated log list.
385
+ */
386
+ lastLogSeq: number;
331
387
  result?: unknown;
332
388
  error?: string;
333
389
  latest: PlayStatus | null;
@@ -355,13 +411,52 @@ function normalizeLiveStatus(value: unknown): PlayStatus['status'] | null {
355
411
  return null;
356
412
  }
357
413
 
414
+ function appendPlayLiveLogLines(
415
+ state: PlayLiveStatusState,
416
+ payload: Record<string, unknown>,
417
+ ): void {
418
+ const lines = readStringArray(payload.lines);
419
+ if (lines.length === 0) {
420
+ return;
421
+ }
422
+ const firstSeq =
423
+ typeof payload.firstSeq === 'number' &&
424
+ Number.isFinite(payload.firstSeq) &&
425
+ payload.firstSeq >= 1
426
+ ? Math.trunc(payload.firstSeq)
427
+ : null;
428
+ if (firstSeq === null) {
429
+ // Marker payloads (gap/unavailable notices) and pre-ADR-0009 servers
430
+ // carry no seq: append verbatim and advance the cursor by the payload's
431
+ // cumulative count when present so later seq-stamped lines line up.
432
+ state.logs.push(...lines);
433
+ const totalLogCount =
434
+ typeof payload.totalLogCount === 'number' &&
435
+ Number.isFinite(payload.totalLogCount)
436
+ ? Math.trunc(payload.totalLogCount)
437
+ : null;
438
+ if (totalLogCount !== null) {
439
+ state.lastLogSeq = Math.max(state.lastLogSeq, totalLogCount);
440
+ }
441
+ return;
442
+ }
443
+ // Positional append: skip the already-seen prefix of overlapping
444
+ // re-deliveries; repeated identical lines are preserved.
445
+ const skip = Math.max(0, state.lastLogSeq + 1 - firstSeq);
446
+ if (skip >= lines.length) {
447
+ return;
448
+ }
449
+ state.logs.push(...lines.slice(skip));
450
+ state.lastLogSeq = Math.max(state.lastLogSeq, firstSeq + lines.length - 1);
451
+ }
452
+
358
453
  function updatePlayLiveStatusState(
359
454
  state: PlayLiveStatusState,
360
455
  event: PlayLiveEvent,
361
456
  ): PlayStatus | null {
362
457
  const payload = getPlayLiveEventPayload(event);
363
458
  if (event.type === 'play.run.log') {
364
- state.logs.push(...readStringArray(payload.lines));
459
+ appendPlayLiveLogLines(state, payload);
365
460
  return null;
366
461
  }
367
462
  if (
@@ -385,15 +480,23 @@ function updatePlayLiveStatusState(
385
480
  : null) ??
386
481
  state.status;
387
482
  const progressPayload = isRecord(payload.progress) ? payload.progress : {};
388
- const payloadLogs = readStringArray(payload.logs);
389
- const progressLogs = readStringArray(progressPayload.logs);
390
- const logs = payloadLogs.length > 0 ? payloadLogs : progressLogs;
483
+ // Snapshots no longer REPLACE accumulated logs (ADR-0009): the snapshot
484
+ // only retains a bounded tail, so replacing would clobber the seq-keyed
485
+ // log list built from play.run.log events (the stream differ always emits
486
+ // log lines through play.run.log, snapshot ticks included). A terminal
487
+ // final_status payload may still seed an EMPTY state — that is the only
488
+ // event some non-stream flows ever see.
391
489
  if (
392
- logs.length > 0 ||
393
- event.type === 'play.run.snapshot' ||
394
- (event.type === 'play.run.final_status' && !isPlayRunPackage(payload))
490
+ event.type === 'play.run.final_status' &&
491
+ state.logs.length === 0 &&
492
+ state.lastLogSeq === 0
395
493
  ) {
396
- state.logs = logs;
494
+ const payloadLogs = readStringArray(payload.logs);
495
+ const progressLogs = readStringArray(progressPayload.logs);
496
+ const seedLogs = payloadLogs.length > 0 ? payloadLogs : progressLogs;
497
+ if (seedLogs.length > 0) {
498
+ state.logs = seedLogs;
499
+ }
397
500
  }
398
501
  if ('result' in payload) {
399
502
  state.result = payload.result;
@@ -1138,6 +1241,7 @@ export class DeeplineClient {
1138
1241
  async compileEnrichPlan(input: {
1139
1242
  plan_args?: string[];
1140
1243
  config?: unknown;
1244
+ native_play_materialization?: 'macro' | 'inline_prebuilt';
1141
1245
  }): Promise<{ config: EnrichCompiledConfig }> {
1142
1246
  return this.http.post('/api/v2/enrich/compile', input);
1143
1247
  }
@@ -1510,6 +1614,128 @@ export class DeeplineClient {
1510
1614
  return response.runs ?? [];
1511
1615
  }
1512
1616
 
1617
+ // ---------------------------------------------------------------------------
1618
+ // Legacy workflows (double-shipped). Thin pass-throughs over the live cloud
1619
+ // `/api/v2/workflows/*` API so the SDK CLI keeps existing cloud workflows
1620
+ // working while users migrate them to plays via `workflows transform`. Kept
1621
+ // intentionally minimal — workflows are a deprecated surface.
1622
+ // ---------------------------------------------------------------------------
1623
+
1624
+ /** List the org's workflows. `GET /api/v2/workflows`. */
1625
+ async listWorkflows(options?: { limit?: number }): Promise<{
1626
+ workflows: Array<{
1627
+ id: string;
1628
+ name: string;
1629
+ status: string;
1630
+ current_published_version: number | null;
1631
+ }>;
1632
+ }> {
1633
+ const params = new URLSearchParams();
1634
+ if (typeof options?.limit === 'number') {
1635
+ params.set('limit', String(options.limit));
1636
+ }
1637
+ const query = params.size > 0 ? `?${params.toString()}` : '';
1638
+ return this.http.get(`/api/v2/workflows${query}`);
1639
+ }
1640
+
1641
+ /**
1642
+ * Fetch a single workflow (including its published-revision config — the
1643
+ * input to `compileWorkflowConfigToPlay`). `GET /api/v2/workflows/:id`.
1644
+ */
1645
+ async getWorkflow(id: string): Promise<{
1646
+ workflow: {
1647
+ id: string;
1648
+ name: string;
1649
+ status: string;
1650
+ current_published_version: number | null;
1651
+ current_published_revision: {
1652
+ version: number;
1653
+ config: unknown;
1654
+ } | null;
1655
+ } | null;
1656
+ validation?: unknown;
1657
+ }> {
1658
+ return this.http.get(`/api/v2/workflows/${encodeURIComponent(id)}`);
1659
+ }
1660
+
1661
+ /** Delete a workflow. `DELETE /api/v2/workflows/:id`. */
1662
+ async deleteWorkflow(id: string): Promise<unknown> {
1663
+ return this.http.delete(`/api/v2/workflows/${encodeURIComponent(id)}`);
1664
+ }
1665
+
1666
+ /** Turn a workflow off. `POST /api/v2/workflows/:id/disable`. */
1667
+ async disableWorkflow(id: string): Promise<unknown> {
1668
+ return this.http.post(
1669
+ `/api/v2/workflows/${encodeURIComponent(id)}/disable`,
1670
+ {},
1671
+ );
1672
+ }
1673
+
1674
+ /** Turn a workflow back on. `POST /api/v2/workflows/:id/enable`. */
1675
+ async enableWorkflow(id: string): Promise<unknown> {
1676
+ return this.http.post(
1677
+ `/api/v2/workflows/${encodeURIComponent(id)}/enable`,
1678
+ {},
1679
+ );
1680
+ }
1681
+
1682
+ /** Create/update a workflow from config. `POST /api/v2/workflows/apply`. */
1683
+ async applyWorkflow(body: Record<string, unknown>): Promise<unknown> {
1684
+ return this.http.post('/api/v2/workflows/apply', body);
1685
+ }
1686
+
1687
+ /** Validate a workflow config without saving. `POST /api/v2/workflows/lint`. */
1688
+ async lintWorkflow(body: Record<string, unknown>): Promise<unknown> {
1689
+ return this.http.post('/api/v2/workflows/lint', body);
1690
+ }
1691
+
1692
+ /** Fetch live workflow request schemas. `GET /api/v2/workflows/schema`. */
1693
+ async getWorkflowSchema(subject?: string): Promise<unknown> {
1694
+ const params = new URLSearchParams();
1695
+ if (subject) params.set('subject', subject);
1696
+ const query = params.size > 0 ? `?${params.toString()}` : '';
1697
+ return this.http.get(`/api/v2/workflows/schema${query}`);
1698
+ }
1699
+
1700
+ /** Queue a workflow run. `POST /api/v2/workflows/call`. */
1701
+ async callWorkflow(body: Record<string, unknown>): Promise<unknown> {
1702
+ return this.http.post('/api/v2/workflows/call', body);
1703
+ }
1704
+
1705
+ /** List a workflow's runs. `GET /api/v2/workflows/:id/runs`. */
1706
+ async listWorkflowRuns(
1707
+ id: string,
1708
+ options?: { limit?: number },
1709
+ ): Promise<unknown> {
1710
+ const params = new URLSearchParams();
1711
+ if (typeof options?.limit === 'number') {
1712
+ params.set('limit', String(options.limit));
1713
+ }
1714
+ const query = params.size > 0 ? `?${params.toString()}` : '';
1715
+ return this.http.get(
1716
+ `/api/v2/workflows/${encodeURIComponent(id)}/runs${query}`,
1717
+ );
1718
+ }
1719
+
1720
+ /** Fetch one workflow run. `GET /api/v2/workflows/:id/runs/:runId`. */
1721
+ async getWorkflowRun(id: string, runId: string): Promise<unknown> {
1722
+ return this.http.get(
1723
+ `/api/v2/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(
1724
+ runId,
1725
+ )}`,
1726
+ );
1727
+ }
1728
+
1729
+ /** Cancel a workflow run. `POST /api/v2/workflows/:id/runs/:runId/cancel`. */
1730
+ async cancelWorkflowRun(id: string, runId: string): Promise<unknown> {
1731
+ return this.http.post(
1732
+ `/api/v2/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(
1733
+ runId,
1734
+ )}/cancel`,
1735
+ {},
1736
+ );
1737
+ }
1738
+
1513
1739
  /**
1514
1740
  * Get a run by id using the public runs resource model.
1515
1741
  *
@@ -1560,44 +1786,161 @@ export class DeeplineClient {
1560
1786
  return response.runs ?? [];
1561
1787
  }
1562
1788
 
1563
- /** Read the canonical run stream and return the latest run snapshot. */
1564
- async tailRun(runId: string, options?: RunsTailOptions): Promise<PlayStatus> {
1789
+ /**
1790
+ * Observe one run's live events through the Convex Run Snapshot
1791
+ * subscription transport (ADR-0008). Yields the same `play.*` event
1792
+ * envelopes as {@link streamPlayRunEvents} and ends after the terminal
1793
+ * snapshot. Throws {@link RunObserveTransportUnavailableError} when this
1794
+ * server cannot serve the transport (older server, unconfigured grants, or
1795
+ * unreachable Convex) — callers fall back to the SSE stream with a notice.
1796
+ */
1797
+ observeRunEvents(
1798
+ runId: string,
1799
+ options?: { signal?: AbortSignal; onNotice?: (message: string) => void },
1800
+ ): AsyncGenerator<PlayLiveEvent> {
1801
+ return observeRunEvents({
1802
+ http: this.http,
1803
+ runId,
1804
+ signal: options?.signal,
1805
+ onNotice: options?.onNotice,
1806
+ }) as AsyncGenerator<PlayLiveEvent>;
1807
+ }
1808
+
1809
+ /**
1810
+ * Tail one run through the subscription transport until terminal, then
1811
+ * return one durable REST status read (the final Run Response Package).
1812
+ */
1813
+ private async tailRunViaObserveTransport(
1814
+ runId: string,
1815
+ options?: RunsTailOptions,
1816
+ ): Promise<PlayStatus> {
1565
1817
  const state: PlayLiveStatusState = {
1566
1818
  runId,
1567
1819
  status: 'running',
1568
1820
  logs: [],
1821
+ lastLogSeq: 0,
1569
1822
  latest: null,
1570
1823
  };
1571
- let terminal = false;
1572
- for await (const event of this.streamPlayRunEvents(runId, {
1573
- mode: 'cli',
1824
+ for await (const event of this.observeRunEvents(runId, {
1574
1825
  signal: options?.signal,
1826
+ onNotice: options?.onNotice,
1575
1827
  })) {
1576
1828
  const status = updatePlayLiveStatusState(state, event);
1577
- if (!status) {
1829
+ if (!status || !TERMINAL_PLAY_STATUSES.has(status.status)) {
1578
1830
  continue;
1579
1831
  }
1580
- terminal = TERMINAL_PLAY_STATUSES.has(status.status);
1581
- if (terminal) {
1582
- break;
1583
- }
1584
- }
1585
- if (terminal && state.latest) {
1586
- return await this.getRunStatus(state.latest.runId || runId).catch(
1832
+ return await this.getRunStatus(status.runId || runId).catch(
1587
1833
  () => state.latest ?? playRunStatusFromState(state),
1588
1834
  );
1589
1835
  }
1590
- if (state.latest) {
1591
- return state.latest;
1836
+ if (options?.signal?.aborted) {
1837
+ throw new DeeplineError('Run observation aborted.', undefined, 'ABORTED');
1838
+ }
1839
+ // The transport ends only after a terminal snapshot; the differ always
1840
+ // emits a terminal `play.run.status` first, so reaching here means the
1841
+ // terminal package read raced — re-check durable status once, loudly.
1842
+ const refreshed = await this.getRunStatus(runId);
1843
+ if (TERMINAL_PLAY_STATUSES.has(refreshed.status)) {
1844
+ return refreshed;
1592
1845
  }
1593
1846
  throw new DeeplineError(
1594
- `Run stream for ${runId} ended before the initial snapshot.`,
1847
+ `Run observation for ${runId} ended before a terminal status.`,
1595
1848
  undefined,
1596
- 'PLAY_RUN_STREAM_EMPTY',
1597
- { runId },
1849
+ 'PLAY_LIVE_STREAM_ENDED',
1598
1850
  );
1599
1851
  }
1600
1852
 
1853
+ /**
1854
+ * Read the canonical run stream until a terminal run status is observed.
1855
+ *
1856
+ * Tries the Convex Run Snapshot subscription transport first (ADR-0008);
1857
+ * when the server cannot serve it (grant endpoint missing/unconfigured or
1858
+ * Convex unreachable) it falls back — with one `onNotice` message — to the
1859
+ * support-window SSE stream below.
1860
+ *
1861
+ * Server stream windows are finite: they end cleanly at the function
1862
+ * ceiling even while the run keeps executing. A window that ends (cleanly
1863
+ * or via transient network error) without a terminal event triggers one
1864
+ * durable-status re-check followed by a backed-off reconnect, so long runs
1865
+ * tail to completion. Abort via `options.signal` to stop waiting.
1866
+ */
1867
+ async tailRun(runId: string, options?: RunsTailOptions): Promise<PlayStatus> {
1868
+ try {
1869
+ return await this.tailRunViaObserveTransport(runId, options);
1870
+ } catch (error) {
1871
+ if (!(error instanceof RunObserveTransportUnavailableError)) {
1872
+ throw error;
1873
+ }
1874
+ options?.onNotice?.(
1875
+ `[observe] live subscription unavailable (${error.reason}); falling back to SSE tail (support window, ADR-0008)`,
1876
+ );
1877
+ }
1878
+ const state: PlayLiveStatusState = {
1879
+ runId,
1880
+ status: 'running',
1881
+ logs: [],
1882
+ lastLogSeq: 0,
1883
+ latest: null,
1884
+ };
1885
+ let reconnectAttempt = 0;
1886
+
1887
+ for (;;) {
1888
+ const connectedAt = Date.now();
1889
+ let sawEvent = false;
1890
+ let endedReason = 'stream window ended before a terminal event';
1891
+ try {
1892
+ for await (const event of this.streamPlayRunEvents(runId, {
1893
+ mode: 'cli',
1894
+ signal: options?.signal,
1895
+ })) {
1896
+ sawEvent = true;
1897
+ const status = updatePlayLiveStatusState(state, event);
1898
+ if (!status || !TERMINAL_PLAY_STATUSES.has(status.status)) {
1899
+ continue;
1900
+ }
1901
+ return await this.getRunStatus(status.runId || runId).catch(
1902
+ () => state.latest ?? playRunStatusFromState(state),
1903
+ );
1904
+ }
1905
+ } catch (error) {
1906
+ if (options?.signal?.aborted || !isTransientPlayStreamError(error)) {
1907
+ throw error;
1908
+ }
1909
+ endedReason = error instanceof Error ? error.message : String(error);
1910
+ }
1911
+
1912
+ // Window ended without a terminal event. The run may have finished
1913
+ // during the gap — re-check durable status once before reconnecting.
1914
+ // Non-transient status failures (e.g. 404 = run gone) fail loudly.
1915
+ let refreshed: PlayStatus | null = null;
1916
+ try {
1917
+ refreshed = await this.getRunStatus(runId);
1918
+ } catch (error) {
1919
+ if (!isTransientPlayStreamError(error)) {
1920
+ throw error;
1921
+ }
1922
+ }
1923
+ if (refreshed && TERMINAL_PLAY_STATUSES.has(refreshed.status)) {
1924
+ return refreshed;
1925
+ }
1926
+
1927
+ if (
1928
+ sawEvent ||
1929
+ Date.now() - connectedAt >= STREAM_HEALTHY_CONNECTION_MS
1930
+ ) {
1931
+ reconnectAttempt = 0;
1932
+ }
1933
+ const delayMs = streamReconnectDelayMs(reconnectAttempt);
1934
+ reconnectAttempt += 1;
1935
+ options?.onReconnect?.({
1936
+ attempt: reconnectAttempt,
1937
+ delayMs,
1938
+ reason: endedReason,
1939
+ });
1940
+ await sleep(delayMs);
1941
+ }
1942
+ }
1943
+
1601
1944
  /**
1602
1945
  * Fetch persisted logs for a run using the public runs resource model.
1603
1946
  *
@@ -1611,23 +1954,51 @@ export class DeeplineClient {
1611
1954
  runId: string,
1612
1955
  options?: RunsLogsOptions,
1613
1956
  ): Promise<RunsLogsResult> {
1614
- const status = await this.getRunStatus(runId, { full: true });
1615
- const logs = status.progress?.logs ?? [];
1616
- const limit =
1617
- typeof options?.limit === 'number' && Number.isFinite(options.limit)
1618
- ? Math.max(0, Math.trunc(options.limit))
1957
+ const limit = options?.all
1958
+ ? Number.MAX_SAFE_INTEGER
1959
+ : typeof options?.limit === 'number' &&
1960
+ Number.isFinite(options.limit) &&
1961
+ options.limit > 0
1962
+ ? Math.trunc(options.limit)
1619
1963
  : 200;
1620
- const entries = logs.slice(Math.max(0, logs.length - limit));
1964
+ const fetchPage = (afterSeq: number, pageLimit: number) =>
1965
+ this.http.get<RunLogsPageResponse>(
1966
+ `/api/v2/runs/${encodeURIComponent(runId)}/logs?afterSeq=${afterSeq}&limit=${pageLimit}`,
1967
+ );
1968
+ // Probe for the run's stored extent, then read the LAST `limit` stored
1969
+ // lines (matching the historical tail-slice semantics), paginating in
1970
+ // server-capped pages until the window is exhausted.
1971
+ const probe = await fetchPage(0, 1);
1972
+ const lastStoredSeq = probe.lastStoredSeq;
1973
+ let afterSeq = options?.all ? 0 : Math.max(0, lastStoredSeq - limit);
1974
+ const entries: Array<{ seq: number; line: string }> = [];
1975
+ while (entries.length < limit) {
1976
+ const page = await fetchPage(
1977
+ afterSeq,
1978
+ Math.min(RUN_LOGS_PAGE_LIMIT, limit - entries.length),
1979
+ );
1980
+ if (page.entries.length === 0) {
1981
+ break;
1982
+ }
1983
+ entries.push(...page.entries);
1984
+ afterSeq = page.entries[page.entries.length - 1]!.seq;
1985
+ if (!page.hasMore) {
1986
+ break;
1987
+ }
1988
+ }
1989
+ const firstSequence = entries.length > 0 ? entries[0]!.seq : null;
1990
+ const lastSequence =
1991
+ entries.length > 0 ? entries[entries.length - 1]!.seq : null;
1621
1992
  return {
1622
- runId: status.runId,
1623
- totalCount: logs.length,
1993
+ runId: probe.runId,
1994
+ totalCount: probe.totalLogCount,
1624
1995
  returnedCount: entries.length,
1625
- firstSequence:
1626
- logs.length === 0 ? null : logs.length - entries.length + 1,
1627
- lastSequence: logs.length === 0 ? null : logs.length,
1628
- truncated: logs.length > entries.length,
1629
- hasMore: logs.length > entries.length,
1630
- entries,
1996
+ firstSequence,
1997
+ lastSequence,
1998
+ truncated: entries.length < probe.totalLogCount,
1999
+ hasMore: lastSequence !== null && lastSequence < lastStoredSeq,
2000
+ entries: entries.map((entry) => entry.line),
2001
+ ...(probe.logsTruncated ? { logsTruncated: true } : {}),
1631
2002
  };
1632
2003
  }
1633
2004
 
@@ -1993,6 +2364,7 @@ export class DeeplineClient {
1993
2364
  runId: workflowId,
1994
2365
  status: 'running',
1995
2366
  logs: [],
2367
+ lastLogSeq: 0,
1996
2368
  latest: null,
1997
2369
  };
1998
2370
 
@@ -55,6 +55,7 @@
55
55
 
56
56
  // ——— Client ———
57
57
  export { DeeplineClient } from './client.js';
58
+ export { RunObserveTransportUnavailableError } from './runs/observe-transport.js';
58
59
  export type {
59
60
  PlayStatus,
60
61
  PlaySheetRow,
@@ -73,6 +73,10 @@
73
73
  import { DeeplineClient } from './client.js';
74
74
  import { DeeplineError } from './errors.js';
75
75
  import { createToolExecuteResult } from '../../shared_libs/play-runtime/tool-result.js';
76
+ export {
77
+ readValue,
78
+ readList,
79
+ } from '../../shared_libs/play-runtime/tool-result.js';
76
80
  import type {
77
81
  PlayDataset,
78
82
  PlayDatasetInput,
@@ -81,6 +85,7 @@ import type {
81
85
  ToolExecuteResult,
82
86
  ToolResultMetadataInput,
83
87
  } from '../../shared_libs/play-runtime/tool-result-types.js';
88
+ import type { EmailStatusExtractorConfig } from '../../shared_libs/play-runtime/email-status.js';
84
89
  import type { PreviousCell } from '../../shared_libs/play-runtime/cell-staleness.js';
85
90
  import type {
86
91
  DeeplineClientOptions,
@@ -1549,6 +1554,50 @@ function stringArray(value: unknown): string[] {
1549
1554
  return Array.isArray(value) ? value.map(String) : [];
1550
1555
  }
1551
1556
 
1557
+ function emailStatusExtractorConfig(
1558
+ value: unknown,
1559
+ ): EmailStatusExtractorConfig | undefined {
1560
+ if (!isRecord(value)) return undefined;
1561
+ const readPaths = (key: string): string[] | undefined => {
1562
+ const paths = stringArray(value[key])
1563
+ .map((path) => path.trim())
1564
+ .filter(Boolean);
1565
+ return paths.length > 0 ? paths : undefined;
1566
+ };
1567
+ const provider =
1568
+ typeof value.provider === 'string' && value.provider.trim()
1569
+ ? value.provider.trim()
1570
+ : null;
1571
+ if (!provider) return undefined;
1572
+ const config: EmailStatusExtractorConfig = { provider };
1573
+ for (const key of [
1574
+ 'rawStatus',
1575
+ 'rawScore',
1576
+ 'valid',
1577
+ 'deliverability',
1578
+ 'catchAll',
1579
+ 'mxProvider',
1580
+ 'mxRecord',
1581
+ 'fraudScore',
1582
+ 'disposable',
1583
+ 'roleBased',
1584
+ 'freeEmail',
1585
+ 'abuse',
1586
+ 'spamtrap',
1587
+ 'suspect',
1588
+ ] as const) {
1589
+ const paths = readPaths(key);
1590
+ if (paths) config[key] = paths;
1591
+ }
1592
+ if (isRecord(value.statusMap)) {
1593
+ config.statusMap = value.statusMap as EmailStatusExtractorConfig['statusMap'];
1594
+ }
1595
+ if (Array.isArray(value.rules)) {
1596
+ config.rules = value.rules as EmailStatusExtractorConfig['rules'];
1597
+ }
1598
+ return config;
1599
+ }
1600
+
1552
1601
  function extractorDescriptorRecord(
1553
1602
  value: unknown,
1554
1603
  ): ToolResultMetadataInput['extractors'] {
@@ -1566,6 +1615,7 @@ function extractorDescriptorRecord(
1566
1615
  const enumValues = stringArray(descriptor.enum)
1567
1616
  .map((entry) => entry.trim())
1568
1617
  .filter(Boolean);
1618
+ const emailStatus = emailStatusExtractorConfig(descriptor.emailStatus);
1569
1619
  return [
1570
1620
  [
1571
1621
  key,
@@ -1573,6 +1623,7 @@ function extractorDescriptorRecord(
1573
1623
  paths,
1574
1624
  ...(transforms.length > 0 ? { transforms } : {}),
1575
1625
  ...(enumValues.length > 0 ? { enum: enumValues } : {}),
1626
+ ...(emailStatus ? { emailStatus } : {}),
1576
1627
  },
1577
1628
  ],
1578
1629
  ];
@@ -50,10 +50,10 @@ export type SdkRelease = {
50
50
  };
51
51
 
52
52
  export const SDK_RELEASE = {
53
- version: '0.1.91',
53
+ version: '0.1.94',
54
54
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
55
55
  supportPolicy: {
56
- latest: '0.1.91',
56
+ latest: '0.1.94',
57
57
  minimumSupported: '0.1.53',
58
58
  deprecatedBelow: '0.1.53',
59
59
  },