Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/sandbox-commands-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"inngest": patch
---

Experimental: code running in a sandbox can now call functions in your app. Pass `tools` to `commands.run`, and each call the code makes runs your function as its own durable step, so it's retried and memoized like any other step.

```ts
await sandbox.commands.run("agent", "node agent.js", {
tools: {
getCustomer: (id: string) => db.customers.find(id),
},
});
```

```js
// agent.js, inside the sandbox
const { tools } = require("inngest_tools");
const customer = await tools.getCustomer("cus_1");
```

In Python, use `from inngest_tools import tools` and `await tools.getCustomer("cus_1")`. A tool that fails raises a `ToolError` the code can catch.
20 changes: 19 additions & 1 deletion packages/inngest/src/components/sandbox/durable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type SandboxResultForAction,
validateSandboxResult,
} from "./protocol.ts";
import type { SandboxToolsRequest } from "./tools.ts";
import { sdkOrigin } from "./trace.ts";
import {
type DurableSandbox,
Expand All @@ -52,6 +53,7 @@ import {
decodeOutputChunk,
decodeUtf8,
encodeBase64,
maxSandboxProcessTimeoutMs,
normalizeSandboxCommandOptions,
normalizeSandboxCreateOptions,
normalizeSandboxListOptions,
Expand Down Expand Up @@ -480,9 +482,17 @@ const callRawTool = async <
idOrOptions: StepOptionsOrId,
operation: SandboxOperationForAction<A>,
finish?: (result: SandboxResultForAction<A>) => T | Promise<T>,
tools?: SandboxToolsRequest,
): Promise<T> => {
try {
const rawTool = await rawToolResolver();
if (tools) {
return validateSandboxResult(
operation,
await rawTool(idOrOptions, operation, undefined, tools),
) as T;
}

if (finish) {
return (await rawTool(idOrOptions, operation, (result) => {
return finish(validateSandboxResult(operation, result));
Expand Down Expand Up @@ -755,7 +765,13 @@ export const createDurableSandboxFacade = (
resources: Object.freeze({ ...ref.resources }),
commands: Object.freeze({
run: async (idOrOptions, command, options) => {
const normalized = normalizeSandboxCommandOptions(command, options);
const { tools, toolOptions, ...commandOptions } = options ?? {};
const normalized = normalizeSandboxCommandOptions(command, {
...commandOptions,
...(tools && {
timeout: commandOptions.timeout ?? maxSandboxProcessTimeoutMs,
}),
});
const { timeout: _timeout, ...input } = normalized;
const operation = parseSandboxOperationForAction("exec", {
protocolVersion: sandboxProtocolVersion,
Expand All @@ -767,6 +783,8 @@ export const createDurableSandboxFacade = (
rawToolResolver,
idOrOptions,
operation,
undefined,
tools && { tools, toolOptions },
);
return {
stdout: decodeUtf8(
Expand Down
206 changes: 134 additions & 72 deletions packages/inngest/src/components/sandbox/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { StepOptionsOrId } from "../../types.ts";
import { getAsyncCtxSync } from "../execution/als.ts";
import type { Inngest } from "../Inngest.ts";
import type { MetadataKind, MetadataOpcode } from "../InngestMetadata.ts";
import { Middleware } from "../middleware/middleware.ts";
import { NonRetriableError } from "../NonRetriableError.ts";
import { sandboxForOperation } from "./client.ts";
import { createSandboxTools, executeSandboxOperation } from "./durable.ts";
import { createGroupSandbox, type GroupSandbox } from "./group.ts";
import {
Expand All @@ -12,7 +15,12 @@ import {
type SandboxRawTool,
} from "./protocol.ts";
import { createOneShotRunner, type OneShotRunner } from "./runners.ts";
import { sandboxMetadataKind, sandboxTraceMetadata } from "./trace.ts";
import { runCommandWithTools } from "./toolRunner.ts";
import {
type SandboxTraceMetadata,
sandboxMetadataKind,
sandboxTraceMetadata,
} from "./trace.ts";
import {
type DurableSandboxTools,
SandboxError,
Expand All @@ -32,12 +40,13 @@ type SandboxGroupExtension = {
};

/**
* Attach `inngest.sandbox` metadata to the step that's executing, describing
* the sandbox action for the trace. Tracing must never fail the step.
* Attach metadata to the step that's executing. Tracing must never fail the
* step.
*/
const describeStep = (
operation: unknown,
outcome: { result: SandboxOperationResultV1 } | { error: unknown },
const addStepMetadata = (
kind: MetadataKind,
values: () => Record<string, unknown> | undefined,
op: MetadataOpcode = "merge",
): void => {
try {
const execution = getAsyncCtxSync()?.execution;
Expand All @@ -46,39 +55,86 @@ const describeStep = (
return;
}

const parsed = parseSandboxOperation(operation);

execution.instance.addMetadata(
stepId,
sandboxMetadataKind,
"step",
"merge",
sandboxTraceMetadata(
parsed,
"result" in outcome
? outcome
: { error: getSandboxError(outcome.error) },
),
const metadata = values();

if (metadata) {
execution.instance.addMetadata(stepId, kind, "step", op, metadata);
}
} catch {
// An operation that fails validation has nothing to describe.
}
};

const describeStep = (
operation: unknown,
outcome: { result: SandboxOperationResultV1 } | { error: unknown },
): void => {
addStepMetadata(sandboxMetadataKind, () => {
return sandboxTraceMetadata(
parseSandboxOperation(operation),
"result" in outcome ? outcome : { error: getSandboxError(outcome.error) },
);
});

if ("error" in outcome) {
addStepMetadata(
"inngest.warnings",
() => {
const parsed = parseSandboxOperation(operation);

if (parsed.action !== "destroy") {
return;
}

if ("error" in outcome && parsed.action === "destroy") {
execution.instance.addMetadata(
stepId,
"inngest.warnings",
"step",
"set",
{
return {
"sdk.sandbox.destroy": `Sandbox ${operationSandboxId(parsed)} couldn't be destroyed: ${
outcome.error instanceof Error
? outcome.error.message
: String(outcome.error)
}`,
},
);
};
},
"set",
);
}
};

/**
* What a failed sandbox step throws: retryable errors retry, the rest do not.
*/
const toStepError = (error: unknown): unknown => {
if (error instanceof SandboxError) {
const cause = {
protocolVersion: error.protocolVersion,
action: error.action,
code: error.code,
message: error.message,
...(error.status !== undefined && { status: error.status }),
...(error.sandboxId !== undefined && { sandboxId: error.sandboxId }),
...(error.processId !== undefined && { processId: error.processId }),
...(error.snapshotId !== undefined && { snapshotId: error.snapshotId }),
ambiguous: error.ambiguous,
retryable: error.retryable,
...(error.requestId !== undefined && { requestId: error.requestId }),
details: [...error.details],
};
if (error.retryable) {
const retryableError = new Error(error.message, { cause });
retryableError.name = error.name;
return retryableError;
}
} catch {
// An operation that fails validation has nothing to describe.
return new NonRetriableError(error.message, { cause });
}
if (error instanceof SandboxValidationError) {
return new NonRetriableError(error.message, {
cause: {
protocolVersion: sandboxProtocolVersion,
type: "sandbox_validation_error" as const,
message: error.message,
},
});
}
return error;
};

const executeAsStep = async (
Expand All @@ -91,46 +147,7 @@ const executeAsStep = async (
return result;
} catch (error) {
describeStep(operation, { error });
if (error instanceof SandboxError) {
const cause = {
protocolVersion: error.protocolVersion,
action: error.action,
code: error.code,
message: error.message,
...(error.status !== undefined && { status: error.status }),
...(error.sandboxId !== undefined && {
sandboxId: error.sandboxId,
}),
...(error.processId !== undefined && {
processId: error.processId,
}),
...(error.snapshotId !== undefined && {
snapshotId: error.snapshotId,
}),
ambiguous: error.ambiguous,
retryable: error.retryable,
...(error.requestId !== undefined && {
requestId: error.requestId,
}),
details: [...error.details],
};
if (error.retryable) {
const retryableError = new Error(error.message, { cause });
retryableError.name = error.name;
throw retryableError;
}
throw new NonRetriableError(error.message, { cause });
}
if (error instanceof SandboxValidationError) {
throw new NonRetriableError(error.message, {
cause: {
protocolVersion: sandboxProtocolVersion,
type: "sandbox_validation_error" as const,
message: error.message,
},
});
}
throw error;
throw toStepError(error);
}
};

Expand All @@ -154,8 +171,52 @@ export class SandboxMiddleware extends Middleware.BaseMiddleware {
SandboxGroupExtension;
};
} {
const rawTool: SandboxRawTool = (idOrOptions, operation, finish) =>
arg.ctx.step.run(
const { step, runId } = arg.ctx;
const rawTool: SandboxRawTool = (idOrOptions, operation, finish, tools) => {
if (tools && operation.action === "exec") {
return runCommandWithTools(
Comment thread
jpwilliams marked this conversation as resolved.
{
run: async <T>(
idOrOptions: StepOptionsOrId,
fn: () => Promise<T>,
meta?: SandboxTraceMetadata,
) => {
return (await step.run(idOrOptions, async () => {
try {
const result = await fn();
if (meta) {
addStepMetadata(sandboxMetadataKind, () => {
return meta;
});
}

return result;
} catch (error) {
if (meta) {
addStepMetadata(sandboxMetadataKind, () => {
const code = getSandboxError(error)?.code;

return code ? { ...meta, error_code: code } : meta;
});
}

throw toStepError(error);
Comment thread
jpwilliams marked this conversation as resolved.
}
})) as T;
},
sandbox: sandboxForOperation(
this.client.sandboxes,
operation.target.sandbox,
),
runId,
},
idOrOptions,
operation,
tools,
);
}

return step.run(
idOrOptions,
async (input) => {
const result = await executeAsStep(this.client, input);
Expand All @@ -164,6 +225,7 @@ export class SandboxMiddleware extends Middleware.BaseMiddleware {
},
operation,
);
};

const sandbox = createSandboxTools(() => rawTool);

Expand Down
5 changes: 5 additions & 0 deletions packages/inngest/src/components/sandbox/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod/v3";

import type { StepOptionsOrId } from "../../types.ts";
import type { SandboxToolsRequest } from "./tools.ts";
import {
type SandboxAction,
SandboxError,
Expand Down Expand Up @@ -675,6 +676,10 @@ export type SandboxRawTool = (
idOrOptions: StepOptionsOrId,
operation: SandboxOperationV1,
finish?: (result: unknown) => unknown,
/**
* Makes an `exec` run with tools the code can call.
*/
tools?: SandboxToolsRequest,
) => Promise<unknown>;
export type SandboxRawToolResolver = () =>
| SandboxRawTool
Expand Down
Loading
Loading