deepline 0.1.275 → 0.1.277

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.
@@ -168,6 +168,10 @@ export type {
168
168
  PlayRunActionPackage,
169
169
  PlayRunDatasetActions,
170
170
  PlayRunPackage,
171
+ PlayActivityObservation,
172
+ PlayActivityState,
173
+ PlayActivityTarget,
174
+ PlayRunActivityProjection,
171
175
  PlayRunStart,
172
176
  ClearPlayHistoryRequest,
173
177
  ClearPlayHistoryResult,
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
155
155
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
156
156
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
157
157
  // Operators use the checkout-local deepline-admin binary instead.
158
- version: '0.1.275',
158
+ version: '0.1.277',
159
159
  contracts: {
160
160
  api: {
161
161
  name: 'sdk-http-api',
@@ -1,5 +1,17 @@
1
1
  import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest';
2
2
  import type { PlayRuntimeSelection } from '../../shared_libs/play-runtime/runtime-environment';
3
+ import type {
4
+ PlayActivityObservation,
5
+ PlayActivityState,
6
+ PlayActivityTarget,
7
+ PlayRunActivityProjection,
8
+ } from '../../shared_libs/play-runtime/activity-observation';
9
+ export type {
10
+ PlayActivityObservation,
11
+ PlayActivityState,
12
+ PlayActivityTarget,
13
+ PlayRunActivityProjection,
14
+ };
3
15
  import type {
4
16
  DeeplineToolCategory,
5
17
  PlayBootstrapFinderKind,
@@ -580,6 +592,8 @@ export interface PlayRunPackage {
580
592
  finishedAt?: number | null;
581
593
  durationMs?: number | null;
582
594
  error?: string;
595
+ /** Canonical explanation of what this run is doing or waiting on. */
596
+ activity?: PlayRunActivityProjection | null;
583
597
  };
584
598
  /** Bounded customer-safe warnings about output projection or availability. */
585
599
  warnings?: string[];
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Canonical, producer-facing description of what a play is doing.
3
+ *
4
+ * Producers report facts. They do not choose log copy, UI placement, polling
5
+ * intervals, or whether a run is "healthy". The activity router and projector
6
+ * own those decisions centrally.
7
+ */
8
+ export type PlayActivityTarget =
9
+ | {
10
+ kind: 'provider';
11
+ provider: string;
12
+ operation: string;
13
+ label?: string;
14
+ }
15
+ | {
16
+ kind: 'dataset';
17
+ tableNamespace: string;
18
+ operation: 'materializing' | 'persisting' | 'indexing' | 'reading';
19
+ label?: string;
20
+ }
21
+ | {
22
+ kind: 'lease';
23
+ resourceKind: 'run' | 'dataset' | 'row' | 'runner';
24
+ resourceLabel: string;
25
+ }
26
+ | {
27
+ kind: 'runner';
28
+ backend: string;
29
+ label?: string;
30
+ }
31
+ | {
32
+ kind: 'step';
33
+ stepId: string;
34
+ label?: string;
35
+ }
36
+ | {
37
+ kind: 'capacity';
38
+ pool: 'worker' | 'provider' | 'dataset' | 'runner';
39
+ label?: string;
40
+ };
41
+
42
+ export type PlayActivityProgress = {
43
+ completed?: number;
44
+ total?: number;
45
+ failed?: number;
46
+ message?: string;
47
+ };
48
+
49
+ export type PlayActivityState =
50
+ | {
51
+ kind: 'active';
52
+ progress?: PlayActivityProgress;
53
+ }
54
+ | {
55
+ kind: 'queued';
56
+ reason: 'capacity';
57
+ }
58
+ | {
59
+ kind: 'waiting';
60
+ reason:
61
+ | {
62
+ kind: 'external_event';
63
+ provider?: string;
64
+ eventKey?: string;
65
+ remaining?: number;
66
+ deadlineAt?: number;
67
+ }
68
+ | {
69
+ kind: 'dataset';
70
+ tableNamespace: string;
71
+ requiredPhase: 'available' | 'persisted' | 'indexed';
72
+ }
73
+ | {
74
+ kind: 'lease';
75
+ resourceKind: 'run' | 'dataset' | 'row' | 'runner';
76
+ resourceLabel: string;
77
+ expiresAt?: number;
78
+ }
79
+ | {
80
+ /** Mixed-version fallback only; new producers must choose a typed reason. */
81
+ kind: 'unknown';
82
+ };
83
+ }
84
+ | {
85
+ kind: 'retrying';
86
+ reason: 'rate_limit' | 'provider_error' | 'transport_error';
87
+ retryAt: number;
88
+ attempt?: number;
89
+ }
90
+ | {
91
+ kind: 'scheduled';
92
+ reason: 'sleep';
93
+ resumeAt: number;
94
+ }
95
+ | {
96
+ kind: 'completed';
97
+ }
98
+ | {
99
+ kind: 'failed';
100
+ code?: string;
101
+ };
102
+
103
+ export type PlayActivityObservation = {
104
+ schemaVersion: 1;
105
+ /** Stable within one run, for example `tool:discover_ctos` or `dataset:leads`. */
106
+ activityId: string;
107
+ /** Optional owning play step. */
108
+ stepId?: string;
109
+ target: PlayActivityTarget;
110
+ state: PlayActivityState;
111
+ /** Producer clock. The runtime envelope supplies run identity and ordering. */
112
+ observedAt: number;
113
+ };
114
+
115
+ export type PlayActivityEvent = {
116
+ type: 'activity.observed';
117
+ observation: PlayActivityObservation;
118
+ };
119
+
120
+ function isRecord(value: unknown): value is Record<string, unknown> {
121
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
122
+ }
123
+
124
+ export function isPlayActivityObservation(
125
+ value: unknown,
126
+ ): value is PlayActivityObservation {
127
+ if (
128
+ !isRecord(value) ||
129
+ value.schemaVersion !== 1 ||
130
+ typeof value.activityId !== 'string' ||
131
+ !value.activityId.trim() ||
132
+ typeof value.observedAt !== 'number' ||
133
+ !Number.isFinite(value.observedAt) ||
134
+ !isRecord(value.target) ||
135
+ !isRecord(value.state)
136
+ ) {
137
+ return false;
138
+ }
139
+ const targetKinds = new Set([
140
+ 'provider',
141
+ 'dataset',
142
+ 'lease',
143
+ 'runner',
144
+ 'step',
145
+ 'capacity',
146
+ ]);
147
+ const stateKinds = new Set([
148
+ 'active',
149
+ 'queued',
150
+ 'waiting',
151
+ 'retrying',
152
+ 'scheduled',
153
+ 'completed',
154
+ 'failed',
155
+ ]);
156
+ return (
157
+ typeof value.target.kind === 'string' &&
158
+ targetKinds.has(value.target.kind) &&
159
+ typeof value.state.kind === 'string' &&
160
+ stateKinds.has(value.state.kind)
161
+ );
162
+ }
163
+
164
+ export type PlayRunActivityProjection = {
165
+ observation: PlayActivityObservation;
166
+ summary: string;
167
+ visibility: PlayActivityVisibility;
168
+ /** False only when old payloads lacked enough typed facts to classify. */
169
+ classified: boolean;
170
+ };
171
+
172
+ /**
173
+ * Persistence lanes are a routing concern, not a producer concern:
174
+ *
175
+ * - durable: append a meaningful transition to the run ledger;
176
+ * - snapshot: replace current progress for this activity;
177
+ * - liveness: update last-seen evidence without growing the event ledger;
178
+ * - drop: stale or duplicate observation.
179
+ */
180
+ export type PlayActivityRoutingLane =
181
+ | 'durable'
182
+ | 'snapshot'
183
+ | 'liveness'
184
+ | 'drop';
185
+
186
+ function stableJson(value: unknown): string {
187
+ return JSON.stringify(value);
188
+ }
189
+
190
+ function targetIdentity(target: PlayActivityTarget): string {
191
+ return stableJson(target);
192
+ }
193
+
194
+ export function routePlayActivityObservation(input: {
195
+ previous?: PlayActivityObservation | null;
196
+ next: PlayActivityObservation;
197
+ }): PlayActivityRoutingLane {
198
+ const { previous, next } = input;
199
+ if (!previous) return 'durable';
200
+ if (next.observedAt <= previous.observedAt) return 'drop';
201
+ if (targetIdentity(previous.target) !== targetIdentity(next.target)) {
202
+ return 'durable';
203
+ }
204
+ if (previous.state.kind !== next.state.kind) return 'durable';
205
+ if (next.state.kind !== 'active') {
206
+ return stableJson(previous.state) === stableJson(next.state)
207
+ ? 'liveness'
208
+ : 'durable';
209
+ }
210
+ return stableJson(previous.state) === stableJson(next.state)
211
+ ? 'liveness'
212
+ : 'snapshot';
213
+ }
214
+
215
+ export type PlayActivityVisibility =
216
+ | 'hidden'
217
+ | 'timeline'
218
+ | 'headline'
219
+ | 'action';
220
+
221
+ /**
222
+ * Central presentation policy. Short, self-healing retries do not become
223
+ * alarming UI/log messages; blockers and failures do.
224
+ */
225
+ export function derivePlayActivityVisibility(input: {
226
+ observation: PlayActivityObservation;
227
+ now: number;
228
+ }): PlayActivityVisibility {
229
+ const { state } = input.observation;
230
+ if (state.kind === 'failed') return 'action';
231
+ if (state.kind === 'waiting') {
232
+ return state.reason.kind === 'external_event' ? 'action' : 'headline';
233
+ }
234
+ if (state.kind === 'queued' || state.kind === 'scheduled') return 'headline';
235
+ if (state.kind === 'retrying') {
236
+ return state.retryAt - input.now <= 30_000 ? 'hidden' : 'timeline';
237
+ }
238
+ if (state.kind === 'active') return 'headline';
239
+ return 'timeline';
240
+ }
241
+
242
+ function targetLabel(target: PlayActivityTarget): string {
243
+ switch (target.kind) {
244
+ case 'provider':
245
+ return target.label ?? `${target.provider} ${target.operation}`;
246
+ case 'dataset':
247
+ return target.label ?? `dataset ${target.tableNamespace}`;
248
+ case 'lease':
249
+ return `${target.resourceKind} ${target.resourceLabel}`;
250
+ case 'runner':
251
+ return target.label ?? `${target.backend} runner`;
252
+ case 'step':
253
+ return target.label ?? target.stepId;
254
+ case 'capacity':
255
+ return target.label ?? `${target.pool} capacity`;
256
+ }
257
+ }
258
+
259
+ export function summarizePlayActivity(
260
+ observation: PlayActivityObservation,
261
+ ): string {
262
+ const label = targetLabel(observation.target);
263
+ const { state } = observation;
264
+ switch (state.kind) {
265
+ case 'active': {
266
+ const progress = state.progress;
267
+ const counts =
268
+ typeof progress?.completed === 'number' &&
269
+ typeof progress.total === 'number'
270
+ ? ` (${progress.completed}/${progress.total})`
271
+ : '';
272
+ return `Active: ${label}${counts}.`;
273
+ }
274
+ case 'queued':
275
+ return `Queued: waiting for ${label}.`;
276
+ case 'waiting':
277
+ if (state.reason.kind === 'external_event') {
278
+ const provider = state.reason.provider
279
+ ? ` from ${state.reason.provider}`
280
+ : '';
281
+ return `Blocked: waiting for an external event${provider}.`;
282
+ }
283
+ if (state.reason.kind === 'dataset') {
284
+ return `Blocked: waiting for dataset ${state.reason.tableNamespace} to become ${state.reason.requiredPhase}.`;
285
+ }
286
+ if (state.reason.kind === 'lease') {
287
+ return `Contended: waiting for ${state.reason.resourceKind} lease ${state.reason.resourceLabel}.`;
288
+ }
289
+ return `Paused: ${label}; the legacy producer did not report a typed reason.`;
290
+ case 'retrying':
291
+ return `Delayed: ${label} will retry automatically.`;
292
+ case 'scheduled':
293
+ return `Scheduled: ${label} will resume automatically.`;
294
+ case 'completed':
295
+ return `Completed: ${label}.`;
296
+ case 'failed':
297
+ return `Failed: ${label}.`;
298
+ }
299
+ }
300
+
301
+ export type PlayActivityReporter = {
302
+ observe(
303
+ input: Omit<PlayActivityObservation, 'schemaVersion' | 'observedAt'> & {
304
+ observedAt?: number;
305
+ },
306
+ ): void;
307
+ };
308
+
309
+ export function createPlayActivityReporter(input: {
310
+ emit: (event: PlayActivityEvent) => void;
311
+ now?: () => number;
312
+ }): PlayActivityReporter {
313
+ return {
314
+ observe(observation) {
315
+ input.emit({
316
+ type: 'activity.observed',
317
+ observation: {
318
+ ...observation,
319
+ schemaVersion: 1,
320
+ observedAt: observation.observedAt ?? input.now?.() ?? Date.now(),
321
+ },
322
+ });
323
+ },
324
+ };
325
+ }
326
+
327
+ type ProjectionStep = {
328
+ nodeId: string;
329
+ status?: string;
330
+ label?: string;
331
+ updatedAt?: number | null;
332
+ progress?: PlayActivityProgress | null;
333
+ };
334
+
335
+ type ProjectionDataset = {
336
+ datasetId?: string;
337
+ tableNamespace: string;
338
+ phase?: string;
339
+ persistedRows?: number;
340
+ succeededRows?: number;
341
+ failedRows?: number;
342
+ complete?: boolean;
343
+ updatedAt?: number;
344
+ };
345
+
346
+ function isTerminalActivityState(state: PlayActivityState): boolean {
347
+ return state.kind === 'completed' || state.kind === 'failed';
348
+ }
349
+
350
+ function projection(
351
+ observation: PlayActivityObservation,
352
+ now: number,
353
+ classified = true,
354
+ ): PlayRunActivityProjection {
355
+ return {
356
+ observation,
357
+ summary: summarizePlayActivity(observation),
358
+ visibility: derivePlayActivityVisibility({ observation, now }),
359
+ classified,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Read-side compatibility projector. Explicit typed observations win; existing
365
+ * suspension, dataset, and step facts are adapted without requiring every
366
+ * legacy producer to migrate in one deploy.
367
+ */
368
+ export function projectPlayRunActivity(input: {
369
+ runId: string;
370
+ playName?: string | null;
371
+ status: string;
372
+ updatedAt?: number | null;
373
+ waitKind?: string | null;
374
+ waitUntil?: number | null;
375
+ eventKey?: string | null;
376
+ runtimeBackend?: string | null;
377
+ activeNodeId?: string | null;
378
+ nodeStates?: ProjectionStep[] | null;
379
+ datasets?: ProjectionDataset[] | null;
380
+ explicit?: PlayActivityObservation[] | null;
381
+ now?: number;
382
+ }): PlayRunActivityProjection | null {
383
+ const now = input.now ?? Date.now();
384
+ const observedAt = input.updatedAt ?? now;
385
+ const normalizedStatus = input.status.trim().toLowerCase();
386
+ if (
387
+ normalizedStatus === 'completed' ||
388
+ normalizedStatus === 'failed' ||
389
+ normalizedStatus === 'cancelled' ||
390
+ normalizedStatus === 'terminated' ||
391
+ normalizedStatus === 'timed_out'
392
+ ) {
393
+ return null;
394
+ }
395
+
396
+ const explicit = [...(input.explicit ?? [])]
397
+ .filter((candidate) => !isTerminalActivityState(candidate.state))
398
+ .sort((left, right) => right.observedAt - left.observedAt)[0];
399
+ if (explicit) return projection(explicit, now);
400
+
401
+ if (input.waitKind === 'detached_runner') {
402
+ return projection(
403
+ {
404
+ schemaVersion: 1,
405
+ activityId: 'runner:managed',
406
+ target: {
407
+ kind: 'runner',
408
+ backend: input.runtimeBackend ?? 'managed',
409
+ label: 'managed play runner',
410
+ },
411
+ state: { kind: 'active' },
412
+ observedAt,
413
+ },
414
+ now,
415
+ );
416
+ }
417
+ if (input.waitKind === 'sleep') {
418
+ return projection(
419
+ {
420
+ schemaVersion: 1,
421
+ activityId: 'run:sleep',
422
+ target: {
423
+ kind: 'step',
424
+ stepId: input.activeNodeId ?? 'run',
425
+ label: input.playName ?? 'play',
426
+ },
427
+ state: {
428
+ kind: 'scheduled',
429
+ reason: 'sleep',
430
+ resumeAt: input.waitUntil ?? observedAt,
431
+ },
432
+ observedAt,
433
+ },
434
+ now,
435
+ );
436
+ }
437
+ if (
438
+ input.waitKind === 'integration_event' ||
439
+ input.waitKind === 'integration_event_batch'
440
+ ) {
441
+ return projection(
442
+ {
443
+ schemaVersion: 1,
444
+ activityId: `event:${input.activeNodeId ?? 'run'}`,
445
+ stepId: input.activeNodeId ?? undefined,
446
+ target: {
447
+ kind: 'step',
448
+ stepId: input.activeNodeId ?? 'run',
449
+ label: input.playName ?? 'play',
450
+ },
451
+ state: {
452
+ kind: 'waiting',
453
+ reason: {
454
+ kind: 'external_event',
455
+ eventKey: input.eventKey ?? undefined,
456
+ deadlineAt: input.waitUntil ?? undefined,
457
+ },
458
+ },
459
+ observedAt,
460
+ },
461
+ now,
462
+ );
463
+ }
464
+ // Mixed-version readers may see the durable WAITING status before the
465
+ // scheduler's typed wait projection is available. Do not let a stale
466
+ // running step overwrite that stronger lifecycle fact.
467
+ if (normalizedStatus === 'waiting') {
468
+ return projection(
469
+ {
470
+ schemaVersion: 1,
471
+ activityId: 'run:legacy-wait',
472
+ target: {
473
+ kind: 'step',
474
+ stepId: input.activeNodeId ?? 'run',
475
+ label: input.playName ?? 'play',
476
+ },
477
+ state: { kind: 'waiting', reason: { kind: 'unknown' } },
478
+ observedAt,
479
+ },
480
+ now,
481
+ false,
482
+ );
483
+ }
484
+
485
+ const pendingDataset = [...(input.datasets ?? [])]
486
+ .filter(
487
+ (dataset) =>
488
+ dataset.phase === 'registered' ||
489
+ (dataset.complete !== true && dataset.phase !== 'failed'),
490
+ )
491
+ .sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
492
+ if (pendingDataset) {
493
+ return projection(
494
+ {
495
+ schemaVersion: 1,
496
+ activityId: `dataset:${pendingDataset.datasetId ?? pendingDataset.tableNamespace}`,
497
+ target: {
498
+ kind: 'dataset',
499
+ tableNamespace: pendingDataset.tableNamespace,
500
+ operation: 'materializing',
501
+ },
502
+ state: {
503
+ kind: 'active',
504
+ progress: {
505
+ completed: pendingDataset.persistedRows,
506
+ total:
507
+ typeof pendingDataset.succeededRows === 'number' ||
508
+ typeof pendingDataset.failedRows === 'number'
509
+ ? (pendingDataset.succeededRows ?? 0) +
510
+ (pendingDataset.failedRows ?? 0)
511
+ : undefined,
512
+ failed: pendingDataset.failedRows,
513
+ },
514
+ },
515
+ observedAt: pendingDataset.updatedAt ?? observedAt,
516
+ },
517
+ now,
518
+ );
519
+ }
520
+
521
+ const activeStep =
522
+ input.nodeStates?.find(
523
+ (candidate) => candidate.nodeId === input.activeNodeId,
524
+ ) ??
525
+ input.nodeStates?.find((candidate) => candidate.status === 'running') ??
526
+ null;
527
+ if (activeStep) {
528
+ const toolId = activeStep.nodeId.startsWith('tool:')
529
+ ? activeStep.nodeId.split(':').at(-1)?.trim() || null
530
+ : null;
531
+ const provider = toolId?.split(/[._]/)[0]?.trim() || null;
532
+ return projection(
533
+ {
534
+ schemaVersion: 1,
535
+ activityId: `step:${activeStep.nodeId}`,
536
+ stepId: activeStep.nodeId,
537
+ target:
538
+ toolId && provider
539
+ ? {
540
+ kind: 'provider',
541
+ provider,
542
+ operation: toolId,
543
+ label: activeStep.label ?? toolId,
544
+ }
545
+ : {
546
+ kind: 'step',
547
+ stepId: activeStep.nodeId,
548
+ label: activeStep.label,
549
+ },
550
+ state: {
551
+ kind: 'active',
552
+ progress: activeStep.progress ?? undefined,
553
+ },
554
+ observedAt: activeStep.updatedAt ?? observedAt,
555
+ },
556
+ now,
557
+ );
558
+ }
559
+
560
+ if (normalizedStatus === 'queued') {
561
+ return projection(
562
+ {
563
+ schemaVersion: 1,
564
+ activityId: 'capacity:worker',
565
+ target: { kind: 'capacity', pool: 'worker' },
566
+ state: { kind: 'queued', reason: 'capacity' },
567
+ observedAt,
568
+ },
569
+ now,
570
+ );
571
+ }
572
+
573
+ const fallback: PlayActivityObservation = {
574
+ schemaVersion: 1,
575
+ activityId: 'run:legacy',
576
+ target: {
577
+ kind: 'step',
578
+ stepId: input.activeNodeId ?? 'run',
579
+ label: input.playName ?? 'play',
580
+ },
581
+ state:
582
+ normalizedStatus === 'waiting'
583
+ ? { kind: 'waiting', reason: { kind: 'unknown' } }
584
+ : { kind: 'active' },
585
+ observedAt,
586
+ };
587
+ return projection(fallback, now, normalizedStatus !== 'waiting');
588
+ }
@@ -5,6 +5,10 @@ import { pipeline } from 'node:stream/promises';
5
5
  import type { PlaySheetContract } from '@shared_libs/plays/static-pipeline';
6
6
  import type { PlayRowUpdate } from '@shared_libs/play-runtime/ctx-types';
7
7
  import type { PlayRunTimelineEntry } from '@shared_libs/play-runtime/live-events';
8
+ import {
9
+ routePlayActivityObservation,
10
+ type PlayActivityObservation,
11
+ } from '@shared_libs/play-runtime/activity-observation';
8
12
  import {
9
13
  buildPlayRunLedgerEventsFromStatusPatch,
10
14
  buildTerminalLogReplayEvents,
@@ -82,6 +86,8 @@ export type RuntimeStatusUpdate = {
82
86
  complete?: boolean;
83
87
  at: number;
84
88
  }>;
89
+ /** Sparse typed activity facts. Routing/deduplication happens server-side. */
90
+ activityObservations?: PlayActivityObservation[];
85
91
  /**
86
92
  * Explicit terminal-output replay for final runner logs when the caller must
87
93
  * keep `status` nonterminal. Direct-worker/Postgres progress finalizers do this so
@@ -1491,6 +1497,25 @@ async function updateRunStatusViaAppRuntimeUnlocked(
1491
1497
  ...(event.complete === undefined ? {} : { complete: event.complete }),
1492
1498
  });
1493
1499
  }
1500
+ const latestActivities = new Map(
1501
+ Object.entries(previousSnapshot.activitiesById),
1502
+ );
1503
+ for (const observation of update.activityObservations ?? []) {
1504
+ const previous = latestActivities.get(observation.activityId) ?? null;
1505
+ const lane = routePlayActivityObservation({
1506
+ previous,
1507
+ next: observation,
1508
+ });
1509
+ if (lane === 'drop' || lane === 'liveness') continue;
1510
+ events.push({
1511
+ type: 'activity.observed',
1512
+ runId: update.playId,
1513
+ source: 'worker',
1514
+ occurredAt: observation.observedAt,
1515
+ observation,
1516
+ });
1517
+ latestActivities.set(observation.activityId, observation);
1518
+ }
1494
1519
  const terminalLogEvents =
1495
1520
  terminalLogReplay && terminalLogReplay.lines.length > 0
1496
1521
  ? buildTerminalLogReplayEvents({