deepline 0.1.199 → 0.1.200
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/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +5 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +51 -17
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
- package/dist/bundling-sources/sdk/src/http.ts +5 -2
- package/dist/bundling-sources/sdk/src/play.ts +1 -1
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
- package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
- package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
- package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +2 -3
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +459 -103
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
- package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
- package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
- package/dist/cli/index.js +167 -54
- package/dist/cli/index.mjs +167 -54
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -8
- package/dist/index.mjs +8 -8
- package/dist/plays/bundle-play-file.mjs +15 -20
- package/package.json +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
- package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
- package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
- package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const encoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder();
|
|
2
|
+
|
|
3
|
+
export const TERMINAL_RUN_RESULT_MAX_BYTES = 2_500_000;
|
|
4
|
+
export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
|
|
5
|
+
export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export class OutputTooLargeError extends Error {
|
|
10
|
+
readonly code = 'OUTPUT_TOO_LARGE';
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly bytes: number;
|
|
13
|
+
readonly limitBytes: number;
|
|
14
|
+
|
|
15
|
+
constructor(input: {
|
|
16
|
+
path: string;
|
|
17
|
+
bytes: number;
|
|
18
|
+
limitBytes: number;
|
|
19
|
+
advice: string;
|
|
20
|
+
}) {
|
|
21
|
+
super(
|
|
22
|
+
`OutputTooLarge: ${input.path} is ${formatBytes(input.bytes)}, above the ${formatBytes(input.limitBytes)} limit. ` +
|
|
23
|
+
input.advice,
|
|
24
|
+
);
|
|
25
|
+
this.name = 'OutputTooLargeError';
|
|
26
|
+
this.path = input.path;
|
|
27
|
+
this.bytes = input.bytes;
|
|
28
|
+
this.limitBytes = input.limitBytes;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function jsonByteLength(value: unknown): number {
|
|
33
|
+
const serialized = JSON.stringify(value);
|
|
34
|
+
if (serialized === undefined) return 0;
|
|
35
|
+
return encoder?.encode(serialized).byteLength ?? serialized.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatBytes(bytes: number): string {
|
|
39
|
+
if (bytes >= 1024 * 1024) {
|
|
40
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
|
|
41
|
+
}
|
|
42
|
+
if (bytes >= 1024) {
|
|
43
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
44
|
+
}
|
|
45
|
+
return `${bytes} B`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertJsonSized(input: {
|
|
49
|
+
value: unknown;
|
|
50
|
+
path: string;
|
|
51
|
+
limitBytes: number;
|
|
52
|
+
advice: string;
|
|
53
|
+
}): number {
|
|
54
|
+
let bytes: number;
|
|
55
|
+
try {
|
|
56
|
+
bytes = jsonByteLength(input.value);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`${input.path} cannot be serialized as JSON: ${
|
|
60
|
+
error instanceof Error ? error.message : String(error)
|
|
61
|
+
}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (bytes > input.limitBytes) {
|
|
65
|
+
throw new OutputTooLargeError({
|
|
66
|
+
path: input.path,
|
|
67
|
+
bytes,
|
|
68
|
+
limitBytes: input.limitBytes,
|
|
69
|
+
advice: input.advice,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return bytes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function valueWithinJsonByteLimit(value: unknown, limitBytes: number) {
|
|
76
|
+
if (value === undefined || value === null) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
return jsonByteLength(value) > limitBytes ? undefined : value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Shared terminal-result contract for every scheduler backend. */
|
|
83
|
+
export function persistableTerminalRunResult(value: unknown): unknown {
|
|
84
|
+
return valueWithinJsonByteLimit(value, TERMINAL_RUN_RESULT_MAX_BYTES);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function assertCustomerOutputValueWithinLimit(input: {
|
|
88
|
+
value: unknown;
|
|
89
|
+
path: string;
|
|
90
|
+
limitBytes?: number;
|
|
91
|
+
}): number {
|
|
92
|
+
return assertJsonSized({
|
|
93
|
+
value: input.value,
|
|
94
|
+
path: input.path,
|
|
95
|
+
limitBytes: input.limitBytes ?? CUSTOMER_OUTPUT_VALUE_MAX_BYTES,
|
|
96
|
+
advice:
|
|
97
|
+
'Deepline can process large provider/cache payloads, but customer-visible outputs must stay bounded. Extract the fields you need before returning or saving this value.',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function assertCustomerOutputObjectWithinLimit(input: {
|
|
102
|
+
value: Record<string, unknown>;
|
|
103
|
+
path: string;
|
|
104
|
+
totalLimitBytes?: number;
|
|
105
|
+
valueLimitBytes?: number;
|
|
106
|
+
}): number {
|
|
107
|
+
for (const [key, value] of Object.entries(input.value)) {
|
|
108
|
+
assertCustomerOutputValueWithinLimit({
|
|
109
|
+
value,
|
|
110
|
+
path: `${input.path}.${key}`,
|
|
111
|
+
limitBytes: input.valueLimitBytes,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return assertJsonSized({
|
|
115
|
+
value: input.value,
|
|
116
|
+
path: input.path,
|
|
117
|
+
limitBytes: input.totalLimitBytes ?? CUSTOMER_OUTPUT_TOTAL_MAX_BYTES,
|
|
118
|
+
advice:
|
|
119
|
+
'This looks like too much data for one customer-visible output object. Split the work into rows, or parse the provider response and save only the columns you actually need.',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function assertRuntimeReceiptOutputWithinLimit(input: {
|
|
124
|
+
output: unknown;
|
|
125
|
+
path: string;
|
|
126
|
+
limitBytes?: number;
|
|
127
|
+
}): number {
|
|
128
|
+
return assertJsonSized({
|
|
129
|
+
value: input.output,
|
|
130
|
+
path: input.path,
|
|
131
|
+
limitBytes: input.limitBytes ?? RUNTIME_RECEIPT_OUTPUT_MAX_BYTES,
|
|
132
|
+
advice:
|
|
133
|
+
'This receipt result is too large for durable call caching. This usually means raw search results, HTML, or a large list is being cached as one cell. Parse it before returning it from the tool boundary.',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type PlayRunInputPayload = Record<string, unknown> | unknown[];
|
|
2
|
+
|
|
3
|
+
export function normalizePlayRunInputPayload(
|
|
4
|
+
input: unknown,
|
|
5
|
+
): PlayRunInputPayload {
|
|
6
|
+
if (Array.isArray(input)) return input;
|
|
7
|
+
if (input && typeof input === 'object') {
|
|
8
|
+
return input as Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function playRunInputObjectView(
|
|
14
|
+
input: PlayRunInputPayload,
|
|
15
|
+
): Record<string, unknown> {
|
|
16
|
+
return Array.isArray(input) ? {} : input;
|
|
17
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { HATCHET_COLD_EXECUTION_PHASE_DEFINITIONS } from './hatchet-cold-execution-target';
|
|
2
|
-
|
|
3
1
|
export type PlayLatencyPhaseDefinition = {
|
|
4
2
|
header: string;
|
|
5
3
|
phase: string;
|
|
@@ -294,7 +292,6 @@ export const PLAY_LATENCY_SUMMARY_COLUMNS: PlayLatencyPhaseDefinition[] = [
|
|
|
294
292
|
description:
|
|
295
293
|
'Runner wall time not explained by inner coordinator.runner_run.',
|
|
296
294
|
},
|
|
297
|
-
...HATCHET_COLD_EXECUTION_PHASE_DEFINITIONS,
|
|
298
295
|
];
|
|
299
296
|
|
|
300
297
|
const PHASE_DEFINITION_BY_KEY = new Map(
|
|
@@ -11,6 +11,7 @@ import type { PlayLiveEventSource } from './live-events';
|
|
|
11
11
|
import type { PreloadedRuntimeDbSession } from './db-session';
|
|
12
12
|
import type { PacingRule } from './governor/rate-state-backend';
|
|
13
13
|
import type { GovernanceSnapshot } from './governor/governor';
|
|
14
|
+
import type { PlayRunInputPayload } from './play-input';
|
|
14
15
|
|
|
15
16
|
export type PlayRunnerRateStateBackendConfig =
|
|
16
17
|
| {
|
|
@@ -49,6 +50,7 @@ export type PlayRunnerRateStatePenalizeInput = {
|
|
|
49
50
|
export interface PlayRunnerContextConfig {
|
|
50
51
|
executorToken?: string;
|
|
51
52
|
baseUrl?: string;
|
|
53
|
+
receiptGatewayBaseUrl?: string | null;
|
|
52
54
|
vercelProtectionBypassToken?: string | null;
|
|
53
55
|
integrationMode?: 'live' | 'eval_stub' | 'fixture';
|
|
54
56
|
orgId?: string;
|
|
@@ -78,6 +80,13 @@ export interface PlayRunnerContextConfig {
|
|
|
78
80
|
preloadedDbSessions?: PreloadedRuntimeDbSession[];
|
|
79
81
|
rateStateBackend?: PlayRunnerRateStateBackendConfig;
|
|
80
82
|
runtimeSchedulerSchema?: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Execution profile a scheduler-backed `ctx.runPlay` child must be launched
|
|
85
|
+
* under. The runner posts it as the `profile` on the child `/api/v2/plays/run`
|
|
86
|
+
* submission so a child inherits the parent's scheduler (e.g. `absurd`) instead
|
|
87
|
+
* of defaulting away from the parent scheduler. Absent means `absurd`.
|
|
88
|
+
*/
|
|
89
|
+
childRunProfile?: string | null;
|
|
81
90
|
governance?: GovernanceSnapshot | null;
|
|
82
91
|
}
|
|
83
92
|
|
|
@@ -87,7 +96,7 @@ export interface PlayRunnerExecutionConfig {
|
|
|
87
96
|
bundledCodePath?: string | null;
|
|
88
97
|
bundledCodeEncoding?: 'utf8' | 'gzip';
|
|
89
98
|
} | null;
|
|
90
|
-
input:
|
|
99
|
+
input: PlayRunInputPayload;
|
|
91
100
|
checkpoint?: PlayCheckpoint | null;
|
|
92
101
|
csvSourcePath?: string | null;
|
|
93
102
|
csvSourceUrl?: string | null;
|
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export const PLAY_RUNTIME_PROVIDER_IDS = {
|
|
14
14
|
workersEdge: 'workers_edge',
|
|
15
|
-
|
|
16
|
-
local: 'local',
|
|
15
|
+
absurd: 'absurd',
|
|
17
16
|
} as const;
|
|
18
17
|
|
|
19
18
|
export type PlayRuntimeProviderId =
|
|
@@ -40,22 +39,17 @@ export const PLAY_RUNTIME_PROVIDERS: Record<
|
|
|
40
39
|
artifactKind: PLAY_ARTIFACT_KINDS.esmWorkers,
|
|
41
40
|
label: 'Cloudflare Dynamic Workflows + Dynamic Workers + DO dedup',
|
|
42
41
|
},
|
|
43
|
-
|
|
44
|
-
id: PLAY_RUNTIME_PROVIDER_IDS.
|
|
45
|
-
scheduler: PLAY_SCHEDULER_BACKENDS.
|
|
42
|
+
absurd: {
|
|
43
|
+
id: PLAY_RUNTIME_PROVIDER_IDS.absurd,
|
|
44
|
+
scheduler: PLAY_SCHEDULER_BACKENDS.absurd,
|
|
45
|
+
// Server runs execute on Daytona; the local-dev/test shape swaps the runner
|
|
46
|
+
// axis to local_process (same as the local profile) via an explicit
|
|
47
|
+
// runtimeBackend override at submit while tests drive local_process.
|
|
46
48
|
runner: PLAY_RUNTIME_BACKENDS.daytona,
|
|
47
49
|
dedup: PLAY_DEDUP_BACKENDS.inMemory,
|
|
48
50
|
artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
|
|
49
51
|
label:
|
|
50
|
-
'
|
|
51
|
-
},
|
|
52
|
-
local: {
|
|
53
|
-
id: PLAY_RUNTIME_PROVIDER_IDS.local,
|
|
54
|
-
scheduler: PLAY_SCHEDULER_BACKENDS.temporal,
|
|
55
|
-
runner: PLAY_RUNTIME_BACKENDS.localProcess,
|
|
56
|
-
dedup: PLAY_DEDUP_BACKENDS.inMemory,
|
|
57
|
-
artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
|
|
58
|
-
label: 'Local Temporal scheduler + local subprocess runner (tests)',
|
|
52
|
+
'Absurd (Postgres-native durable) scheduler + one-shot Daytona runner + receipt-backed in-process dedup',
|
|
59
53
|
},
|
|
60
54
|
};
|
|
61
55
|
|
|
@@ -72,7 +66,7 @@ export function resolvePlayRuntimeProvider(
|
|
|
72
66
|
return PLAY_RUNTIME_PROVIDERS[id as PlayRuntimeProviderId];
|
|
73
67
|
}
|
|
74
68
|
throw new Error(
|
|
75
|
-
`
|
|
69
|
+
`Unsupported play runtime provider "${id}". Expected one of: ${Object.keys(
|
|
76
70
|
PLAY_RUNTIME_PROVIDERS,
|
|
77
71
|
).join(', ')}.`,
|
|
78
72
|
);
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES,
|
|
3
|
+
RUNTIME_RECEIPT_OUTPUT_MAX_BYTES,
|
|
4
|
+
} from './output-size-limits';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_SIZE = 200;
|
|
7
|
+
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_FLUSH_MS = 10;
|
|
8
|
+
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BUFFERED_BYTES =
|
|
9
|
+
RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES;
|
|
10
|
+
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_INPUT_BYTES =
|
|
11
|
+
RUNTIME_RECEIPT_OUTPUT_MAX_BYTES;
|
|
12
|
+
|
|
13
|
+
type ReceiptCompletionSinkTimer = ReturnType<typeof setTimeout>;
|
|
14
|
+
|
|
15
|
+
export interface ReceiptCompletionSinkFlushEvent {
|
|
16
|
+
count: number;
|
|
17
|
+
bytes: number;
|
|
18
|
+
elapsedMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RuntimeReceiptCompletionSinkOptions<TInput, TOutput> {
|
|
22
|
+
maxBatchSize?: number;
|
|
23
|
+
maxFlushMs?: number;
|
|
24
|
+
maxBufferedBytes?: number;
|
|
25
|
+
maxInputBytes?: number;
|
|
26
|
+
estimateBytes?: (input: TInput) => number;
|
|
27
|
+
completeMany: (inputs: TInput[]) => Promise<TOutput[]>;
|
|
28
|
+
onFlushStart?: (event: ReceiptCompletionSinkFlushEvent) => void;
|
|
29
|
+
onFlushDone?: (event: ReceiptCompletionSinkFlushEvent) => void;
|
|
30
|
+
onFlushError?: (
|
|
31
|
+
event: ReceiptCompletionSinkFlushEvent & { error: unknown },
|
|
32
|
+
) => void;
|
|
33
|
+
now?: () => number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PendingReceiptCompletion<TInput, TOutput> {
|
|
37
|
+
input: TInput;
|
|
38
|
+
bytes: number;
|
|
39
|
+
resolve: (output: TOutput) => void;
|
|
40
|
+
reject: (error: unknown) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizePositiveInteger(
|
|
44
|
+
value: number | undefined,
|
|
45
|
+
fallback: number,
|
|
46
|
+
): number {
|
|
47
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
|
|
48
|
+
return Math.max(1, Math.floor(value));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultReceiptCompletionByteEstimate(input: unknown): number {
|
|
52
|
+
try {
|
|
53
|
+
return new TextEncoder().encode(JSON.stringify(input)).byteLength;
|
|
54
|
+
} catch {
|
|
55
|
+
return 1024;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class ReceiptCompletionBufferLimitError extends Error {
|
|
60
|
+
readonly code = 'RECEIPT_COMPLETION_BUFFER_LIMIT_EXCEEDED';
|
|
61
|
+
|
|
62
|
+
constructor(message: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'ReceiptCompletionBufferLimitError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ReceiptCompletionResultCountError extends Error {
|
|
69
|
+
readonly code = 'RECEIPT_COMPLETION_RESULT_COUNT_MISMATCH';
|
|
70
|
+
|
|
71
|
+
constructor(message: string) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'ReceiptCompletionResultCountError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class RuntimeReceiptCompletionSink<TInput, TOutput> {
|
|
78
|
+
readonly maxBatchSize: number;
|
|
79
|
+
readonly maxFlushMs: number;
|
|
80
|
+
readonly maxBufferedBytes: number;
|
|
81
|
+
readonly maxInputBytes: number;
|
|
82
|
+
|
|
83
|
+
#pending: Array<PendingReceiptCompletion<TInput, TOutput>> = [];
|
|
84
|
+
#pendingBytes = 0;
|
|
85
|
+
#flushTimer: ReceiptCompletionSinkTimer | null = null;
|
|
86
|
+
#flushPromise: Promise<void> | null = null;
|
|
87
|
+
#closed = false;
|
|
88
|
+
#completeMany: (inputs: TInput[]) => Promise<TOutput[]>;
|
|
89
|
+
#estimateBytes: (input: TInput) => number;
|
|
90
|
+
#onFlushStart?: (event: ReceiptCompletionSinkFlushEvent) => void;
|
|
91
|
+
#onFlushDone?: (event: ReceiptCompletionSinkFlushEvent) => void;
|
|
92
|
+
#onFlushError?: (
|
|
93
|
+
event: ReceiptCompletionSinkFlushEvent & { error: unknown },
|
|
94
|
+
) => void;
|
|
95
|
+
#now: () => number;
|
|
96
|
+
|
|
97
|
+
constructor(options: RuntimeReceiptCompletionSinkOptions<TInput, TOutput>) {
|
|
98
|
+
this.maxBatchSize = normalizePositiveInteger(
|
|
99
|
+
options.maxBatchSize,
|
|
100
|
+
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_SIZE,
|
|
101
|
+
);
|
|
102
|
+
this.maxFlushMs = normalizePositiveInteger(
|
|
103
|
+
options.maxFlushMs,
|
|
104
|
+
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_FLUSH_MS,
|
|
105
|
+
);
|
|
106
|
+
this.maxBufferedBytes = normalizePositiveInteger(
|
|
107
|
+
options.maxBufferedBytes,
|
|
108
|
+
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BUFFERED_BYTES,
|
|
109
|
+
);
|
|
110
|
+
this.maxInputBytes = normalizePositiveInteger(
|
|
111
|
+
options.maxInputBytes,
|
|
112
|
+
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_INPUT_BYTES,
|
|
113
|
+
);
|
|
114
|
+
this.#completeMany = options.completeMany;
|
|
115
|
+
this.#estimateBytes =
|
|
116
|
+
options.estimateBytes ?? defaultReceiptCompletionByteEstimate;
|
|
117
|
+
this.#onFlushStart = options.onFlushStart;
|
|
118
|
+
this.#onFlushDone = options.onFlushDone;
|
|
119
|
+
this.#onFlushError = options.onFlushError;
|
|
120
|
+
this.#now = options.now ?? Date.now;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addComplete(input: TInput): Promise<TOutput> {
|
|
124
|
+
return this.addMany([input]).then((outputs) => outputs[0]!);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
addMany(inputs: readonly TInput[]): Promise<TOutput[]> {
|
|
128
|
+
if (inputs.length === 0) return Promise.resolve([]);
|
|
129
|
+
if (this.#closed) {
|
|
130
|
+
return Promise.reject(
|
|
131
|
+
new Error('Runtime receipt completion sink is already closed.'),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const outputPromises = inputs.map((input) => this.#enqueue(input));
|
|
136
|
+
this.#scheduleFlush();
|
|
137
|
+
return Promise.all(outputPromises);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async flush(): Promise<void> {
|
|
141
|
+
this.#clearFlushTimer();
|
|
142
|
+
this.#flushPromise ??= this.#runFlushLoop().finally(() => {
|
|
143
|
+
this.#flushPromise = null;
|
|
144
|
+
});
|
|
145
|
+
return await this.#flushPromise;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async close(): Promise<void> {
|
|
149
|
+
this.#closed = true;
|
|
150
|
+
await this.flush();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getStats(): { pending: number; pendingBytes: number; flushing: boolean } {
|
|
154
|
+
return {
|
|
155
|
+
pending: this.#pending.length,
|
|
156
|
+
pendingBytes: this.#pendingBytes,
|
|
157
|
+
flushing: Boolean(this.#flushPromise),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#enqueue(input: TInput): Promise<TOutput> {
|
|
162
|
+
const bytes = Math.max(1, Math.ceil(this.#estimateBytes(input)));
|
|
163
|
+
if (bytes > this.maxInputBytes) {
|
|
164
|
+
return Promise.reject(
|
|
165
|
+
new ReceiptCompletionBufferLimitError(
|
|
166
|
+
`Runtime receipt completion payload is ${bytes} bytes, above the ${this.maxInputBytes} byte per-receipt limit. Parse large provider/search responses before returning them from the tool boundary.`,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
this.#pending.length >= this.maxBatchSize ||
|
|
173
|
+
this.#pendingBytes + bytes > this.maxBufferedBytes
|
|
174
|
+
) {
|
|
175
|
+
void this.flush().catch(() => undefined);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Promise<TOutput>((resolve, reject) => {
|
|
179
|
+
this.#pending.push({ input, bytes, resolve, reject });
|
|
180
|
+
this.#pendingBytes += bytes;
|
|
181
|
+
if (this.#pending.length >= this.maxBatchSize) {
|
|
182
|
+
void this.flush().catch(() => undefined);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#scheduleFlush(): void {
|
|
188
|
+
if (this.#flushTimer || this.#flushPromise || this.#pending.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.#flushTimer = setTimeout(() => {
|
|
192
|
+
this.#flushTimer = null;
|
|
193
|
+
void this.flush().catch(() => undefined);
|
|
194
|
+
}, this.maxFlushMs);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#clearFlushTimer(): void {
|
|
198
|
+
if (!this.#flushTimer) return;
|
|
199
|
+
clearTimeout(this.#flushTimer);
|
|
200
|
+
this.#flushTimer = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async #runFlushLoop(): Promise<void> {
|
|
204
|
+
while (this.#pending.length > 0) {
|
|
205
|
+
const batch = this.#takeBatch();
|
|
206
|
+
await this.#flushBatch(batch);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#takeBatch(): Array<PendingReceiptCompletion<TInput, TOutput>> {
|
|
211
|
+
const batch: Array<PendingReceiptCompletion<TInput, TOutput>> = [];
|
|
212
|
+
let batchBytes = 0;
|
|
213
|
+
while (this.#pending.length > 0 && batch.length < this.maxBatchSize) {
|
|
214
|
+
const next = this.#pending[0]!;
|
|
215
|
+
if (
|
|
216
|
+
batch.length > 0 &&
|
|
217
|
+
batchBytes + next.bytes > this.maxBufferedBytes
|
|
218
|
+
) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
this.#pending.shift();
|
|
222
|
+
this.#pendingBytes -= next.bytes;
|
|
223
|
+
batch.push(next);
|
|
224
|
+
batchBytes += next.bytes;
|
|
225
|
+
}
|
|
226
|
+
return batch;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async #flushBatch(
|
|
230
|
+
batch: Array<PendingReceiptCompletion<TInput, TOutput>>,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const inputs = batch.map((item) => item.input);
|
|
233
|
+
const bytes = batch.reduce((sum, item) => sum + item.bytes, 0);
|
|
234
|
+
const startedAt = this.#now();
|
|
235
|
+
this.#onFlushStart?.({ count: inputs.length, bytes });
|
|
236
|
+
try {
|
|
237
|
+
const outputs = await this.#completeMany(inputs);
|
|
238
|
+
if (outputs.length !== batch.length) {
|
|
239
|
+
throw new ReceiptCompletionResultCountError(
|
|
240
|
+
`Runtime receipt completion sink expected ${batch.length} result(s), received ${outputs.length}.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
outputs.forEach((output, index) => batch[index]!.resolve(output));
|
|
244
|
+
this.#onFlushDone?.({
|
|
245
|
+
count: inputs.length,
|
|
246
|
+
bytes,
|
|
247
|
+
elapsedMs: this.#now() - startedAt,
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
for (const item of batch) {
|
|
251
|
+
item.reject(error);
|
|
252
|
+
}
|
|
253
|
+
this.#onFlushError?.({
|
|
254
|
+
count: inputs.length,
|
|
255
|
+
bytes,
|
|
256
|
+
elapsedMs: this.#now() - startedAt,
|
|
257
|
+
error,
|
|
258
|
+
});
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type RuntimeReceiptHeartbeatOutcome = 'active' | 'terminal';
|
|
2
|
+
|
|
3
|
+
export type RuntimeReceiptHeartbeatSupervisor = {
|
|
4
|
+
start(): void;
|
|
5
|
+
stop(): void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createRuntimeReceiptHeartbeatSupervisor(input: {
|
|
9
|
+
intervalMs: number;
|
|
10
|
+
heartbeat: () => Promise<RuntimeReceiptHeartbeatOutcome>;
|
|
11
|
+
isLeaseLost: (error: unknown) => boolean;
|
|
12
|
+
onLeaseLost: (error: unknown) => void;
|
|
13
|
+
onTransientFailure?: (error: unknown) => void;
|
|
14
|
+
onTerminal?: () => void;
|
|
15
|
+
}): RuntimeReceiptHeartbeatSupervisor {
|
|
16
|
+
if (!Number.isFinite(input.intervalMs) || input.intervalMs <= 0) {
|
|
17
|
+
throw new Error('Runtime receipt heartbeat interval must be positive.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let stopped = false;
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
+
|
|
23
|
+
const stop = () => {
|
|
24
|
+
stopped = true;
|
|
25
|
+
if (timer !== null) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const schedule = () => {
|
|
32
|
+
if (stopped || timer !== null) return;
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
timer = null;
|
|
35
|
+
void input
|
|
36
|
+
.heartbeat()
|
|
37
|
+
.then((outcome) => {
|
|
38
|
+
if (outcome === 'terminal') {
|
|
39
|
+
stop();
|
|
40
|
+
input.onTerminal?.();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
schedule();
|
|
44
|
+
})
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
if (input.isLeaseLost(error)) {
|
|
47
|
+
stop();
|
|
48
|
+
input.onLeaseLost(error);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
input.onTransientFailure?.(error);
|
|
52
|
+
schedule();
|
|
53
|
+
});
|
|
54
|
+
}, input.intervalMs);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
start() {
|
|
59
|
+
if (stopped) return;
|
|
60
|
+
schedule();
|
|
61
|
+
},
|
|
62
|
+
stop,
|
|
63
|
+
};
|
|
64
|
+
}
|