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.
- package/dist/cli/index.js +4012 -705
- package/dist/cli/index.mjs +4028 -714
- package/dist/index.d.mts +232 -108
- package/dist/index.d.ts +232 -108
- package/dist/index.js +1145 -99
- package/dist/index.mjs +1134 -99
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +87 -20
- package/dist/repo/apps/play-runner-workers/src/entry.ts +75 -22
- package/dist/repo/sdk/src/client.ts +412 -40
- package/dist/repo/sdk/src/index.ts +1 -0
- package/dist/repo/sdk/src/play.ts +51 -0
- package/dist/repo/sdk/src/release.ts +2 -2
- package/dist/repo/sdk/src/runs/observe-transport.ts +481 -0
- package/dist/repo/sdk/src/stream-reconnect.ts +44 -0
- package/dist/repo/sdk/src/types.ts +10 -3
- package/dist/repo/shared_libs/play-runtime/email-status.ts +10 -36
- package/dist/repo/shared_libs/play-runtime/extractor-targets.ts +3 -3
- package/dist/repo/shared_libs/play-runtime/live-events.ts +217 -0
- package/dist/repo/shared_libs/play-runtime/run-ledger.ts +1074 -0
- package/dist/repo/shared_libs/play-runtime/run-snapshot-stream.ts +581 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +44 -0
- package/dist/repo/shared_libs/plays/secret-guardrails.ts +22 -11
- package/package.json +5 -2
|
@@ -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
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
490
|
+
event.type === 'play.run.final_status' &&
|
|
491
|
+
state.logs.length === 0 &&
|
|
492
|
+
state.lastLogSeq === 0
|
|
395
493
|
) {
|
|
396
|
-
|
|
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
|
-
/**
|
|
1564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1591
|
-
|
|
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
|
|
1847
|
+
`Run observation for ${runId} ended before a terminal status.`,
|
|
1595
1848
|
undefined,
|
|
1596
|
-
'
|
|
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
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
|
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:
|
|
1623
|
-
totalCount:
|
|
1993
|
+
runId: probe.runId,
|
|
1994
|
+
totalCount: probe.totalLogCount,
|
|
1624
1995
|
returnedCount: entries.length,
|
|
1625
|
-
firstSequence
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
|
|
@@ -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.
|
|
53
|
+
version: '0.1.94',
|
|
54
54
|
apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
|
|
55
55
|
supportPolicy: {
|
|
56
|
-
latest: '0.1.
|
|
56
|
+
latest: '0.1.94',
|
|
57
57
|
minimumSupported: '0.1.53',
|
|
58
58
|
deprecatedBelow: '0.1.53',
|
|
59
59
|
},
|