threadwire 0.1.6 → 0.1.9

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.
@@ -0,0 +1,158 @@
1
+ // @ts-nocheck
2
+ /* eslint-disable jsdoc/require-jsdoc */
3
+
4
+ import {deadlineAfter, abortable, readResponseCapped} from "./absolute-deadline.js"
5
+
6
+ const MAX_RESPONSE_BYTES = 2_097_152
7
+ const DEFAULT_TIMEOUT_MS = 3_660_000
8
+ const MAX_RAW_OUTPUT_BYTES = 1_048_576
9
+
10
+ export class IsolatedRuntimeClient {
11
+ /** @param {{url: string, controlToken: string, timeoutMs?: number | string, fetchImplementation?: typeof fetch}} options */
12
+ constructor(options) {
13
+ this.url = normalizedUrl(options.url)
14
+ this.controlToken = nonempty(options.controlToken, "isolated runtime control token")
15
+ this.fetch = options.fetchImplementation ?? fetch
16
+ this.timeoutMs = positiveDuration(options.timeoutMs, DEFAULT_TIMEOUT_MS)
17
+ }
18
+
19
+ /**
20
+ * @param {{provider: string, profile: string, repositoryRoot: string, cwd: string, providerArguments: string[], resumeSession?: string, signal?: AbortSignal, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}} request
21
+ * @returns {Promise<{preflightId: string, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}>}
22
+ */
23
+ async preflight(request) {
24
+ const deadline = request.deadline ?? deadlineAfter(this.timeoutMs, {signal: request.signal, timeoutMessage: "Isolated runtime launch timeout"})
25
+ const response = await this.request("/preflight", {...request, launchDeadlineAt: deadline.expiresAt}, deadline)
26
+ if (response.ok !== true || typeof response.preflightId !== "string") throw new Error("Isolated runtime preflight failed")
27
+ return {preflightId: response.preflightId, deadline}
28
+ }
29
+
30
+ /**
31
+ * @param {{
32
+ * preflightId: string, prompt: string, providerArguments: string[],
33
+ * resumeSession?: string,
34
+ * signal?: AbortSignal,
35
+ * deadline?: import("./absolute-deadline.js").AbsoluteDeadline,
36
+ * onEvent: (event: import("./types.js").WorkerEvent) => void | Promise<void>,
37
+ * onRecord?: (record: unknown) => void | Promise<void>,
38
+ * onStdoutChunk?: (chunk: Buffer) => void | Promise<void>,
39
+ * onStderrChunk?: (chunk: Buffer) => void | Promise<void>
40
+ * }} options
41
+ */
42
+ async run(options) {
43
+ const deadline = options.deadline ?? deadlineAfter(this.timeoutMs, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"})
44
+ const response = await this.request("/run", {
45
+ preflightId: options.preflightId,
46
+ prompt: options.prompt,
47
+ providerArguments: options.providerArguments,
48
+ ...(options.resumeSession === undefined ? {} : {resumeSession: options.resumeSession})
49
+ }, deadline)
50
+ if (!Array.isArray(response.records) || !Array.isArray(response.rawChunks) || !Number.isSafeInteger(response.exitCode)) throw new Error("Isolated runtime emitted an invalid response")
51
+ let rawBytes = 0
52
+ const decoded = response.rawChunks.map((item) => {
53
+ if (!isRecord(item) || Object.keys(item).sort().join(",") !== "channel,data"
54
+ || (item.channel !== "stdout" && item.channel !== "stderr") || typeof item.data !== "string"
55
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(item.data)) {
56
+ throw new Error("Isolated runtime emitted an invalid response")
57
+ }
58
+ const data = Buffer.from(item.data, "base64")
59
+ rawBytes += data.length
60
+ if (rawBytes > MAX_RAW_OUTPUT_BYTES) throw new Error("Isolated runtime emitted an invalid response")
61
+ return {channel: item.channel, data}
62
+ })
63
+ for (const chunk of decoded) {
64
+ deadline.throwIfAborted()
65
+ const callback = chunk.channel === "stdout" ? options.onStdoutChunk : options.onStderrChunk
66
+ await abortable(callback?.(chunk.data), deadline.signal)
67
+ }
68
+ for (const record of response.records) {
69
+ deadline.throwIfAborted()
70
+ await abortable(options.onRecord?.(record), deadline.signal)
71
+ if (isWorkerEventEnvelope(record)) await abortable(options.onEvent(record.event), deadline.signal)
72
+ }
73
+ return /** @type {number} */ (response.exitCode)
74
+ }
75
+
76
+ /** @param {string} path @param {unknown} body */
77
+ async request(path, body, deadline) {
78
+ const signal = deadline.signal
79
+ const response = await abortable(this.fetch(`${this.url}${path}`, {
80
+ method: "POST",
81
+ redirect: "error",
82
+ headers: {
83
+ authorization: `Bearer ${this.controlToken}`,
84
+ "content-type": "application/json",
85
+ connection: "close",
86
+ "x-threadwire-launch-deadline": String(deadline.expiresAt)
87
+ },
88
+ body: JSON.stringify(body),
89
+ signal
90
+ }), signal)
91
+ let bytes
92
+ try {
93
+ bytes = await readResponseCapped(response, MAX_RESPONSE_BYTES, signal)
94
+ } catch (error) {
95
+ if (error instanceof Error && error.message === "Response exceeded capacity") throw new Error("Isolated runtime response exceeded the configured capacity", {cause: error})
96
+ throw error
97
+ }
98
+ if (!response.ok) throw new Error(safeRemoteError(bytes))
99
+ try {
100
+ const value = JSON.parse(bytes.toString("utf8"))
101
+ if (!isRecord(value)) throw new Error()
102
+ return value
103
+ } catch {
104
+ throw new Error("Isolated runtime emitted an invalid response")
105
+ }
106
+ }
107
+ }
108
+
109
+ function positiveDuration(value, fallback) {
110
+ if (value === undefined) return fallback
111
+ const number = Number(value)
112
+ if (!Number.isSafeInteger(number) || number < 1 || number > 604_800_000) throw new Error("Invalid isolated runtime client timeout")
113
+ return number
114
+ }
115
+ /** @param {NodeJS.ProcessEnv} environment */
116
+ export function isolatedRuntimeClientFromEnvironment(environment) {
117
+ return new IsolatedRuntimeClient({
118
+ url: nonempty(environment.THREADWIRE_ISOLATED_RUNTIME_URL, "THREADWIRE_ISOLATED_RUNTIME_URL"),
119
+ controlToken: nonempty(environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN"),
120
+ timeoutMs: environment.THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS
121
+ })
122
+ }
123
+
124
+ /** @param {NodeJS.ProcessEnv} environment */
125
+ export function kimiIsolatedRuntimeClientFromEnvironment(environment) {
126
+ return new IsolatedRuntimeClient({
127
+ url: nonempty(environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_URL, "THREADWIRE_KIMI_ISOLATED_RUNTIME_URL"),
128
+ controlToken: nonempty(environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN"),
129
+ timeoutMs: environment.THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS
130
+ })
131
+ }
132
+
133
+ function normalizedUrl(value) {
134
+ const url = new URL(value)
135
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("THREADWIRE_ISOLATED_RUNTIME_URL must use HTTP or HTTPS")
136
+ url.pathname = url.pathname.replace(/\/+$/u, "")
137
+ url.search = ""
138
+ url.hash = ""
139
+ return url.toString().replace(/\/$/u, "")
140
+ }
141
+
142
+ function nonempty(value, name) {
143
+ if (typeof value !== "string" || value.trim().length === 0 || /[\r\n]/u.test(value)) throw new Error(`${name} is required`)
144
+ return value
145
+ }
146
+
147
+ function safeRemoteError(bytes) {
148
+ const text = bytes.toString("utf8").trim()
149
+ return /^[\x20-\x7e]{1,200}$/u.test(text) ? text : "Isolated runtime request failed"
150
+ }
151
+
152
+ function isRecord(value) {
153
+ return typeof value === "object" && value !== null && !Array.isArray(value)
154
+ }
155
+
156
+ function isWorkerEventEnvelope(value) {
157
+ return isRecord(value) && value.type === "worker-event" && isRecord(value.event)
158
+ }