@dbx-tools/appkit-mastra 0.6.215 → 0.6.217
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/README.md +96 -4
- package/index.ts +9 -3
- package/lib/index.d.ts +9 -3
- package/lib/index.js +6 -2
- package/lib/src/agents.d.ts +4 -3
- package/lib/src/agents.js +10 -5
- package/lib/src/config.d.ts +8 -0
- package/lib/src/config.js +5 -1
- package/lib/src/model.d.ts +2 -0
- package/lib/src/model.js +21 -22
- package/lib/src/monty-sandbox.d.ts +51 -0
- package/lib/src/monty-sandbox.js +331 -0
- package/lib/src/plugin.d.ts +1 -1
- package/lib/src/sandbox.d.ts +100 -0
- package/lib/src/sandbox.js +418 -0
- package/lib/src/serving-sanitize.d.ts +18 -1
- package/lib/src/serving-sanitize.js +41 -3
- package/lib/src/workspaces.d.ts +19 -6
- package/lib/src/workspaces.js +70 -7
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -22
- package/src/agents.ts +13 -5
- package/src/config.ts +13 -0
- package/src/model.ts +21 -23
- package/src/monty-sandbox.ts +406 -0
- package/src/sandbox.ts +515 -0
- package/src/serving-sanitize.ts +50 -2
- package/src/workspaces.ts +105 -6
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Databricks Sandbox adapter for Mastra workspaces.
|
|
3
|
+
*
|
|
4
|
+
* Implements Mastra's foreground command surface over the Databricks Sandbox
|
|
5
|
+
* REST API. Sandboxes are created lazily, retain their home directory across
|
|
6
|
+
* inactivity stops, and execute untrusted shell commands outside the App
|
|
7
|
+
* container.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { WorkspaceClient } from "@databricks/appkit";
|
|
13
|
+
import { databricks } from "@dbx-tools/appkit";
|
|
14
|
+
import { async, error, log, string } from "@dbx-tools/shared-core";
|
|
15
|
+
import type { RequestContext } from "@mastra/core/request-context";
|
|
16
|
+
import type {
|
|
17
|
+
CommandResult,
|
|
18
|
+
ExecuteCommandOptions,
|
|
19
|
+
ProviderStatus,
|
|
20
|
+
SandboxInfo,
|
|
21
|
+
WorkspaceSandbox,
|
|
22
|
+
} from "@mastra/core/workspace";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { MontySandbox, type MontySandboxOptions } from "./monty-sandbox.ts";
|
|
25
|
+
|
|
26
|
+
const SANDBOX_API_PATH = "/api/2.0/sandboxes";
|
|
27
|
+
const SANDBOX_EXEC_API_PATH = "/api/2.0/sandbox-exec/sandboxes";
|
|
28
|
+
const DEFAULT_INACTIVITY_TIMEOUT = "900s";
|
|
29
|
+
const DEFAULT_STARTUP_TIMEOUT_MS = 120_000;
|
|
30
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
|
|
31
|
+
const SANDBOX_POLL_INTERVAL_MS = 500;
|
|
32
|
+
const logger = log.logger("mastra/sandbox");
|
|
33
|
+
|
|
34
|
+
const SandboxStateSchema = z.object({
|
|
35
|
+
state: z.string().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const SandboxResponseSchema = z.object({
|
|
39
|
+
name: z.string(),
|
|
40
|
+
display_name: z.string().optional(),
|
|
41
|
+
create_time: z.string().optional(),
|
|
42
|
+
update_time: z.string().optional(),
|
|
43
|
+
status: SandboxStateSchema.optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const ExecuteResponseSchema = z.object({
|
|
47
|
+
exit_code: z.number().int().optional(),
|
|
48
|
+
status: z.string(),
|
|
49
|
+
stdout: z.string().optional(),
|
|
50
|
+
stderr: z.string().optional(),
|
|
51
|
+
command_id: z.string().optional(),
|
|
52
|
+
truncated: z.boolean().optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** Options for one Databricks-backed Mastra sandbox. */
|
|
56
|
+
export interface DatabricksSandboxOptions {
|
|
57
|
+
/** AppKit workspace client used for every sandbox API call. */
|
|
58
|
+
client: WorkspaceClient;
|
|
59
|
+
/** Resource id, with or without the `sandboxes/` prefix. */
|
|
60
|
+
sandboxId: string;
|
|
61
|
+
/** Human-readable Databricks label. */
|
|
62
|
+
displayName?: string;
|
|
63
|
+
/** Server-side idle timeout in protobuf duration form. Defaults to `900s`. */
|
|
64
|
+
inactivityTimeout?: string;
|
|
65
|
+
/** Maximum time to wait for the sandbox to become runnable. */
|
|
66
|
+
startupTimeoutMs?: number;
|
|
67
|
+
/** Default command timeout. Per-call `options.timeout` wins. */
|
|
68
|
+
commandTimeoutMs?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Provider used only when Databricks Sandbox is definitively unavailable.
|
|
71
|
+
* Defaults to the Node Pydantic Monty runtime; `false` fails instead.
|
|
72
|
+
*/
|
|
73
|
+
fallback?: false | "monty" | MontySandboxOptions | WorkspaceSandbox;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Databricks options accepted by the higher-level workspace factory. */
|
|
77
|
+
export interface DatabricksWorkspaceSandboxOptions extends Omit<
|
|
78
|
+
DatabricksSandboxOptions,
|
|
79
|
+
"client" | "sandboxId"
|
|
80
|
+
> {
|
|
81
|
+
/** Provider discriminator. Defaults to `databricks`. */
|
|
82
|
+
provider?: "databricks";
|
|
83
|
+
/**
|
|
84
|
+
* Credential source. Defaults to a fresh AppKit client using the normal
|
|
85
|
+
* environment/profile chain, which is the app service principal in a
|
|
86
|
+
* Databricks App.
|
|
87
|
+
*/
|
|
88
|
+
client?: WorkspaceClient | ((context: { requestContext: RequestContext }) => WorkspaceClient);
|
|
89
|
+
/**
|
|
90
|
+
* Fixed id or per-request resolver. Omit to derive a stable, opaque id from
|
|
91
|
+
* the workspace and attributed user.
|
|
92
|
+
*/
|
|
93
|
+
sandboxId?: string | ((context: { requestContext: RequestContext }) => string);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Mastra sandbox backed by Databricks serverless Sandbox compute. */
|
|
97
|
+
export class DatabricksSandbox implements WorkspaceSandbox {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly name: string;
|
|
100
|
+
status: ProviderStatus = "pending";
|
|
101
|
+
error?: string;
|
|
102
|
+
|
|
103
|
+
private readonly client: WorkspaceClient;
|
|
104
|
+
private readonly displayName: string;
|
|
105
|
+
private readonly inactivityTimeout: string;
|
|
106
|
+
private readonly startupTimeoutMs: number;
|
|
107
|
+
private readonly commandTimeoutMs: number;
|
|
108
|
+
private readonly fallbackConfig: Exclude<DatabricksSandboxOptions["fallback"], undefined>;
|
|
109
|
+
private readonly createdAt = new Date();
|
|
110
|
+
private remoteCreatedAt?: Date;
|
|
111
|
+
private lastUsedAt?: Date;
|
|
112
|
+
private activeFallback?: WorkspaceSandbox;
|
|
113
|
+
private startPromise?: Promise<void>;
|
|
114
|
+
|
|
115
|
+
constructor(options: DatabricksSandboxOptions) {
|
|
116
|
+
const id = string.trimToNull(options.sandboxId.replace(/^sandboxes\//, ""));
|
|
117
|
+
if (!id) throw new TypeError("Databricks sandbox id must not be blank");
|
|
118
|
+
this.id = id;
|
|
119
|
+
this.name = options.displayName?.trim() || `Mastra sandbox ${id}`;
|
|
120
|
+
this.client = options.client;
|
|
121
|
+
this.displayName = this.name;
|
|
122
|
+
this.inactivityTimeout = options.inactivityTimeout ?? DEFAULT_INACTIVITY_TIMEOUT;
|
|
123
|
+
this.startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
124
|
+
this.commandTimeoutMs = options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
125
|
+
this.fallbackConfig = options.fallback ?? "monty";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get provider(): string {
|
|
129
|
+
return this.activeFallback?.provider ?? "databricks";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Ensure the remote sandbox exists and has reached its running state. */
|
|
133
|
+
async start(): Promise<void> {
|
|
134
|
+
await this.startWithSignal();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async startWithSignal(signal?: AbortSignal): Promise<void> {
|
|
138
|
+
if (this.status === "running") return;
|
|
139
|
+
if (this.startPromise) {
|
|
140
|
+
await this.startPromise;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const pending = this.performStart(signal);
|
|
144
|
+
this.startPromise = pending;
|
|
145
|
+
try {
|
|
146
|
+
await pending;
|
|
147
|
+
} finally {
|
|
148
|
+
if (this.startPromise === pending) this.startPromise = undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async performStart(signal?: AbortSignal): Promise<void> {
|
|
153
|
+
if (this.activeFallback) {
|
|
154
|
+
await this.activeFallback.start?.();
|
|
155
|
+
this.status = this.activeFallback.status;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.status = "starting";
|
|
159
|
+
this.error = undefined;
|
|
160
|
+
try {
|
|
161
|
+
let sandbox = await this.getOrCreate(signal);
|
|
162
|
+
if (sandbox.status?.state === "SANDBOX_STATE_STOPPED") {
|
|
163
|
+
sandbox = await this.mutate("start", signal);
|
|
164
|
+
}
|
|
165
|
+
if (sandbox.status?.state === "SANDBOX_STATE_STOPPING") {
|
|
166
|
+
await this.waitForState("SANDBOX_STATE_STOPPED", signal);
|
|
167
|
+
sandbox = await this.mutate("start", signal);
|
|
168
|
+
}
|
|
169
|
+
if (sandbox.status?.state !== "SANDBOX_STATE_RUNNING") {
|
|
170
|
+
await this.waitForState("SANDBOX_STATE_RUNNING", signal);
|
|
171
|
+
}
|
|
172
|
+
this.status = "running";
|
|
173
|
+
this.lastUsedAt = new Date();
|
|
174
|
+
} catch (caught) {
|
|
175
|
+
if (this.fallbackConfig !== false && isDatabricksSandboxUnavailable(caught)) {
|
|
176
|
+
const fallback = resolveFallback(this.fallbackConfig);
|
|
177
|
+
logger.warn("Databricks Sandbox unavailable; using fallback", {
|
|
178
|
+
sandboxId: this.id,
|
|
179
|
+
fallback: fallback.provider,
|
|
180
|
+
error: error.errorMessage(caught),
|
|
181
|
+
});
|
|
182
|
+
await fallback.start?.();
|
|
183
|
+
this.activeFallback = fallback;
|
|
184
|
+
this.status = fallback.status;
|
|
185
|
+
this.error = fallback.error;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.status = "error";
|
|
189
|
+
this.error = error.errorMessage(caught);
|
|
190
|
+
throw caught;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Databricks home persistence is not a cloneable Mastra checkpoint. */
|
|
195
|
+
async snapshot(): Promise<void> {
|
|
196
|
+
await this.activeFallback?.snapshot();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get supportsCheckpoints(): boolean {
|
|
200
|
+
return this.activeFallback?.supportsCheckpoints ?? false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Stop compute while preserving the sandbox home directory. */
|
|
204
|
+
async stop(): Promise<void> {
|
|
205
|
+
if (this.activeFallback) {
|
|
206
|
+
await this.activeFallback.stop?.();
|
|
207
|
+
this.status = this.activeFallback.status;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (this.status === "stopped" || this.status === "destroyed") return;
|
|
211
|
+
this.status = "stopping";
|
|
212
|
+
try {
|
|
213
|
+
await this.mutate("stop");
|
|
214
|
+
this.status = "stopped";
|
|
215
|
+
} catch (caught) {
|
|
216
|
+
if (isStatus(caught, 404)) {
|
|
217
|
+
this.status = "stopped";
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.status = "error";
|
|
221
|
+
this.error = error.errorMessage(caught);
|
|
222
|
+
throw caught;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Permanently delete the sandbox and its persisted home directory. */
|
|
227
|
+
async destroy(): Promise<void> {
|
|
228
|
+
if (this.activeFallback) {
|
|
229
|
+
await this.activeFallback.destroy?.();
|
|
230
|
+
this.status = this.activeFallback.status;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (this.status === "destroyed") return;
|
|
234
|
+
this.status = "destroying";
|
|
235
|
+
try {
|
|
236
|
+
await this.request(`${this.resourcePath}`, "DELETE", z.object({}).passthrough());
|
|
237
|
+
this.status = "destroyed";
|
|
238
|
+
} catch (caught) {
|
|
239
|
+
if (isStatus(caught, 404)) {
|
|
240
|
+
this.status = "destroyed";
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.status = "error";
|
|
244
|
+
this.error = error.errorMessage(caught);
|
|
245
|
+
throw caught;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Execute one foreground shell command in the remote sandbox. */
|
|
250
|
+
async executeCommand(
|
|
251
|
+
command: string,
|
|
252
|
+
args: string[] = [],
|
|
253
|
+
options: ExecuteCommandOptions = {},
|
|
254
|
+
): Promise<CommandResult> {
|
|
255
|
+
await this.startWithSignal(options.abortSignal);
|
|
256
|
+
if (this.activeFallback) {
|
|
257
|
+
if (!this.activeFallback.executeCommand) {
|
|
258
|
+
return {
|
|
259
|
+
command,
|
|
260
|
+
args,
|
|
261
|
+
success: false,
|
|
262
|
+
exitCode: 1,
|
|
263
|
+
stdout: "",
|
|
264
|
+
stderr: `Fallback sandbox ${this.activeFallback.provider} does not support command execution.`,
|
|
265
|
+
executionTimeMs: 0,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return this.activeFallback.executeCommand(command, args, options);
|
|
269
|
+
}
|
|
270
|
+
const timeout = options.timeout ?? this.commandTimeoutMs;
|
|
271
|
+
const script = commandScript(command, args, options.cwd);
|
|
272
|
+
const started = performance.now();
|
|
273
|
+
const response = await this.request(
|
|
274
|
+
`${SANDBOX_EXEC_API_PATH}/${encodeURIComponent(this.id)}/exec-sync`,
|
|
275
|
+
"POST",
|
|
276
|
+
ExecuteResponseSchema,
|
|
277
|
+
{
|
|
278
|
+
payload: {
|
|
279
|
+
cmd: "/bin/bash",
|
|
280
|
+
args: ["-lc", script],
|
|
281
|
+
envs: definedEnvironment(options.env),
|
|
282
|
+
execution_timeout: `${Math.max(1, Math.ceil(timeout / 1_000))}s`,
|
|
283
|
+
},
|
|
284
|
+
signal: options.abortSignal,
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
const stdout = response.stdout ?? "";
|
|
288
|
+
const stderr = response.stderr ?? "";
|
|
289
|
+
options.onStdout?.(stdout);
|
|
290
|
+
options.onStderr?.(stderr);
|
|
291
|
+
this.lastUsedAt = new Date();
|
|
292
|
+
const timedOut = response.status === "EXECUTE_COMMAND_STATUS_TIMED_OUT";
|
|
293
|
+
const exitCode = response.exit_code ?? -1;
|
|
294
|
+
return {
|
|
295
|
+
command,
|
|
296
|
+
args,
|
|
297
|
+
success: response.status === "EXECUTE_COMMAND_STATUS_COMPLETED" && exitCode === 0,
|
|
298
|
+
exitCode,
|
|
299
|
+
stdout,
|
|
300
|
+
stderr,
|
|
301
|
+
executionTimeMs: performance.now() - started,
|
|
302
|
+
...(timedOut ? { timedOut: true, killed: true } : {}),
|
|
303
|
+
...(response.truncated
|
|
304
|
+
? {
|
|
305
|
+
stdoutTruncated: true,
|
|
306
|
+
stderrTruncated: true,
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Whether the adapter currently knows the remote sandbox to be running. */
|
|
313
|
+
async isReady(): Promise<boolean> {
|
|
314
|
+
if (this.activeFallback) {
|
|
315
|
+
return this.activeFallback.isReady?.() ?? this.activeFallback.status === "running";
|
|
316
|
+
}
|
|
317
|
+
if (this.status === "running") return true;
|
|
318
|
+
try {
|
|
319
|
+
const sandbox = await this.get();
|
|
320
|
+
this.status =
|
|
321
|
+
sandbox.status?.state === "SANDBOX_STATE_RUNNING" ? "running" : remoteStatus(sandbox);
|
|
322
|
+
return this.status === "running";
|
|
323
|
+
} catch (caught) {
|
|
324
|
+
if (isStatus(caught, 404)) return false;
|
|
325
|
+
throw caught;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Report Mastra-facing sandbox metadata. */
|
|
330
|
+
async getInfo(): Promise<SandboxInfo> {
|
|
331
|
+
if (this.activeFallback) {
|
|
332
|
+
return (
|
|
333
|
+
(await this.activeFallback.getInfo?.()) ?? {
|
|
334
|
+
id: this.activeFallback.id,
|
|
335
|
+
name: this.activeFallback.name,
|
|
336
|
+
provider: this.activeFallback.provider,
|
|
337
|
+
status: this.activeFallback.status,
|
|
338
|
+
createdAt: this.createdAt,
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const sandbox = await this.get();
|
|
343
|
+
this.status = remoteStatus(sandbox);
|
|
344
|
+
return {
|
|
345
|
+
id: this.id,
|
|
346
|
+
name: sandbox.display_name ?? this.name,
|
|
347
|
+
provider: this.provider,
|
|
348
|
+
status: this.status,
|
|
349
|
+
createdAt: this.remoteCreatedAt ?? this.createdAt,
|
|
350
|
+
...(this.lastUsedAt ? { lastUsedAt: this.lastUsedAt } : {}),
|
|
351
|
+
metadata: {
|
|
352
|
+
resourceName: sandbox.name,
|
|
353
|
+
remoteState: sandbox.status?.state,
|
|
354
|
+
updateTime: sandbox.update_time,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Explain the Databricks execution boundary to the agent. */
|
|
360
|
+
getInstructions(): string {
|
|
361
|
+
return [
|
|
362
|
+
"Commands run in a persistent Databricks serverless sandbox, not in the App container.",
|
|
363
|
+
"Use shell commands normally. The sandbox home directory survives inactivity stops.",
|
|
364
|
+
"If Databricks Sandbox is unavailable, commands fall back to Pydantic Monty and must be Python source.",
|
|
365
|
+
"Databricks Workspace filesystem mounts remain separate unless a command copies data explicitly.",
|
|
366
|
+
].join(" ");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private get resourcePath(): string {
|
|
370
|
+
return `${SANDBOX_API_PATH}/${encodeURIComponent(this.id)}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private async getOrCreate(signal?: AbortSignal) {
|
|
374
|
+
try {
|
|
375
|
+
return await this.get(signal);
|
|
376
|
+
} catch (caught) {
|
|
377
|
+
if (!isStatus(caught, 404)) throw caught;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const sandbox = await this.request(SANDBOX_API_PATH, "POST", SandboxResponseSchema, {
|
|
381
|
+
query: { sandbox_id: this.id },
|
|
382
|
+
payload: {
|
|
383
|
+
display_name: this.displayName,
|
|
384
|
+
spec: {
|
|
385
|
+
compute: {
|
|
386
|
+
inactivity_timeout: this.inactivityTimeout,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
signal,
|
|
391
|
+
});
|
|
392
|
+
this.rememberCreatedAt(sandbox.create_time);
|
|
393
|
+
return sandbox;
|
|
394
|
+
} catch (caught) {
|
|
395
|
+
if (!isStatus(caught, 409)) throw caught;
|
|
396
|
+
return this.get(signal);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private async get(signal?: AbortSignal) {
|
|
401
|
+
const sandbox = await this.request(this.resourcePath, "GET", SandboxResponseSchema, {
|
|
402
|
+
signal,
|
|
403
|
+
});
|
|
404
|
+
this.rememberCreatedAt(sandbox.create_time);
|
|
405
|
+
return sandbox;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private mutate(operation: "start" | "stop", signal?: AbortSignal) {
|
|
409
|
+
return this.request(`${this.resourcePath}/${operation}`, "POST", SandboxResponseSchema, {
|
|
410
|
+
signal,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async waitForState(expected: string, signal?: AbortSignal) {
|
|
415
|
+
const deadline = Date.now() + this.startupTimeoutMs;
|
|
416
|
+
while (Date.now() < deadline) {
|
|
417
|
+
const sandbox = await this.get(signal);
|
|
418
|
+
if (sandbox.status?.state === expected) return sandbox;
|
|
419
|
+
await async.sleep(SANDBOX_POLL_INTERVAL_MS, signal);
|
|
420
|
+
}
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Databricks sandbox ${this.id} did not reach ${expected} within ${this.startupTimeoutMs}ms`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private rememberCreatedAt(value: string | undefined): void {
|
|
427
|
+
if (!value) return;
|
|
428
|
+
const parsed = new Date(value);
|
|
429
|
+
if (!Number.isNaN(parsed.getTime())) this.remoteCreatedAt = parsed;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private async request<T>(
|
|
433
|
+
path: string,
|
|
434
|
+
method: "GET" | "POST" | "DELETE",
|
|
435
|
+
schema: z.ZodType<T>,
|
|
436
|
+
options: {
|
|
437
|
+
payload?: unknown;
|
|
438
|
+
query?: Record<string, string>;
|
|
439
|
+
signal?: AbortSignal;
|
|
440
|
+
} = {},
|
|
441
|
+
): Promise<T> {
|
|
442
|
+
const response = await this.client.apiClient.request(
|
|
443
|
+
{
|
|
444
|
+
path,
|
|
445
|
+
method,
|
|
446
|
+
query: options.query,
|
|
447
|
+
headers: new Headers({
|
|
448
|
+
Accept: "application/json",
|
|
449
|
+
...(options.payload === undefined ? {} : { "Content-Type": "application/json" }),
|
|
450
|
+
}),
|
|
451
|
+
raw: false,
|
|
452
|
+
...(options.payload === undefined ? {} : { payload: options.payload }),
|
|
453
|
+
},
|
|
454
|
+
options.signal ? databricks.toContext(options.signal) : undefined,
|
|
455
|
+
);
|
|
456
|
+
return schema.parse(response);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function commandScript(command: string, args: readonly string[], cwd: string | undefined): string {
|
|
461
|
+
const invocation = args.length ? [command, ...args].map(shellQuote).join(" ") : command;
|
|
462
|
+
return cwd ? `cd -- ${shellQuote(cwd)} && ${invocation}` : invocation;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function shellQuote(value: string): string {
|
|
466
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function definedEnvironment(input: NodeJS.ProcessEnv | undefined): Record<string, string> {
|
|
470
|
+
return Object.fromEntries(
|
|
471
|
+
Object.entries(input ?? {}).filter(
|
|
472
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isStatus(caught: unknown, status: number): boolean {
|
|
478
|
+
return error.errorContext(caught).statusCode === status;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function remoteStatus(sandbox: z.infer<typeof SandboxResponseSchema>): ProviderStatus {
|
|
482
|
+
switch (sandbox.status?.state) {
|
|
483
|
+
case "SANDBOX_STATE_RUNNING":
|
|
484
|
+
return "running";
|
|
485
|
+
case "SANDBOX_STATE_STOPPED":
|
|
486
|
+
return "stopped";
|
|
487
|
+
case "SANDBOX_STATE_STOPPING":
|
|
488
|
+
return "stopping";
|
|
489
|
+
case "SANDBOX_STATE_PENDING":
|
|
490
|
+
return "starting";
|
|
491
|
+
default:
|
|
492
|
+
return "pending";
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function resolveFallback(
|
|
497
|
+
configured: Exclude<DatabricksSandboxOptions["fallback"], false | undefined>,
|
|
498
|
+
): WorkspaceSandbox {
|
|
499
|
+
if (typeof configured === "object" && "provider" in configured && "status" in configured) {
|
|
500
|
+
return configured;
|
|
501
|
+
}
|
|
502
|
+
return new MontySandbox(configured === "monty" ? {} : configured);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isDatabricksSandboxUnavailable(caught: unknown): boolean {
|
|
506
|
+
const context = error.errorContext(caught);
|
|
507
|
+
return (
|
|
508
|
+
context.statusCode === 404 ||
|
|
509
|
+
context.hasMessage("feature", "disabled") ||
|
|
510
|
+
context.hasMessage("sandbox", "not", "enabled") ||
|
|
511
|
+
context.hasMessage("preview", "not", "enabled") ||
|
|
512
|
+
context.hasMessage("preview", "unavailable") ||
|
|
513
|
+
context.hasMessage("preview", "disabled")
|
|
514
|
+
);
|
|
515
|
+
}
|
package/src/serving-sanitize.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Outbound ({@link rewriteServingBody}), because the transcript Mastra
|
|
6
6
|
* persists is not always a transcript the provider will accept back:
|
|
7
7
|
* Databricks-hosted Claude rejects replayed extended-thinking blocks and reads
|
|
8
|
-
* a trailing assistant message as a prefill request
|
|
8
|
+
* a trailing assistant message as a prefill request, while GPT Astra requires
|
|
9
|
+
* `reasoning_effort: "none"` when Chat Completions carries function tools.
|
|
9
10
|
*
|
|
10
11
|
* Inbound ({@link rewriteServingResponseBody} and
|
|
11
12
|
* {@link rewriteServingResponseStream}), because Databricks-hosted Gemini and
|
|
@@ -56,7 +57,8 @@ export function rewriteServingBody(body: string): string {
|
|
|
56
57
|
|
|
57
58
|
// Runs regardless of `messages`: Databricks refuses to parse a body carrying
|
|
58
59
|
// an unknown top-level field, so this failure is not specific to a transcript.
|
|
59
|
-
|
|
60
|
+
const astraTools = applyAstraToolCompatibility(parsed);
|
|
61
|
+
let changed = openaiChat.stripUnsupportedChatFields(parsed).length > 0 || astraTools;
|
|
60
62
|
|
|
61
63
|
if (Array.isArray(parsed.messages)) {
|
|
62
64
|
const messages = parsed.messages as ServingChatMessage[];
|
|
@@ -70,6 +72,52 @@ export function rewriteServingBody(body: string): string {
|
|
|
70
72
|
return changed ? JSON.stringify(parsed) : body;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/** Prepared fetch arguments plus the post-sanitize body used for diagnostics. */
|
|
76
|
+
export interface RewrittenServingRequest {
|
|
77
|
+
input: Parameters<typeof fetch>[0];
|
|
78
|
+
init: Parameters<typeof fetch>[1];
|
|
79
|
+
body: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read and rewrite a serving POST whether its JSON body lives on `init` or on
|
|
84
|
+
* a `Request`. A changed Request is rebuilt without stale byte headers.
|
|
85
|
+
*/
|
|
86
|
+
export async function rewriteServingRequest(
|
|
87
|
+
input: Parameters<typeof fetch>[0],
|
|
88
|
+
init?: Parameters<typeof fetch>[1],
|
|
89
|
+
): Promise<RewrittenServingRequest> {
|
|
90
|
+
const request = new Request(input, init);
|
|
91
|
+
const body = await request.clone().text();
|
|
92
|
+
const rewritten = rewriteServingBody(body);
|
|
93
|
+
if (rewritten === body) return { input, init, body };
|
|
94
|
+
|
|
95
|
+
const headers = new Headers(request.headers);
|
|
96
|
+
headers.delete("content-length");
|
|
97
|
+
headers.delete("content-encoding");
|
|
98
|
+
return {
|
|
99
|
+
input: new Request(request, { body: rewritten, headers }),
|
|
100
|
+
init: undefined,
|
|
101
|
+
body: rewritten,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add the Chat Completions option Databricks-hosted GPT Astra requires when
|
|
107
|
+
* function tools are present. An explicit caller value always wins.
|
|
108
|
+
*/
|
|
109
|
+
export function applyAstraToolCompatibility(body: Record<string, unknown>): boolean {
|
|
110
|
+
if ("reasoning_effort" in body || !Array.isArray(body.tools) || body.tools.length === 0) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const model = string.trimToNull(body.model);
|
|
114
|
+
if (!model) return false;
|
|
115
|
+
const tokens = new Set(string.tokenizeWithOptions({ lowerCase: true }, model));
|
|
116
|
+
if (!tokens.has("gpt") || !tokens.has("astra")) return false;
|
|
117
|
+
body.reasoning_effort = "none";
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
/**
|
|
74
122
|
* Drop extended-thinking / reasoning blocks from a replayed transcript.
|
|
75
123
|
*
|